
인증(Authentication)을 통해 사용자가 누구인지 확인했다면, 이제 인가(Authorization)를 통해 사용자가 무엇을 할 수 있는지 확인해야 합니다. NestJS에서 RBAC는 Custom Decorator와 Guard를 결합하여 구현하는 것이 표준입니다.
1. 💡 RBAC 개요 및 목표
RBAC는 사용자에게 역할(Role)을 할당하고, 그 역할에 따라 리소스 접근 권한을 부여하는 인가 방식입니다.
- 사용자: Jane
- 역할 (Role): Admin
- 권한 (Permission): 사용자 목록 조회, 사용자 생성, 사용자 삭제
우리의 목표는 특정 API 엔드포인트에 필요한 역할을 명시하고, 요청을 보낸 사용자의 역할이 해당 권한을 충족하는지 Guard에서 검사하는 것입니다.
2. 📝 라우트에 역할 메타데이터 추가
Guard에서 필요한 정보를 읽을 수 있도록, 해당 라우트에 필요한 Role 정보를 메타데이터(Metadata)로 추가해야 합니다. NestJS의 @nestjs/common에서 제공하는 @SetMetadata() 함수를 사용합니다.
2.1. Custom Decorator 생성 (@Roles)
@SetMetadata()를 직접 사용하는 것은 번거롭기 때문에, 이를 래핑하는 커스텀 데코레이터를 만듭니다.
// src/common/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
// 💡 역할 상수 정의 (선택적)
export enum UserRole {
Admin = 'admin',
User = 'user',
Guest = 'guest',
}
// 💡 'roles'라는 키로, 필요한 역할 문자열 배열을 메타데이터로 설정
export const ROLES_KEY = 'roles';
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles);
2.2. 컨트롤러에 적용
이제 이 @Roles() 데코레이터를 사용하여 특정 엔드포인트에 접근할 수 있는 최소 권한을 명시합니다.
// src/users/users.controller.ts
@Controller('users')
export class UsersController {
// 💡 'admin' 역할을 가진 사용자만 접근 가능
@Roles(UserRole.Admin)
@Post()
createUser(@Body() userData: CreateUserDto) {
return this.usersService.create(userData);
}
// 💡 'admin'과 'user' 역할 모두 접근 가능
@Roles(UserRole.Admin, UserRole.User)
@Get(':id')
findUser(@Param('id') id: string) {
return this.usersService.findOne(id);
}
}
3. 🛡️ RolesGuard 구현 (권한 확인)
이제 모든 요청에서 roles 메타데이터를 읽고, JWT Strategy를 통해 요청 객체(req.user)에 담긴 사용자의 실제 역할과 비교하는 Guard를 구현합니다.
3.1. Reflector 사용
Guard에서 메타데이터를 읽으려면 NestJS의 Reflector 헬퍼 클래스를 사용해야 합니다.
// src/auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY, UserRole } from '../../common/decorators/roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
// 💡 Reflector를 주입받아 메타데이터를 읽음
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// 1. 💡 라우트 핸들러에 필요한 역할(Roles) 메타데이터를 읽어옴
const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>(ROLES_KEY, [
context.getHandler(), // 메서드 레벨
context.getClass(), // 클래스 레벨
]);
// 💡 라우트에 @Roles() 데코레이터가 없는 경우, 모두 접근 가능 (true 반환)
if (!requiredRoles) {
return true;
}
// 2. 💡 요청 객체에서 현재 사용자의 실제 역할 정보를 가져옴
// (이 정보는 앞서 실행된 JwtStrategy에서 req.user에 주입되었음)
const { user } = context.switchToHttp().getRequest();
// 💡 user.roles는 DB에서 가져온 사용자의 역할 배열이라고 가정
const userRoles: UserRole[] = user.roles;
// 3. 💡 권한 비교 로직 (하나라도 일치하면 접근 허용)
const hasRole = requiredRoles.some((role) => userRoles.includes(role));
if (!hasRole) {
throw new ForbiddenException('해당 리소스에 접근할 권한이 없습니다.');
}
return true; // 접근 허용
}
}
4. 🥇 Guard 적용
JWT 인증 Guard와 Role 권한 Guard를 함께 적용하여 요청을 보호합니다.
// src/users/users.controller.ts (최종 적용)
import { JwtAuthGuard } from '...'; // 💡 JWT 인증 Guard (AuthGuard('jwt')를 래핑한 것)
import { RolesGuard } from '...'; // 💡 역할 권한 Guard
@Controller('users')
// 💡 컨트롤러 레벨에 두 Guard를 모두 적용
@UseGuards(JwtAuthGuard, RolesGuard)
export class UsersController {
// 💡 @Roles 데코레이터만으로 권한 설정 완료
@Roles(UserRole.Admin)
@Post()
createUser(@Body() userData: CreateUserDto) {
// ...
}
}
실행 순서:
- JwtAuthGuard: 토큰이 유효한지 확인하고 req.user에 사용자 정보(역할 포함)를 주입합니다.
- RolesGuard: req.user.roles를 읽어 라우트에 필요한 역할과 비교하여 권한을 확인합니다.
이로써 NestJS의 Guard 시스템을 완벽하게 활용하여 유연하고 강력한 Role 기반 권한 관리 시스템을 구축했습니다.
'Nest.js를 배워보자 > 7. NestJS 인증 시스템 구축 — JWT & Passport 실전' 카테고리의 다른 글
| WT 인증 시스템 구축: Refresh Token 패턴 (0) | 2025.12.02 |
|---|---|
| Passport Strategy 구현: Local & JWT 실전 (0) | 2025.12.02 |
| NestJS 인증 시스템 구축: JWT & Passport 실전 (0) | 2025.12.02 |