2020-3-10 NestJS에서 JWT인증

JWT

  • jwt전략을 사용하기 위해 아래와 같은 패키지를 설치한다.
$npm install @nestjs/jwt passport-jwt
$npm install @types/passport-jwt --save-dev
  • @nestjs/jwt : jwt를 다루는 패키지,
  • passport-jwt : jwt전략을 구현하는 passport패키지,
  • @types/passport-jwt : 타입스크립트의 정의한 패키지
  • POST /auth/login에서는 AuthoGuard를 통해 passport-local전략이 제공이된다. 이 라우터의 경우 user가 validate될때는 불러지고 여기서 JWT를 생성하고 리턴해줄수있다.
  • JWT를 authService에서 생성.
@Injectable()
export class AuthService {
  constructor(
    @Inject(forwardRef(() => AccountsService))
    private readonly accountsService: AccountsService,
    private readonly jwtService: JwtService,
  ) {}

  // Token에 있는 email과 실제 email을 비교.
  async validateAccount(payload: JwtPayload): Promise<any> {
    console.log(`validate account ${payload}`);
    console.log(`payload email : ${payload.email}`);
    const account: Account = await this.accountsService.findByEmail(payload.email);
    if (account.getEmail() == payload.email) {
      console.log(`validateAccount Success`);
      const {...result} = account;
      return result;
    }
    return null;
  }

  // Account를 받으면 JWT토큰을 새로 발급한다.
  makeAccessToken(account: Account) {
    return this.jwtService.sign({'email': account.getEmail()});
  };
}


// /auth/constants.js
export const jwtConstants = {
  secret: 'secretKey',
};

  • @nest/jwt에서 sign()이 제공되고 sign()에서 우리의 account object의 properties를 보면서 JWT가 생성이 되고 리턴해준다.
  • secretKey를 노출시키면 안된다 !!!
//jwt.strategy.ts
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
    });
  }

  // controller에 @UseGuards(JwtAuthGuard) 붙은 메서드들은 여기를 사전에 거쳐서 검증을 한다.
  async validate(payload: JwtPayload) {
    const account = await this.authService.validateAccount(payload);
    if (!account)
      throw new UnauthorizedException();
    return account;
  }
}



//auth.module.ts
@Module({
  imports: [
    AccountsService,
    PassportModule,
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: {expiresIn: '60s'},
    }),
  ],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

//
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

  • jwtstrategy역시 passport strategy와 같은 전략을 따라간다. 시작할때 몇몇 옵션이 필요하고 super()를 통해 필요한 옵션들을 제공한다.
  • jwtFromRequest: Request로부터 JWT를 추출한다. 표준은 Request의 헤더인 Authorization header에서 bearer token을 사용하는것이다.
  • validate(): jet-strategy를 사용할때, passport는 JWT 사인을 json으로 디코드한다. 그리고 여기서 validate()를 call한다.
  • 이전에 서명한 사용자가 유효한지 검증한다. 이 다음 validate()의 return으로 정보들이 들어있는 객체를 Request객체에 리턴한다.
  • 검증은 passport에서 하고, jwt 서명은 authservice에서 한다.
@UseGuards(JwtAuthGuard)
@Put(':id')
async update(@Request() req): Promise<ResponseDto> {
  console.log(`PUT /accounts/:id param ${JSON.stringify(req.params)}`);
  if (!this.accountsService.compareUserId(req.params.id, req.user.id))
    throw new UnauthorizedException("수정권한이 없습니다.");
  return await this.accountsService.updateAccount(req.params.id, req.body.email, req.body.password);
}
  • jwt인증을 하기 위해서는 Controller에서 @UseGuards와 같이 어노테이션을 붙인다. 이 어노테이션이 붙은 컨트롤러들은 요청이 들어오면 jwt.strategy에 정의되어있는 validate메서드를 통과한다.
  • 처음에는 토큰값에 email, password만 제공을 했지만, 해당유저인지 아닌지 판별하면 되므로 나중에는 email값만 맞으면 인증을 처리하도록 했다.
  • user의 email이 확인되면 parameter로 전달된 id로 user를 조회하고 토큰에 있는 user를 비교검증한다.

느낀점

  • 프로젝트를 하면서 로그인, 인증에 대한 구현은 해본적이 거의 없었다. 대부분 개인프로젝트를 하더라도 로그인을 간단하게 구현만 하는 정도이고 거기다 좀 더 신경을 써도 암호화 정도만 조금 신경을 쓰는정도였다. 인증이라는 개념 자체가 아무리 라이브러리가 많이 나오고 사용하기 편하다고 하더라도 기본적인 동작원리를 알아야됬다. JWT를 이용해서 인증을 구현하라고 했을때 처음에 너무나도 막막했다. 특히 나는 jwt를 쓸거기때문에 passport-jwt만 사용하고 passport-local에 대한 고려를 전혀안했었는데, 계속 공식문서를 보다보니 각 기능에 대한 역할을 습득할 수 있었다. 토큰을 발급하고 해당 유저를 인증하고 또 인증을 하고 난 뒤에 새로 토큰을 발급하고 리셋하는 그 과정이 즐거웠다. 좋은 경험을 해서 좋았다 :)
Written on March 10, 2020