nodejs/nestjs

jwt role guard 적용기

펀치맨 2022. 2. 7. 22:02

✨ jwt + role guard 를 적용해 보자 !

jwt 인증과 payload 에 심어진 role로 guard 를 적용하자
(예시를 위하여 jwt access token은 post 요청시 body에 id 와 role 을 전달하면 받을수 있게 함 !)

install

yarn add @nestjs/config @nestjs/jwt @nestjs/passport passport passport-jwt

공통 적으로 사용하는 값들

export enum Role {
  role1 = 'role1',
  role2 = 'role2',
}

export class AuthDto {
  @ApiProperty()
  @IsString()
  id: string;

  @ApiProperty()
  @IsEnum(Role)
  role: Role;
}

// role 적용을 위한 key 값
export const ROLES_KEY = 'roles';

auth.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';

import { JwtStrategy } from '#common/auth/Jwt.strategy';
import { AuthController } from '#controller/auth.controller';
import { AuthService } from '#service/auth.service';

@Module({
  imports: [
      JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        secret: config.get('JWT_SECRET_KEY'),
        signOptions: { expiresIn: '1h' },
      }),
    }),
    TypeOrmModule.forFeature()
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy],
})

export class AuthModule {}

JwtModule import 할때 registe로 적용하면, import가 config파일을 불러오기 전에 호출되기 때문에 환경 변수 값을 들고오지 못한다.

그래서 registerAsyncConfigModule를 사용하여 환경 변수 값을 적용했다.

auth.controller.ts

@Controller('/auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post()
  async getToken(@Body() auth: AuthDto) {
    if ((await this.authService.validateUser(auth)) !== true)
      return {
        error: {
          id: 'UNAUTH',
        },
      };

    const accessToken = await this.authService.getToken(auth);
    return {
      data: {
        accessToken,
      },
    };
  }
}

/auth 로 body에 id와 role 값을 전달 한다.
이때 유효한 회원인지 검사를 하고 유효하면 jwt token 을 반환한다.

유효한 회원인지 검사와, token 생성 하는 부분은 service 단에서 처리함 !

auth.service.ts


@Injectable()
export class AuthService {
  constructor(
    private jwtService: JwtService,
    @InjectRepository(Entity1)
    private entity1Repository: Repository<Entity1>,
    @InjectRepository(Entity2, BridgeDB)
    private entity2Repository: Repository<Entity2>,
  ){}


  async validateUser(auth: AuthDto) {
    switch(auth.role) {
      case Role.role1 :
        return !! (await this.entity1Repository.findOne({id : auth.id}));
      case Role.role2 :
        return !! (await this.entity2Repository.findOne({id : auth.id}));
    }

    return false;
  }

  async getToken(auth: AuthDto) {
      const payload = {id: auth.id, role: auth.role};
    return this.jwtService.sign(payload);
  }

controller을 통하여 jwt를 위한 token을 발급 받았다.

이 토큰을 api호출 할때 마다 header에 포함시켜서 호출해야 한다.

이때 이 토큰이 유효한지 검사는 passport 모듈에서 처리해 준다. 적용해 보자 !

jwt.strategy.ts

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: true,
      secretOrKey: process.env.JWT_SECRET_KEY,
    });
  }

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

나는 jwt 토큰을 bearer 방법을 통해 검증하기때문에 ExtractJwt.fromAuthHeaderAsBearerToken() 방법을 적용했다.

validate method를 통해 return 값이 req.user 에 들어 간다 !!

jwt.guard.ts

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

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

jwt를 사용하겠다는 guard 이다. 이 guard를 적용하고자 하는 controller에 적용하면 된다. --> @UseGuards(JwtGuard)

여기까지가 jwt 인증 부분 이다.

✨ 이제 payload에 가지고있는 role 값을 통해 특정 role을 가진 user만 api를 호출할수 있도록 RoleGuard를 적용하자 !

role.guard.ts

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

import { ROLES_KEY } from '#common/auth/roles.decorator';

@Injectable()
export class RoleGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<Role[]>(ROLES_KEY, context.getHandler());
    if (!roles) return true;

    const { user } = context.switchToHttp().getRequest(); 

    return this.matchRoles(roles, user.role);
  }

  matchRoles(roles: Role[], userRole: Role) {
    return roles.some((role) => role === userRole);
  }
}

context.switchToHttp().getRequest() 에서 가져온 user값은 위, JwtStrategy validate 에서 return 된 값에 해당한다.

controller에 적용하기


@UseGuards(JwtGuard)
@Controller()
export class AppController {
  ...

  @Get()
  @UseGuards(RoleGuard)
  SetMetadata(ROLES_KEY, [(Role.role1, Role.role2]) 
  async test(@Req() req) {

   // req.user.id  << 이렇게 접근할수 있다 ! 
    ...
  }


  @Get('/good)
  @UseGuards(RoleGuard)
  @Roles(Role.role1, Role.role2) // role decorator
  async test(@Req() req) {

   // req.user.id  << 이렇게 접근할수 있다 ! 
    ...
  }

}

JwtGuard와 마찬가지로 controller에 @UseGuards(JwtGuard) << 이런식으로 적용해 주자

role decorator 적용하기

role decorator를 통해 SetMetadata를 좀더 간단하게 해보자 !

export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
반응형