NestJS
[NestJS] JWT 인증 가드 작성 방법에 대한 두가지 방법 및 나의 생각
개발조하
2023. 12. 26. 16:46
개요
- 인프런 강의를 듣다가 지금까지 못본 방법으로 jwt 인증을 구현하는 것을 보았다.
- 내가 기존에 알고있던 방법과 새로 알게된 방법을 보고 이에 대한 차이점과 앞으로는 나는 어떤 방법을 통해서 개발할지를 생각해보자.
기존 방식으로 구현하는 법 (내가 원래 알고있던 방식)
내가 기존에 알고있는 방식은 PassportStrategy
를 사용하는 방식이였다. 사용 방식은 다음과 같다.
사용할 모듈에
JwtModule
등록// auth.module.ts @Module({ imports: [ PassportModule, JwtModule.register({ // JwtModule 등록 부분 secret: process.env.JWT_SECRET || jwtConfig['accessSecret'], // 비밀키 등록 signOptions: { // default 유효 기간 셋팅 expiresIn: jwtConfig['accessTokenExpiresIn'], }, }), ], controllers: [AuthController], providers: [ AuthService, AuthRepository, LocalStrategy, JwtRefreshStretagy, JwtAccessStretagy, ], }) export class Auth...
strategy 작성
local.strategy.ts
import { PassportStrategy } from '@nestjs/passport'; import { AuthService } from '../auth.service'; import { Strategy } from 'passport-local'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; @Injectable() export class LocalStrategy extends PassportStrategy(Strategy) { constructor(private readonly authService: AuthService) { super({ usernameField: 'user_id', // username 으로 쓸 필드 이름 passwordField: 'password', // 비밀번호로 쓸 필드 이름 passReqToCallback: false, }); } async validate(user_id, password) { console.log('[Guard] local guard'); const user = await this.authService.validateLocalStrategy( user_id, password, ); if (!user) { throw new HttpException( { message: '로그인에 실패하였습니다. (존재하지 않는 사용자 입니다.)' }, HttpStatus.UNAUTHORIZED, ); } return user; } }
accessToken.strategy.ts
import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import * as config from 'config'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { AuthService } from '../auth.service'; /** * PassportStrategy 인증을 진행할 때 사용할 전략을 정의 */ @Injectable() export class JwtAccessStretagy extends PassportStrategy(Strategy, 'access') { constructor(private readonly authService: AuthService) { super({ secretOrKey: process.env.JWT_SECRET || config.get('jwt.accessSecret'), ignoreExpiration: false, jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), }); } async validate(payload) { console.log('[Guard] access jwt guard'); const user = await this.authService.validateAccessTokenStretagy( payload.user_id, ); if (!user) { throw new HttpException( { message: '잘못된 사용자 입니다.' }, HttpStatus.UNAUTHORIZED, ); } return user; } }
컨트롤러에
AuthGuard
로 우리가 작성한 전략으로 가드를 수행하도록 등록// auth.controller.ts @Post('login') @UseGuards(AuthGuard('local')) // local 전략 async loginProcess(@Res() res: Response, @Body() user: LoginDto) {}
해당 방식의 특징으로는 PassportStrategy 객체에서 입력되는 토큰에 대해 사용자가 별도의 로직을 안해도 검증을 해준다는 점이 있다. 또한 토큰 검증 이후 사용자가 추가적으로 validate
함수를 통해서 인증 로직을 구현할 수 있다. validate
함수의 반환값은 request 객체에 삽입되게 된다. (ex. 2번 전략의 경우 request.user 에 반환값이 들어있다.)
새로운 방식
새로운 방식으로는 PassportStrategy 를 사용하는게 아닌 직접 토큰 파싱, jwt 검증, 헤더에 반환값 삽입 등을 구현하는 것이다. (물론 jwt 검증은 jwtService.verify 함수를 사용한다.) 사용방법은 다음과 같다.
사용할 모듈에
JwtModule
등록// auth.module.ts import { Module, forwardRef } from '@nestjs/common'; import { AuthService } from './auth.service'; import { AuthController } from './auth.controller'; import { JwtModule } from '@nestjs/jwt'; import { UsersModule } from 'src/users/users.module'; @Module({ imports: [JwtModule.register({}), forwardRef(() => UsersModule)], exports: [AuthService], controllers: [AuthController], providers: [AuthService], }) export class AuthModule {}
Custom Guard 작성
basic-token.guard.ts (local 과 비슷한 놈으로 사용자가 로그인시 id, password 를 그대로 주는게 아니라 base64(id:password) 값을 주는게 일반적이라고 한다..)
@Injectable() export class BasicTokenGuard implements CanActivate { constructor(private readonly authService: AuthService) {} async canActivate(context: ExecutionContext): Promise<boolean> { const req = context.switchToHttp().getRequest(); // {authorization: 'Basic asdkjadkljasld'} const rawToken = req.headers['authorization']; if (!rawToken) { throw new UnauthorizedException('권한이 없습니다.'); } // Basic asldjkhasdjklajsldkj -> asldjkhasdjklajsldkj 추출 const token = this.authService.extractTokenFromHeader(rawToken, false); // asldjkhasdjklajsldkj -> email, passowrd 추출 const { email, password } = this.authService.decodeBasicToken(token); const user = await this.authService.authenticateWithEmailAndPassword({ email, password, }); // 실행 컨텍스트에서 직접적으로 request 객체를 추출해서 user 값을 저장한다. req.user = user; return true; } }
bearer-token.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException, } from '@nestjs/common'; import { AuthService } from '../auth.service'; import { UsersService } from 'src/users/users.service'; @Injectable() export class BearerTokenGuard implements CanActivate { constructor( private readonly authService: AuthService, private readonly usersService: UsersService, ) {} async canActivate(context: ExecutionContext): Promise<boolean> { const req = context.switchToHttp().getRequest(); const rawToken = req.headers['authorization']; if (!rawToken) { throw new UnauthorizedException('토큰이 없습니다.'); } // Bearer asldjkhasdjklajsldkj -> asldjkhasdjklajsldkj 추출 const token = this.authService.extractTokenFromHeader(rawToken, true); // jwtService.verify 함수를 통해서 토큰 검증 const result = await this.authService.verifyToken(token); /** * request 에 넣을 정보 * * 1) 사용자 정보 - user * 2) token - token * 3) tokenType - access | refresh */ req.token = token; req.tokenType = result.type; const user = await this.usersService.getUserByEmail(result.email); req.user = user; return true; } } @Injectable() export class AccessTokenGuard extends BearerTokenGuard { async canActivate(context: ExecutionContext): Promise<boolean> { await super.canActivate(context); const req = context.switchToHttp().getRequest(); if (req.tokenType !== 'access') { throw new UnauthorizedException('Access Token이 아닙니다.'); } return true; } } @Injectable() export class RefreshTokenGuard extends BearerTokenGuard { async canActivate(context: ExecutionContext): Promise<boolean> { await super.canActivate(context); const req = context.switchToHttp().getRequest(); if (req.tokenType !== 'refresh') { throw new UnauthorizedException('Refresh Token이 아닙니다.'); } return true; } }
컨트롤러에 가드를 등록해준다.
@Post('token/access') @UseGuards(RefreshTokenGuard) postTokenAccess(@Headers('authorization') rawToken: string) {
두가지 방법에 대한 나의 생각
- 두가지 방법 모두 간결하고 쉬워서 복잡하고 그럴게 없어보인다.
- 다만 개인적으로
PassportStrategy
사용하는 방식이 더 끌린다.- 이유1 : 토큰 검증에 대해서 사용자가 별도로 서비스 로직을 구현하지 않아도 됨.
- 이유2: validate 라는 메소르도 분리되어 해당 메소드 안에 토큰 검증 이후 추가적으로 로직을 구현할 수가 있어서 각 역할이 분리되어있는게 개인적으로는 트러블 슈팅에 좋을 듯함 (ex. 토큰이 잘못된건지, 인증 로직이 잘못되었는지 구분)
- 물론 나는 두가지 방법 모두 좋다고 생각하고, 어떻게 구현하든 통일만 하면 상관없어보이긴하다.
- 위 두가지 방법을 모두 알고있는 다음 다른 회사 또는 오픈소스를 분석할 때 참고하면 될듯 하다.