이번 포스팅에서는 JWT를 사용하여 사용자의 인증 및 인가를 처리하고 이를 데코레이터로 구현하여 가드로 활용하는 방법에 대해 알아보도록 하겠습니다.
JWT 기본 개념
JWT는 Jason Web Token의 약자로 두 주체가 안전한 방식으로 클레임(claims)을 주고 받는 방법입니다. JWT에 포함된 클레임은 JSON 객체로 인코딩되는데요. 공식 웹사이트인 Jwt.io에서 JWT를 인코딩 또는 디코딩해볼 수 있습니다.
JWT 구성
JWT를 구현하기 전에 JWT를 사용하기 위해 알아야 할 기본 구성에 대해 살펴보도록 하겠습니다. JWT는 크게 3가지 요소인 헤더, 페이로드, 서명으로 구성되는데요. 각 요소는 .
으로 구분합니다.
헤더(header)
헤더에는 JWT 유형과 알고리즘이 담깁니다.
{
"typ": "JWT",
"alg": "HS256"
}
페이로드(payload)
페이로드에는 클레임 정보가 담깁니다.
{
"iss": "발급자",
"sub": "클레임 설명",
"aud": "수신자",
"exp": "만료 시간",
"nbf": "토큰 활성화 시점",
"iat": "토큰 발급 시점",
"jti": "JWT 토큰 식별자"
}
서명(signature)
서명은 생성한 토큰이 유효한지 검증합니다. 헤더에서 "alg": "HS256"
이라고 알고리즘을 지정했다면, 해당 방식으로 암호화해야 합니다.
여기서 중요한 것은 서명은 토큰의 유효성을 검사할 뿐이지 페이로드를 암호화하는 것이 아니라는 점입니다.
따라서 클레임에는 비밀번호와 같은 민감한 정보를 포함해서는 안 됩니다.
NestJS에서 JWT 구현하기
이제 NestJS에서 JWT를 구현하는 방법에 대해 살펴보도록 하겠습니다. 먼저, jwt 패키지를 설치해줍니다.
$ npm install --save @nestjs/jwt passport-jwt
$ npm install --save -dev @types/passport-jwt
다음으로 모듈에 다음과 같이 JwtModule
을 임포트해줍니다.
//auth.module.ts
@Module({
imports: [
JwtModule.registerAsync({
useFactory: () => ({
secret: process.env.AUTH_SECRET,
signOptions: {
expiresIn: '30m',
},
}),
}),
],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}
간단한 사용자 로그인 정보를 받아 토큰을 발급받아 보겠습니다. 먼저, 컨트롤러에 login
라우터를 하나 만들어보겠습니다.
// auth.controller.ts
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
async getTokenForUser(@Body() body) {
return this.authService.getTokenForUser(body);
}
}
이제 서비스를 구현할 차례입니다. 아래 코드는 컨트롤러에서 전달받은 username
과 email
을 jwt 페이로드에 담아 전달합니다.
// auth.service.ts
@Injectable()
export class AuthService {
constructor(private readonly jwtService: JwtService) {}
public getTokenForUser(user: User): string {
return this.jwtService.sign({
username: user.username,
email: user.email,
});
}
}
포스트맨에서 요청을 테스트해보면 아래와 같이 정상적으로 코드가 발급된 것을 확인할 수 있습니다.
이제 jwt 홈페이지로 이동하여 해당 토큰을 디코딩하면 페이로드에 담았던 정보를 확인할 수 있습니다.
NestJS에서 JWT 디코딩하기
발급한 토큰은 어떻게 디코딩할 수 있을까요? 먼저, 아래와 같이 jwt 토큰을 디코딩해 줄 strategy 파일을 생성합니다.
// jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { AuthService } from './auth.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(private authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: true,
secretOrKey: process.env.AUTH_SECRET,
});
}
// 위 로직을 통과하면 아래의 validate 함수가 실행됨
async validate(payload) {
return payload;
}
}
이후 컨트롤러로 이동하여 가드를 추가해줍니다.
// auth.controller.ts
@Get('profile')
@UseGuards(AuthGuard('jwt'))
async authTest(@Req() req) {
return req.user;
}
포스트맨에서 발급받은 코드를 넣고 실행해보면 다음과 같이 정상적으로 페이로드 부분을 획득할 수 있는 것을 확인할 수 있습니다.
커스텀 데코레이터 만들기
구현한 가드를 커스텀 데코레이터로 사용하여 원하는 곳에서 사용할 수도 있습니다. decorators 디렉토리를 하나 만들어주고, get-user.decorator.ts 파일을 하나 만들어주겠습니다.
//get-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { User } from '../../users/schemas/users.schema';
export const GetUser = createParamDecorator(
(data, ctx: ExecutionContext): User => {
const req = ctx.switchToHttp().getRequest();
return req.user;
},
);
이제 앞서 가드를 사용했던 컨트롤러 파일로 이동하여 새롭게 구성한 데코레이터를 임포트하여 사용할 수 있습니다.
@Get('profile')
@UseGuards(AuthGuard('jwt'))
async authTest(@GetUser() user: User) {
console.log(user);
return user;
}
참고 자료
'개발 > NestJS' 카테고리의 다른 글
NestJS 기초 (14) 이미지 파일 업로드하기 (0) | 2022.11.07 |
---|---|
NestJS 기초 (12) 가드를 사용한 인증과 인가 (0) | 2022.10.17 |
NestJS 기초 (11) API 문서 작성하기 (스웨거) (0) | 2022.10.10 |