Nest.js Security Jwt strategy 적용

JWT(JSON Wen Token)

백엔드 개발자라면 지구 끝까지 따라와 묻게 되는 용어 JWT 알고나면 별로 어려운 용어는 아니지만 처음에 배울 때는 많이 힘들게 하는 친구중 하나입니다. 해당 글에서는 JWT에 대한 내용을 별도로 다루고 있지 않습니다. 그러므로 JWT에 대한 내용을 공부하시고자 한다면 아래 글을 참고 해주시기 바랍니다.

 

JWT(JSON Web Token)

JWT JWT는 유저를 인증하고 식벽하기 위한 인증 방식이다. 토큰은 세션과는 달리 서버가 아닌 클라이언트에 저장되기 때문에 메모리나 스토리지 등을 통해 세션을 관리했던 서버의 부담을 덜 수

jamesblog95.tistory.com

 

Token 발급 하기

발급을 해주기 위해서는 기본적인 jwt 설정이 필요로 합니다. /src/auth/auth.module.ts 에 모듈을 추가 해줍니다. 

  imports: [
    UserModule,
    PassportModule,
    JwtModule.register({
      global: true,
      secret: 'tempSecretKey',
      signOptions: { expiresIn: '1d' },
    }),
  ],

 

옵션 설명
global 전역으로 jwt를 설정하겠다는 옵션
secret jwt를 암호화시에 사용되는 secrey key
signOptions expired 기간을 지정하는 옵션

 

기본적인 로직은 회원가입이 완료된 회원이라는 전제하에 설명하도록 하겠습니다. 일단 회원가입이 마친 유저를 DB에서 찾아 Password를 검증하는 과정을 거치게 됩니다. 검증을 성공적으로 마치게 된다면 새로운 AccessToken을 발급을 해줍니다. 

@Injectable()
export class AuthService {
  constructor(private userService: UserService, private jwtService: JwtService) {}

  async signin(email: string, password: string) {
    const user = await this.userService.findOneByEmail(email);

    if (!user) throw new UnauthorizedException();

    const isMatch = password === user.password;

    if (!isMatch) throw new UnauthorizedException();

    return {
      accessToken: this.jwtService.sign({
        sub: user.id,
      }),
    };
  }
}

 

여기서 this.jwtService 라는 메소드를 사용하게 되는데 이전에 설정한 jwtModule 설정 값이 주입 받아서 사용되어지고 있습니다.

 

이렇게 만들어진 토큰은 로그인을 요청한 유저에게 보내지게 됩니다. 

토큰을 발급하는 것을 어렵지 않게 구현해나갈 수 있는 부분입니다. 

 

Token 인증하기

Token을 발급 하였으니 이제는 인증해야하는 과정을 구현해나가야 합니다. 인증 과정은 발급 과정보다 복잡하니 차근차근 읽어 보시기 바랍니다. 

 

먼저 /src/auth/jwt-auth.guard.ts  를 작성해줍니다. 

// jwt-auth.auard.ts

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super();
  }

  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (isPublic) return true;

    return super.canActivate(context);
  }
}

 

guard의 경우 하나의 검증 미들웨어라는 용어로 사용되고 있습니다. 여기서 jwt-guard의 경우 private한 엔드포인트와 public한 엔드포인트를 구분하는 하나의 미들웨어로 보시면 되겠습니다. 매개변수 중 IS_PUBLIC_KEY 라는 변수를 받고 있는데 해당 값이 true 라면 jwt 검증없이 사용가능한 엔드포인트를 제공하고 있다고 보시면 되겠습니다. 

 

코드 설명
extends AuthGuard('jwt') AuthGuard는 @nestjs/passport 패키지에서 제공하는 클래스입니다. 이를 상속받아 jwt 전략을 사용하도록 설정합니다.
constructor(private reflector: Reflector) Reflector는 Nest.js에서 제공하는 유틸리티 클래스로, 데코레이터에 대한 메타데이터를 추출할 때 사용됩니다.
이 클래스의 생성자에서는 Reflector를 의존성으로 주입받고 있습니다.
canActivate(context: ExecutionContext) AuthGuard 클래스에서 정의한 canActivate 메서드를 오버라이딩합니다.
this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [context.getHandler(), context.getClass()]); Reflector를 사용해 현재 요청의 메타데이터를 추출하고, 그 요청이 Public인지 여부를 판단합니다.
if (isPublic) return true;
만약 요청이 Public이라고 표시되어 있다면, 인증 검사 없이 접근을 허용합니다.
return super.canActivate(context);
그렇지 않다면, 부모 클래스(AuthGuard)의 canActivate 메서드를 호출하여 일반적인 JWT 인증을 수행합니다.

 

이제는 이러한 전략을 사용할 수 있도록 데코레이터를 작성해보도록 하겠습니다. /src/common/decorator/public.decorator.ts 파일을 생성하고 아래 코드를 작성하겠습니다. 

export const IS_PUBLIC_KEY = 'isPublic';

export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

 

이제 이렇게 생성한 데코레이터를 컨트롤러에 사용해주면 검증없이 통과되는 public 엔드포인트가 설정이 된 것입니다. 

  @Public()
  @Post('signin')
  async signin(@Body() { email, password }: SigninReqDto) {
    return this.authService.signin(email, password);
  }

 

 

이번에는 jwt를 검증하는 /src/auth/jwt.strategy.ts를 작성해주도록 하겠습니다. 

// jwt.strategy.ts
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false, 
      secretOrKey: 'tempSecretKey',
    });
  }

  async validate(payload: any) {
    return { id: payload.sub };
  }
}

 

각 과정에 따른 설명은 아래와 같습니다. 

코드 설명
class JwtStrategy extends PassportStrategy(Strategy) jwt을 활용하는 인증처리를 구현하기 위해 passport 라이브러리를 상속 받아 구현하는 클래스라는 것을 말해줍니다.
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken() header에 authentication bearer가 포함 되어져야 하는 전략을 구현.
ignoreExpiration: false
expiretime을 인증 한다면 false를 아니면 true
secretOrKey: 'tempSecretKey' 암호화시 사용된 secretKey를 복호화시 사용하게 함.
async validate(payload: any)
모든 검증이 완료가 되면 반환되는 값을 만들어 주는 메소드

 

각 내용들이 이름이 길어서 어려워 보이지만 각 요소들을 자세하게 보면 그나마 쉽게 이해 할 수 있습니다. 이렇게 인증이 완료된 토큰은 쉽게 꺼내서 사용 할 수 있게 Decorator를 작성 해줍니다. 

 

아래의 코드는 요청시 검증이 완료된 토큰에서 반환된 유저의 ID 넘버를 Decorator로 작성한 코드 입니다. 아래 UserAfterAuthID 값이 String 인 것을 증명하기 위한 타입 지정용 interface 입니다.

export const User = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
  const request = ctx.switchToHttp().getRequest();
  return request.user;
});

export interface UserAfterAuth {
  id: string;
}

 

마지막으로 컨트롤러에서 ID를 꺼내어서 사용하면 됩니다.

  @ApiBearerAuth()
  @ApiGetItemsResponse(FindUserResDto)
  @Get()
  @UseGuards(JwtAuthGuard)
  findAll(@Query() { page, size }: PageReqDto, @User() user: UserAfterAuth) {
    console.log(user);
    return this.userService.findAll();
  }