블로그 이미지
윤영식
Full Stacker, Application Architecter, KnowHow Dispenser and Bike Rider

Publication

12-09 15:07

Category

2021. 9. 30. 13:22 React/Architecture

 

NestJS에서 제공하는 Auth와 Role 기능을 확장해 본다. NestJS는 그외 Configuration, Logging, Filter, Interceptor등 다양한 기능을 확장하여 적용할 수 있도록 한다. 

 

 

Role 데코레이터 추가

Role 체크를 위한 데코레이터를 libs/shared/src/lib/decorator/roles.decorator.ts 를 추가한다.

// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

 

 

Role 가드 추가

request시에 user 정보의 role을 통해 match되는지를 체크하는 가드(guard)를 libs/shared/src/lib/guard/role.guard.ts 추가한다. 

  • 요구하는 roles가 없으면 bypass 한다.
  • user가 없다면 즉, 로그인한 사용자가 아니거나, Login Token이 없다면 Forbidden 에러를 발생시킨다.
// role.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

const matchRoles = (roles: string[], userRoles: string) => {
  return roles.some(role => role === userRoles);
};

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

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) {
      return true;
    }

    const req = context.switchToHttp().getRequest() as any;
    const user = req.user;
    if (!user) {
      throw new ForbiddenException('User does not exist');
    }
    return matchRoles(requiredRoles, user.role);
  }
}

 

로그인후 express의 request에 user 객체 할당

로그인을 하면 사용자 정보가 Token에 담긴다. @Role 데코레이터를 체크하기 전에 Token 정보를 기반으로 user 정보를 추출한다. 

  • 로그인 토큰: LOGIN_TOKEN

libs/domain/src/lib/auth/auth.middleware.ts 파일을 생성하고, 쿠키의 LOGIN_TOKEN에서 user정보를 얻는다.

import { ForbiddenException, Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { verify } from 'jsonwebtoken';

import { loadConfigJson } from '@rnm/shared';
const config: any = loadConfigJson();

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    if (req.user) {
      next();
      return;
    }
    
    const accessToken = req?.cookies?.LOGIN_TOKEN;
    let user;
    try {
      user = verify(
        accessToken,
        config?.AUTH?.SECRET,
      );
    } catch (error) {
      throw new ForbiddenException('Please register or sign in.');
    }

    if (user) {
      req.user = user;
    }
    next();
  }
}

request에 user를 할당하는 미들웨어와 Role Guard를 apps/gateway/api/src/app/app.module.ts 에 설정한다. 

  • RolesGuard 등록
  • AuthMiddleware path들 등록
// app.module.ts
import { join } from 'path';
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { APP_FILTER, APP_GUARD } from '@nestjs/core';
import { ServeStaticModule } from '@nestjs/serve-static';
import { TypeOrmModule } from '@nestjs/typeorm';
import { getMetadataArgsStorage } from 'typeorm';

import { GatewayApiAppService, EntitiesModule, AuthModule, AuthMiddleware } from '@rnm/domain';
import { GlobalExceptionFilter, ormConfigService, RolesGuard } from '@rnm/shared';

import { DashboardModule } from './dashboard/microservice/dashboard.module';
import { ConfigurationModule } from './configuration/microservice/configuration.module';
import { BackOfficeModule } from './back-office/microservice/back-office.module';
import { AppController } from './app.controller';
import { AuthController } from './auth/auth.controller';
import { UserController } from './user/user.controller';

@Module({
  imports: [
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, 'public'),
      exclude: [
        '/api/auth*',
        '/api/gateway*', '/api/dashboard*', '/api/configuration*', '/api/back-office*',
        '/dashboard*', '/configuration*', '/back-office*'
      ],
    }),
    // ORM
    TypeOrmModule.forRoot({
      ...ormConfigService.getTypeOrmConfig(),
      entities: getMetadataArgsStorage().tables.map(tbl => tbl.target)
    }),
    EntitiesModule,
    // MicroService
    DashboardModule,
    ConfigurationModule,
    BackOfficeModule,
    // Auth
    AuthModule
  ],
  controllers: [
    AuthController,
    AppController,
    UserController
  ],
  providers: [
    GatewayApiAppService,
    // Global Exception Filter
    {
      provide: APP_FILTER,
      useClass: GlobalExceptionFilter,
    },
    // 1) Role Guard 등록
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ]
})
export class AppModule implements NestModule {
  // 2) Auth Middleware 등록
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(AuthMiddleware)
      .forRoutes(...[
        { path: '/dashboard*', method: RequestMethod.ALL },
        { path: '/configuration*', method: RequestMethod.ALL },
        { path: '/back-office*', method: RequestMethod.ALL },
        { path: '/api*', method: RequestMethod.ALL },
      ]);
  }
}

 

 

Role 사용하기

user 테이블에 Role이 저장되어있다. 

user.model.ts 소스에 UserRole enum을 추가한다. 

// user.model.ts
export interface User {
  id?: number;
  username: string;
  password?: string;
  email?: string;
  firstName?: string;
  lastName?: string;
  role?: string;
  sub?: string | number;
  currentHashedRefreshToken?: string;
}
export type LoginDto = Pick<User, 'username' | 'password'>;
export type TokenPayload = Omit<User, 'password'>;

// User Role
export enum UserRole {
  ADMIN = 'ADMIN',
  MANAGER = 'MANAGER',
  CUSTOMER = 'CUSTOMER',
  GUEST = 'GUEST',
}

apps/gateway/api/src/app/user/user.controller.ts 안에 @Roles을 적용한다. 

// user.controller.ts
import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';

import { JwtAuthGuard, UserService } from '@rnm/domain';
import { User, UserRole } from '@rnm/model';
import { Roles } from '@rnm/shared';

@Controller('api/gateway/user')
export class UserController {
  constructor(
    private readonly service: UserService
  ) { }

  @UseGuards(JwtAuthGuard)
  @Post()
  @Roles(UserRole.ADMIN, UserRole.MANAGER) // <== 요기
  async create(@Body() data: User): Promise<User> {
    const savedUser = await this.service.create(data);
    if (!savedUser) {
      return;
    }
    return savedUser;
  }
  ....
 }

 

이후 열심히 사용해 보자.

소스: https://github.com/ysyun/rnm-stack/releases/tag/ms-8

 

Release ms-8 · ysyun/rnm-stack

[ms-8] added role guard for authorization

github.com

 

 

<참조>

- NestJS Authorization: https://docs.nestjs.kr/security/authorization

 

네스트JS 한국어 매뉴얼 사이트

네스트JS 한국, 네스트JS Korea 한국어 매뉴얼

docs.nestjs.kr

- JWT Role based authentication: https://github.com/rangle/jwt-role-based-authentication-examples

 

GitHub - rangle/jwt-role-based-authentication-examples: Implement the same backend using graphql, nestjs and deno.

Implement the same backend using graphql, nestjs and deno. - GitHub - rangle/jwt-role-based-authentication-examples: Implement the same backend using graphql, nestjs and deno.

github.com

 

posted by peter yun 윤영식