인증과 인가 과정
- 인증(Authentication) :
식별 가능한 정보
를 입력하여 자신의 신원을 시스템에증명
- 아이디와 패스워드를 입력해 인증서버를 통해 로그인
- 인가(Authorization) :
인증된 사용자
에 대하여자원 접근 권한
을 확인- 로그인한 사용자가 자원서버에서의 글 쓰기와 같은 서비스를 이용할 수 있는지 확인
JWT(Json Web Token)
- 로그인 이후
인증
과 관련한 정보들을인코딩
한 상태로 담은토큰
- 토큰은 Header.Payload.Signature로 세 파트의 JSON 쌍이
인코딩(base64)
된 구조- Header(헤더) : 토큰의
종류
(typ)와해시 알고리즘 방식
(alg) - Payload(정보) : 토큰에 대한
정보
- 등록된 클레임 : 토큰에 대한 정보들을 담기 위해 이름이 이미 정해진 클레임(sub, lat)
- 공개 클레임 : 그 외 다른 정보들로 충돌이 방지된 이름을 가지고 있어야 함
- 비공개 클레임 : 클라이언트-서버 간 서비스를 공유하기 위한 정보를 담은 클레임
- Signature(서명) :
토큰의 무결성
을 위한암호화 코드
- Header의 인코딩 값과 Payload의 인코딩 값, 그리고 secret 키를 해쉬 함수로 생성
- 헤더에서 지정한 알고리즘을 통해 암호화
- 복호화도 가능
- Header(헤더) : 토큰의
Cookie
와Session
의 단점을 보완할 수 있다.- Cookie
- 요청 보낼 때 값을 그대로 보내기 때문에, 중간에 유출 및 조작 당할 수 있음
- 용량 제한이 있기 때문에 많은 정보를 담을 수 없으며, 쿠키 사이즈가 클 수록 네트워크 부하가 심해짐
- 웹 브라우저 간 쿠키 지원 형태가 다르기 때문에, 브라우저 간 공유가 불가능
- Session
- 쿠키에 저장된 세션을 탈취하여, 공격자가 사용자인 척 위장할 수 있음(세션 하이재킹)
- 세션을 저장하기 위한 저장소를 사용하기 때문에 요청이 많아질 경우 서버 부하가 심해짐
- 확장성이 낮음
- JWT
- Header와 Payload를 가지고 Signature을 생성하므로 데이터 위변조를 막을 수 있음
- 인증 정보에 대한 별도의 저장소가 필요 없음
- 서버가 클라이언트 상태를 저장할 필요가 없어, 서버를 무상태(Stateless)로 만들 수 있으며, 확장성(Scalability)이 좋음
- 다른 서비스에 권공유(소셜 계정 로그인)하는 등, 로그인 정보의 확장성(Extensibility)이 좋음
- Cookie
Access Token & Refresh Token
- JWT는 발급 시 사용자의 인증과 인가를 무효화할 수 없기 때문에, 토큰 탈취 위험을 막기 위해 만료 시간을 짧게 줌
- 토큰 만료 시간을 짧게 주면 매번 다시 인증 과정을 거쳐야 하는 문제가 있기 때문에 도입된 개념
인증
ID
와Password
정보와 함께로그인 요청
- 데이터베이스에서 ID와 Password 대조
- 일치할 경우,
Access Token
과Refresh Token
응답 - 클라이언트에서 두 토큰을 저장
- 일치할 경우,
인가
Header
에Access Token
을 포함시켜API 요청
- 토큰의 유효성 검증
- Access Token 만료가 되지 않고 토큰 유효성 검증 완료 시 API 요청을 처리하고 응답
재발급
Access Token
이만료
될 경우, 만료가 되었음을 응답Header
에Refresh Token
을 포함시켜Access Token 재발급 요청
Refresh Token
의 유효성 검증하고, 새로운Access Token
을 응답- 클라이언트에서
Access Token
저장
- 클라이언트에서
NestJS에서 JWT 사용
프로젝트에서는
Access Token
발급하고 사용하는 과정만 진행하였습니다!
- JWT 모듈 설치
npm install @nestjs/passport passport passport-local # 인증 미들웨어 관련 모듈
npm install @nestjs/jwt passport-jwt # JWT 관련 모듈
npm install cookie-parser # 쿠키 관련한 모듈
npm install -D @types/passport-local @types/passport-jwt # Typescript 타입 정의 모듈
- 쿠키를 사용하기 위해
cookie-parser
Import
// main.ts
import { NestFactory } from '@nestjs/core';
import * as cookieParser from 'cookie-parser';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(cookieParser()); // cookieParser() 사용
// ...
}
bootstrap();
- 인증 모듈에
JWT 모듈
Import
// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
JwtModule.registerAsync({
useFactory: (configService: ConfigService) => {
return {
// JWT 비밀키 값 (추후에 .env 파일에 저장하여 가져와주는 것이 좋다.)
secret: 'secret',
// JWT 서명 옵션
signOptions: {
// 토큰 만료 시간, 단위가 없으면 단위 ms(ex: 24h, 120)
expiresIn: '24h',
// algorithm: 사용할 알고리즘(default: HS256)
// notBefore: 토큰 발급 후 사용을 허용하는 시간, 단위가 없으면 단위 ms
// audience: 토큰 발급 대상
// issuer: 토큰 발급자
// jwtid: jwt 식별자
// subject: 토큰의 주제
// noTimestamp: 토큰에 타임스탬프 포함 여부
// header: JWT 헤더 설정
// keyid: 키 식별자 설정
},
};
},
inject: [ConfigService],
}),
],
// ...
})
export class AuthModule {
}
- 로그인 시
JWT Access Token
발급 및 인증
// auth/auth.controller.ts
@Controller('auth')
export class AuthController {
// ...
/** 사용자 로그인
* @Body signInUserDto 로그인 정보 */
@Post('/login')
@HttpCode(HttpStatus.OK)
async signIn(@Body() signInUserDto: SignInUserDto, @Res() res: Response) {
// 동록된 사용자 확인
const verifiedUser = await this.authService.verifyUser(signInUserDto);
// JWT Token 발급
const payload = { id: verifiedUser.id, username: verifiedUser.username };
const accessToken = await this.authService.getAccessToken(payload);
// Set-Cookie 헤더로 JWT 토큰 & 응답 body로 사용자 정보 반환
return res.cookie('accessToken', accessToken, { httpOnly: true }).json({
message: SuccessType.USER_SIGN_IN,
data: payload,
});
}
}
// auth/auth.service.ts
@Injectable()
export class AuthService {
/**
* 사용자 검증
* @return 검증된 User 객체
* @param signInUserDto LoginDto
*/
async verifyUser(signInUserDto: SignInUserDto): Promise<User> {
const user = await this.userRepository.findOneBy({
username: signInUserDto.username,
});
if (!user) {
throw new UnauthorizedException(ErrorType.USER_NOT_EXIST);
}
const isMatch = await this.comparePassword(
signInUserDto.password,
user.password,
);
if (!isMatch) {
throw new UnauthorizedException(ErrorType.PASSWORD_MISMATCH);
}
return user;
}
// ...
/** JWT Access Token 발급
* @param payload payload 요소 */
async getAccessToken(payload: any) {
return await this.jwtService.signAsync(payload);
}
}
JWT 인가
과정
@UseGuards(JwtAuthGuard) // 전역으로 사용할 수 있고, 메소드에 따로 사용할 수 있음
@Controller('users')
export class UserController {
// ...
}
// auth/guard/jwtAuth.guard.ts
@Injectable()
// AuthGuard(타입명)을 상속받는 것으로, 타입명을 설정해주면 그에 맞는 strategy를 따라감
export class JwtAuthGuard extends AuthGuard('jwt') {
handleRequest(err: any, user: any) {
if (err || !user) {
throw err || new UnauthorizedException('유효하지 않은 토큰입니다.');
}
return user;
}
}
// auth/strategy/jwt.strategy.ts
@Injectable()
// PassportStrategy(Strategy, '타입명, (default: jwt)')을 상속 받는 것으로 타입명에 그에 대한 strategy
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private readonly userLib: UserLib,
private readonly configService: ConfigService,
) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
(req: Request) => {
return req?.cookies?.accessToken;
},
]),
ignoreExpiration: false,
secretOrKey: 'secret',
});
}
// 사용자에 대한 유효성 확인
async validate(payload: { id: number; username: string }) {
const user = await this.userLib.getUserById(payload.id);
if (!user) {
throw new UnauthorizedException('유효하지 않는 사용자입니다.');
}
// req.user에 사용자 정보를 담음
return {
id: user.id,
username: user.username,
};
}
}
관련 Repository
- [ FeedMoa ] 소셜 미디어 통합 Feed 서비스 RESTful API 서버, laetipark, github
- [ 여기가자 ] 지리기반 맛집 추천 서비스 RESTful API 서버, laetipark, github
- [ 돈이머니 ] 예산 관리 서비스 RESTful API 서버, laetipark, github
Ghost