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
<참조>
- NestJS Authorization: https://docs.nestjs.kr/security/authorization
- JWT Role based authentication: https://github.com/rangle/jwt-role-based-authentication-examples
'React > Architecture' 카테고리의 다른 글
[MS-10] API/Web i18n 적용 및 리팩토링 (0) | 2021.10.02 |
---|---|
[MS-9] Login 화면 개발 (0) | 2021.09.30 |
[MS-7] Refresh Token 설정 (0) | 2021.09.30 |
[MS-6] NestJS의 JWT 기반 Auth Server 환경구축 (0) | 2021.09.27 |
[MS-5] NestJS에 TypeORM 사용하기 (0) | 2021.09.27 |