역할 기반 접근 제어 (RBAC) 구현
인증으로 만든 현재 사용자와 라우트 역할 메타데이터를 분리하고, 전역 가드의 기본 보호 정책과 명시적 공개 경계를 구현합니다.
지난 절에서는 JWT access token을 검증해 현재 요청의 req.user를 만드는 인증(Authentication) 경계를 구현했습니다.
이번 절의 권한 부여(Authorization)는 인증된 사용자가 특정 라우트를 실행할 수 있는지 판단합니다. 인증 실패는 “누구인지 확인할 수 없음”이므로 401 Unauthorized, 역할 부족은 “누구인지는 알지만 이 작업은 허용되지 않음”이므로 403 Forbidden 경계입니다.
RBAC에서 비교하는 두 역할 집합
역할 기반 접근 제어(Role-Based Access Control, RBAC)는 사용자에게 개별 권한을 직접 붙이는 대신 역할을 부여하고, 역할을 권한 집합과 연결하는 모델입니다.
- 사용자(User): 인증된 주체입니다.
- 역할(Role):
admin,editor,user처럼 직무나 책임을 나타내는 이름입니다. - 권한(Permission):
create:post,read:post처럼 리소스에 수행할 수 있는 구체적인 행위입니다.
이 절의 코드는 라우트가 요구하는 역할과 사용자의 역할이 하나라도 정확히 일치하는지 확인하는 기본 RBAC입니다. 역할에서 권한으로 확장하는 매핑이나 역할 계층은 자동으로 생기지 않습니다. 따라서 admin이 user를 암묵적으로 포함하지 않으며, 필요한 경우 중앙 정책에서 명시적으로 설계해야 합니다.
Nest RBAC · 세 정책 상태
공개 예외, 인증 전용, 역할 제한을 명시적으로 구분합니다
JWT guard가 기본 인증을, RolesGuard가 추가 역할 제한을 담당합니다. handler 메타데이터가 class를 override하며 역할은 exact any-of입니다. 공개 예외는 두 guard가 같은 규칙으로 먼저 확인합니다.
명시적 공개
@Public()@Roles()보다 우선합니다.기본 인증
JwtAuthGuard401입니다. 인증 성공 뒤에만 req.user가 역할 가드로 전달됩니다.요구 역할 조회
@Roles()getAllAndOverride()는 handler 값을 우선하고 class 값을 fallback으로 사용합니다. 두 배열을 합치지 않습니다.메타데이터 없음
undefined역할 판정
some()403입니다. 암묵적 역할 계층은 없습니다.변경과 신선도
Role 어휘, 사용자 데이터, JWT이 표의 정책은 “기본 인증, 명시적 공개, 필요한 곳의 역할 제한”입니다. 모든 route가 권한 메타데이터를 반드시 선언해야 하는 조직이라면 별도의 policy-required 검사를 추가합니다.
이 구현의 공개·보호 계약은 다음과 같습니다.
JwtAuthGuard를 전역으로 등록해 모든 라우트를 기본적으로 인증 필요 상태로 둡니다.- 익명 접근은
@Public()을 붙인 라우트에서만 명시적으로 허용하고, 두 전역 가드가 같은 공개 메타데이터를 먼저 확인합니다. @Roles()가 없다는 것은 “공개”가 아니라 “인증만 필요하고 추가 역할 제한은 없음”을 뜻합니다.@Roles(Role.User, Role.Editor)는 둘 중 하나를 가진 사용자를 허용하는 any-of 규칙입니다.
하나의 역할 어휘 정의
문자열을 여러 파일에서 직접 반복하지 않도록 역할을 한곳에서 정의합니다.
export enum Role {
User = 'user',
Editor = 'editor',
Admin = 'admin',
}사용자 저장소, JWT claim 검증, @Roles() 메타데이터와 테스트가 모두 이 어휘를 사용해야 합니다. 데이터베이스에서 문자열을 읽는다면 애플리케이션 경계에서 허용된 Role 값인지 검증합니다.
지난 절의 JwtStrategy.validate(payload: unknown)도 단순히 roles가 문자열 배열인지만 보지 말고, 현재 애플리케이션이 아는 역할인지 확인하도록 좁힙니다.
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Role } from '../role.enum';
type JwtPayload = {
sub: string;
username: string;
roles: Role[];
};
const roleValues = new Set<string>(Object.values(Role));
function isJwtPayload(value: unknown): value is JwtPayload {
if (typeof value !== 'object' || value === null) return false;
const payload = value as Record<string, unknown>;
return (
typeof payload.sub === 'string' &&
Number.isSafeInteger(Number(payload.sub)) &&
String(Number(payload.sub)) === payload.sub &&
typeof payload.username === 'string' &&
Array.isArray(payload.roles) &&
payload.roles.every(
(role): role is Role =>
typeof role === 'string' && roleValues.has(role),
)
);
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey:
configService.getOrThrow<string>('JWT_SECRET'),
algorithms: ['HS256'],
ignoreExpiration: false,
});
}
validate(payload: unknown) {
if (!isJwtPayload(payload)) {
throw new UnauthorizedException('Invalid JWT claims');
}
return {
userId: Number(payload.sub),
username: payload.username,
roles: payload.roles,
};
}
}Passport JWT가 허용 알고리즘·서명·만료를 검증한 뒤에만 validate()가 호출됩니다. 위 검사는 그다음 애플리케이션 경계에서 역할 claim의 런타임 형태와 어휘를 검증합니다. sub도 앞 절처럼 안전한 정수를 나타내는 십진 문자열로 확인한 뒤 Number(payload.sub)를 내부 userId로 반환합니다. 문자열 claim과 숫자형 요청 사용자 ID를 같은 타입으로 혼동하지 않습니다.
JWT의 역할은 발급 시점의 스냅샷입니다. 관리자가 역할을 변경해도 기존 token에는 만료 전까지 이전 값이 남을 수 있습니다. 즉시 반영이 필요하면 짧은 access token 수명만 믿지 말고 요청 중 사용자 저장소 조회, token version 또는 별도 폐기 상태 같은 정책을 선택합니다.
역할과 공개 메타데이터
@Roles()는 필요한 역할 배열을, @Public()은 익명 접근 예외를 메타데이터로 기록합니다.
import { SetMetadata } from '@nestjs/common';
import { Role } from '../../auth/role.enum';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) =>
SetMetadata(ROLES_KEY, roles);import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);SetMetadata()는 정책을 실행하지 않습니다. 현재 handler와 controller class에 데이터를 붙일 뿐이며, 실제 판정은 가드가 담당합니다.
@Public()과 @Roles()는 서로 반대되는 정책이므로 같은 대상에 함께 붙이지 않습니다. 다만 @Roles()가 붙은 controller 안에서 특정 handler만 @Public()으로 여는 구성은 유효합니다. 이때 두 전역 가드 모두 handler의 공개 메타데이터를 먼저 읽어야 class의 역할 제한이 익명 요청을 다시 거부하지 않습니다. 반대로 controller 전체에 @Public()을 붙이면 handler의 @Roles()만으로 다시 보호할 수 없으므로, 공개 범위는 가능한 한 handler 단위로 좁게 선언합니다.
기본 인증 가드와 명시적 공개 경계
Passport의 JWT 가드를 확장해 @Public() 메타데이터가 있는 handler 또는 controller만 인증을 건너뛰게 합니다.
import { ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import {
IS_PUBLIC_KEY,
} from '../../common/decorators/public.decorator';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private readonly reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(
IS_PUBLIC_KEY,
[context.getHandler(), context.getClass()],
);
if (isPublic) return true;
return super.canActivate(context);
}
}getAllAndOverride()는 배열 앞쪽에서 처음 정의된 값을 사용합니다. 따라서 handler 메타데이터를 먼저, controller class를 나중에 전달하면 handler 설정이 class 기본값을 대체합니다.
RolesGuard 구현
RolesGuard는 handler가 요구하는 역할을 먼저 찾고, 없으면 controller class의 기본 역할을 사용합니다.
import {
CanActivate,
ExecutionContext,
Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from '../role.enum';
import {
IS_PUBLIC_KEY,
} from '../../common/decorators/public.decorator';
import {
ROLES_KEY,
} from '../../common/decorators/roles.decorator';
type AuthenticatedRequest = {
user?: {
userId: number;
username: string;
roles: Role[];
};
};
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const targets = [context.getHandler(), context.getClass()];
const isPublic = this.reflector.getAllAndOverride<boolean>(
IS_PUBLIC_KEY,
targets,
);
if (isPublic) return true;
const requiredRoles =
this.reflector.getAllAndOverride<Role[]>(
ROLES_KEY,
targets,
);
if (requiredRoles === undefined) {
return true;
}
const { user } =
context.switchToHttp().getRequest<AuthenticatedRequest>();
return (
user !== undefined &&
requiredRoles.some((role) => user.roles.includes(role))
);
}
}중요한 경계는 다음과 같습니다.
- 유효한
@Public()메타데이터가 있으면 역할 조회보다 먼저 통과합니다. 따라서@Roles()controller 안의 공개 handler도 두 전역 가드를 모두 건너뜁니다. - handler의
@Roles()는 class의@Roles()를 대체합니다. 둘을 합치려면getAllAndMerge()를 선택하고 그 결과가 OR인지 AND인지 별도로 정의해야 합니다. - 메타데이터가 없을 때
RolesGuard가true를 반환해도 앞선 전역JwtAuthGuard는 이미 인증을 요구했습니다. 따라서 이 라우트는 공개가 아니라 인증 전용입니다. - 필요한 역할이 있고 교집합이 없으면
false가 반환되며 Nest는 기본적으로403 Forbidden을 응답합니다. - 인수 없는
@Roles()는 빈 배열을 저장하고 어떤 사용자도 일치하지 않으므로 사용하지 않습니다.
전역 가드 등록
두 가드를 APP_GUARD로 등록하면 모듈 위치와 관계없이 애플리케이션 전체에 적용됩니다. 가드는 바인딩된 순서로 실행되므로 JWT 인증 가드를 역할 가드보다 먼저 둡니다.
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { RolesGuard } from './guards/roles.guard';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';
@Module({
// imports, controllers 생략
providers: [
AuthService,
LocalStrategy,
JwtStrategy,
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AuthModule {}전역 등록을 사용하지 않는다면 보호할 handler에 @UseGuards(JwtAuthGuard, RolesGuard)를 이 순서로 붙여야 합니다. 일부 라우트에서 역할 가드만 실행해 req.user가 아직 없는 상태를 만들지 않습니다.
첫 로그인 요청 열기
전역 JWT 가드를 켜면 아직 token이 없는 로그인 요청도 기본 보호 대상이 됩니다. 앞 절의 src/auth/auth.controller.ts에서 로그인 메서드만 명시적으로 열어 두어야 자격 증명을 검사하고 첫 token을 발급할 수 있습니다. 먼저 그 파일의 import 구간에 이 절에서 정의한 Public을 추가합니다.
import { Public } from '../common/decorators/public.decorator';같은 파일의 AuthController 클래스 안에 있는 기존 login 메서드에 다음처럼 @Public()을 붙입니다. 아래는 메서드 부분만 발췌했으므로 기존 import·constructor와 프로필 메서드는 유지하세요.
@Public()
@UseGuards(LocalAuthGuard)
@Post('login')
@HttpCode(HttpStatus.OK)
async login(@Request() req) {
return this.authService.login(req.user);
}@Public()은 이 절의 두 전역 가드에서 JWT·역할 검사를 건너뛰게 합니다. LocalAuthGuard는 그대로 실행되어 사용자 이름과 비밀번호를 검사하므로, 잘못된 자격 증명은 여전히 401로 거부됩니다. token 없이 보낸 로그인 요청이 JWT 검사에서 먼저 막힌다면 이 메서드의 공개 메타데이터와 import 경로부터 확인하세요. AuthController 클래스 전체나 보호된 프로필 메서드에는 @Public()을 붙이지 않습니다.
Controller 정책 선언
import { Controller, Get, Request } from '@nestjs/common';
import { Role } from './auth/role.enum';
import { Public } from './common/decorators/public.decorator';
import { Roles } from './common/decorators/roles.decorator';
@Controller()
export class AppController {
@Public()
@Get()
getHello() {
return { message: 'public endpoint' };
}
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
@Roles(Role.Admin)
@Get('admin-dashboard')
getAdminDashboard() {
return { message: 'admin only' };
}
@Roles(Role.User, Role.Editor)
@Get('user-content')
getUserContent() {
return { message: 'user or editor' };
}
}profile에는 @Roles()가 없지만 전역 JWT 가드 때문에 인증은 필요합니다. user-content는 user 또는 editor 중 하나를 요구합니다. 이 예제에는 역할 계층이 없으므로 admin만 가진 사용자는 자동으로 통과하지 않습니다.
Nest RBAC 공개·인증·역할 판정 흐름
public 여부·JWT 인증·handler 우선 roles·exact any-of·handler/401/403에서 마지막 도달 경계가 달라진다.
Nest RBAC · 실제 판정 순서
공개 여부 뒤에 인증과 역할을 순서대로 판정합니다
Public 예외가 없으면 JWT 인증 실패는 401, 인증 뒤 역할 불일치는 403입니다. Roles가 undefined이면 역할 제한만 생략하며 인증은 유지됩니다. admin의 암묵적 역할 상속은 없습니다.
-
@Public()예외를 확인합니다.두 전역 guard가 같은 공개 메타데이터를 먼저 확인하므로, class의 역할 제한이 있어도 공개 handler를 실행합니다.
-
나머지 요청은 JWT로 인증합니다.
token 누락·검증 실패는
401입니다. 성공하면req.user.roles가 준비됩니다. -
Reflector가 역할 메타데이터를 읽습니다.handler 값을 class 기본값보다 먼저 사용합니다.
-
@Roles()가 없으면 인증 전용으로 통과합니다.역할 제한만 생략하며 공개로 바뀌지 않습니다.
-
요구 역할이 있으면 exact any-of로 비교합니다.
하나라도 일치하면 handler를 실행하고, 일치하지 않으면
403입니다.
인증은 현재 사용자를 만들고, 권한 부여는 route 정책과 그 사용자를 비교합니다. admin이 다른 역할을 자동 포함하는 계층은 이 흐름에 없습니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- Public 메타데이터가 있으면 JwtAuthGuard와 RolesGuard가 모두 통과해 handler로 가고, 없으면 JwtAuthGuard가 token을 검증해 실패 시 401로 끝낸다. 인증 성공 뒤 Reflector가 handler 우선으로 Roles 메타데이터를 읽고, 역할 제한이 없으면 인증 전용 handler를 실행하며, 있으면 사용자 역할과 하나라도 정확히 일치할 때 실행하고 아니면 403으로 끝낸다.
- Public이면 두 전역 가드가 공개 예외를 먼저 확인하고 handler로 간다. 나머지는 JWT 인증 실패 시 401로 끝나고 성공 시 Reflector가 handler 우선 역할을 읽는다. 역할 제한 없음은 인증만 필요한 handler로, 역할 있음은 정확한 any-of 비교로 간다. 일치는 handler, 불일치는 403으로 끝난다. 오른쪽 외곽의 두 경로는 공개 예외와 역할 제한 없음이고 왼쪽 외곽의 두 경로는 401과 403 거부다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | Nest RBAC 공개·인증·역할 판정 흐름 |
| 32 | Public이면 두 전역 가드가 공개 예외를 먼저 확인하고 handler로 간다. 나머지는 JWT 인증 실패 시 401로 끝나고 성공 시 Reflector가 handler 우선 역할을 읽는다. 역할 제한 없음은 인증만 필요한 handler로, 역할 있음은 정확한 any-of 비교로 간다. 일치는 handler, 불일치는 403으로 끝난다. 오른쪽 외곽의 두 경로는 공개 예외와 역할 제한 없음이고 왼쪽 외곽의 두 경로는 401과 403 거부다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| public·JWT 분기 Public이면 두 guard를 통과하고 아니면 JWT를 검증합니다. | Public: 두 guard 통과 · JWT 실패: 401 | 인증 경계의 도달 여부가 정해집니다. |
| handler 우선 역할 Public이 아니고 JWT 인증이 성공한 요청만 Reflector에서 handler 우선 Roles를 읽습니다. | 진입 조건: Public 아님·JWT 인증 성공 · 제한 없음: 역할 비교 생략·인증 전용 handler · 제한 있음: any-of 역할 비교 진행 | 공개 예외와 JWT 실패는 이 역할 조회를 지나지 않으며 인증에 성공한 요청의 정책만 읽습니다. |
| any-of 결과 Public이 아니고 JWT 인증이 성공했으며 Roles 제한이 있는 요청에서만 exact any-of를 판정합니다. | 진입 조건: Public 아님·JWT 성공·Roles 제한 있음 · exact 하나라도 일치: handler · 불일치: 403·handler 미실행 | 역할 제한 없음의 인증 전용 경로와 구별하며 admin의 암묵적 역할 상속은 적용하지 않습니다. |
Nest RBAC 공개·인증·역할 판정 흐름
public 여부·JWT 인증·handler 우선 roles·exact any-of·handler/401/403에서 마지막 도달 경계가 달라진다.
Nest RBAC · 실제 판정 순서
공개 여부 뒤에 인증과 역할을 순서대로 판정합니다
Public 예외가 없으면 JWT 인증 실패는 401, 인증 뒤 역할 불일치는 403입니다. Roles가 undefined이면 역할 제한만 생략하며 인증은 유지됩니다. admin의 암묵적 역할 상속은 없습니다.
-
@Public()예외를 확인합니다.두 전역 guard가 같은 공개 메타데이터를 먼저 확인하므로, class의 역할 제한이 있어도 공개 handler를 실행합니다.
-
나머지 요청은 JWT로 인증합니다.
token 누락·검증 실패는
401입니다. 성공하면req.user.roles가 준비됩니다. -
Reflector가 역할 메타데이터를 읽습니다.handler 값을 class 기본값보다 먼저 사용합니다.
-
@Roles()가 없으면 인증 전용으로 통과합니다.역할 제한만 생략하며 공개로 바뀌지 않습니다.
-
요구 역할이 있으면 exact any-of로 비교합니다.
하나라도 일치하면 handler를 실행하고, 일치하지 않으면
403입니다.
인증은 현재 사용자를 만들고, 권한 부여는 route 정책과 그 사용자를 비교합니다. admin이 다른 역할을 자동 포함하는 계층은 이 흐름에 없습니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- Public 메타데이터가 있으면 JwtAuthGuard와 RolesGuard가 모두 통과해 handler로 가고, 없으면 JwtAuthGuard가 token을 검증해 실패 시 401로 끝낸다. 인증 성공 뒤 Reflector가 handler 우선으로 Roles 메타데이터를 읽고, 역할 제한이 없으면 인증 전용 handler를 실행하며, 있으면 사용자 역할과 하나라도 정확히 일치할 때 실행하고 아니면 403으로 끝낸다.
- Public이면 두 전역 가드가 공개 예외를 먼저 확인하고 handler로 간다. 나머지는 JWT 인증 실패 시 401로 끝나고 성공 시 Reflector가 handler 우선 역할을 읽는다. 역할 제한 없음은 인증만 필요한 handler로, 역할 있음은 정확한 any-of 비교로 간다. 일치는 handler, 불일치는 403으로 끝난다. 오른쪽 외곽의 두 경로는 공개 예외와 역할 제한 없음이고 왼쪽 외곽의 두 경로는 401과 403 거부다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | Nest RBAC 공개·인증·역할 판정 흐름 |
| 32 | Public이면 두 전역 가드가 공개 예외를 먼저 확인하고 handler로 간다. 나머지는 JWT 인증 실패 시 401로 끝나고 성공 시 Reflector가 handler 우선 역할을 읽는다. 역할 제한 없음은 인증만 필요한 handler로, 역할 있음은 정확한 any-of 비교로 간다. 일치는 handler, 불일치는 403으로 끝난다. 오른쪽 외곽의 두 경로는 공개 예외와 역할 제한 없음이고 왼쪽 외곽의 두 경로는 401과 403 거부다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| public·JWT 분기 Public이면 두 guard를 통과하고 아니면 JWT를 검증합니다. | Public: 두 guard 통과 · JWT 실패: 401 | 인증 경계의 도달 여부가 정해집니다. |
| handler 우선 역할 Public이 아니고 JWT 인증이 성공한 요청만 Reflector에서 handler 우선 Roles를 읽습니다. | 진입 조건: Public 아님·JWT 인증 성공 · 제한 없음: 역할 비교 생략·인증 전용 handler · 제한 있음: any-of 역할 비교 진행 | 공개 예외와 JWT 실패는 이 역할 조회를 지나지 않으며 인증에 성공한 요청의 정책만 읽습니다. |
| any-of 결과 Public이 아니고 JWT 인증이 성공했으며 Roles 제한이 있는 요청에서만 exact any-of를 판정합니다. | 진입 조건: Public 아님·JWT 성공·Roles 제한 있음 · exact 하나라도 일치: handler · 불일치: 403·handler 미실행 | 역할 제한 없음의 인증 전용 경로와 구별하며 admin의 암묵적 역할 상속은 적용하지 않습니다. |
경계 테스트
같은 경로의 성공 사례만 확인하지 말고 공개·인증·인가 실패를 나눠 검증합니다. 아래 표와 그림은 접두사 없이 표기합니다. api를 유지했다면 /는 /api, /profile은 /api/profile, /admin-dashboard는 /api/admin-dashboard, /user-content는 /api/user-content로 읽습니다. 이 AppController의 /profile과 앞 AuthController의 /auth/profile은 서로 다른 경로입니다.
| 요청 | 사용자 상태 | 예상 결과 |
|---|---|---|
GET / | token 없음 | 200 OK · @Public() |
class에 @Roles(Role.Admin), handler에 @Public() | token 없음 | 200 OK · 공개 정책이 상속된 역할 제한보다 우선 |
GET /profile | token 없음 또는 유효하지 않음 | 401 Unauthorized |
GET /profile | 유효한 user token | 200 OK · 인증 전용 |
GET /admin-dashboard | user 역할 | 403 Forbidden |
GET /admin-dashboard | admin 역할 | 200 OK |
GET /user-content | admin 역할만 보유 | 403 Forbidden · 계층 없음 |
역할 이름을 변경할 때는 enum만 바꾸고 끝내지 않습니다. 사용자 데이터와 seed, 관리 UI, 발급 로직, 라우트 메타데이터, 기존 JWT에 담긴 역할을 최신 상태와 맞추는 정책, 허용·401·403 테스트를 같은 변경 단위로 검토합니다.
상속된 정책을 직접 계산하기
controller class에는 @Roles(Role.Admin)이 있고, 특정 handler에는 @Roles(Role.User, Role.Editor)만 있습니다. 전역 가드는 본문의 순서로 등록되어 있고 유효한 editor 토큰을 보냅니다. 이 handler를 실행할 수 있을까요? 같은 handler의 역할 선언을 지우면 결과가 어떻게 바뀔까요? 다른 handler에 @Public()만 붙이고 토큰 없이 요청한 경우도 따로 판단하세요.
override·인증·공개 순서로 풀이하기
첫 handler의 요구 배열은 class 배열을 대체하므로 editor가 any-of 조건에 일치해 실행됩니다. handler 선언을 지우면 class의 admin이 기본값이 되어 editor만 가진 인증 사용자는 403으로 거부됩니다. 다른 공개 handler는 두 전역 가드가 공개 메타데이터를 먼저 확인하므로 토큰 없이도 통과합니다. 공개 선언이 없는 경로에서 역할 메타데이터를 지우는 것만으로 JWT 인증이 생략되지는 않습니다.
세밀한 리소스 행위나 소유권 조건이 필요해지면 역할 문자열을 계속 늘리기보다 permission claim 또는 정책 기반 권한 부여로 확장합니다.
공식 문서
- NestJS Authorization: 기본 RBAC guard, 역할 metadata와
403 Forbidden - NestJS Execution context:
Reflector,getAllAndOverride(),getAllAndMerge() - NestJS Request lifecycle: global·controller·route guard 실행 순서
- NestJS Authentication: global authentication guard와 명시적
@Public()경계
RBAC 구현은 인증이 만든 신뢰 가능한 현재 사용자, 라우트 메타데이터, 비교 의미와 공개 정책이 같은 계약을 바라볼 때 안정적으로 동작합니다.
다음 절에서는 외부 제공자 계정으로 로그인하는 OAuth 흐름을 연결합니다.