이 문서는 Express.js의 미들웨어 기반 인증을 NestJS의 가드(Guard)와 데코레이터(Decorator)를 사용하여 구현하는 방법에 대한 대화 내용을 정리한 것입니다.
사용자 질문: 이 부분의 auth(ROLE.ADMIN)부분을 nestjs에서 구현한다면 어떻게 구혀할 수 있을까?
안녕하세요! legacy Express.js 프로젝트의 auth(ROLE.ADMIN)과 같은 역할을 하는 인증 및 인가 미들웨어를 NestJS에서 어떻게 구현할 수 있는지 설명해 드리겠습니다.
NestJS에서는 주로 가드(Guards) 와 데코레이터(Decorators) 를 사용하여 이 기능을 구현합니다.
요청하신 기능의 흐름은 다음과 같습니다.
- 사용자가 특정 권한(예:
ADMIN)을 요구하는 API를 요청합니다. - API 핸들러에 적용된
AuthGuard가 먼저 실행되어, 사용자가 유효한 JWT 토큰을 가지고 있는지(인증되었는지) 확인합니다. - 인증이 통과되면,
RolesGuard가 실행되어 사용자가 해당 API를 호출하는 데 필요한 권한(e.g.,ADMIN)을 가지고 있는지 확인합니다. - 모든 검사를 통과하면, 컨트롤러의 로직이 실행됩니다.
아래에 단계별 구현 방법을 보여드리겠습니다.
먼저, 각 라우트 핸들러에 필요한 역할을 지정할 수 있는 커스텀 데코레이터를 만듭니다.
src/modules/auth/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Role } from '../enums/role.enum'; // Role Enum의 경로에 맞게 수정해주세요.
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);(만약 Role enum이 없다면, export enum Role { ADMIN = 'ADMIN', USER = 'USER' } 와 같이 생성해주시면 됩니다.)
다음으로, 위에서 만든 @Roles 데코레이터로 지정된 역할과 현재 접속한 유저의 역할을 비교하는 가드를 만듭니다.
src/modules/auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
import { Role } from '../enums/role.enum';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true; // 역할이 지정되지 않은 API는 누구나 접근 가능
}
const { user } = context.switchToHttp().getRequest();
// user 객체가 없거나 user.role이 없으면 false를 반환합니다.
// 이는 AuthGuard가 RolesGuard보다 먼저 실행되어야 함을 의미합니다.
return requiredRoles.some((role) => user?.role === role);
}
}NestJS는 Passport.js 라이브러리와의 통합을 통해 인증을 쉽게 처리할 수 있습니다. 이미 JwtStrategy가 설정되어 있다고 가정하겠습니다. JwtAuthGuard는 passport-jwt 전략을 사용하여 요청 헤더의 JWT를 검증합니다.
src/modules/auth/guards/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}이제 위에서 만든 요소들을 컨트롤러에 적용할 차례입니다. admin 모듈의 컨트롤러를 예시로 보여드리겠습니다.
src/modules/admin/admin.controller.ts
import { Controller, Put, Param, Body, UseGuards } from '@nestjs/common';
import { AdminService } from './admin.service';
import { UpdateAdminDto } from './dto/update-admin.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; // 경로 수정 필요
import { RolesGuard } from '../auth/guards/roles.guard'; // 경로 수정 필요
import { Roles } from '../auth/decorators/roles.decorator'; // 경로 수정 필요
import { Role } from '../auth/enums/role.enum'; // 경로 수정 필요
@Controller('admin')
export class AdminController {
constructor(private readonly adminService: AdminService) {}
// ... 다른 메소드들
@Put(':id')
@UseGuards(JwtAuthGuard, RolesGuard) // 1. 인증 가드, 2. 권한 가드 순으로 적용
@Roles(Role.ADMIN) // ADMIN 역할이 필요함을 명시
update(@Param('id') id: string, @Body() updateAdminDto: UpdateAdminDto) {
return this.adminService.update(+id, updateAdminDto);
}
// ...
}사용자 질문: jwt의 payload를 decode 해서 role_id를 얻고, 그 값을 바탕으로 role 권한 확인을 한다면
네, 알겠습니다. JWT payload에 포함된 role_id를 사용하여 역할 기반 권한을 확인하는 NestJS 코드를 더 구체적으로 설명해 드리겠습니다.
src/modules/auth/enums/role.enum.ts
export enum Role {
STUDENT = 'STUDENT',
ADMIN = 'ADMIN',
}src/modules/auth/strategies/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
// JWT payload의 타입을 정의합니다.
interface JwtPayload {
sub: number; // 사용자 ID
role_id: number;
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'), // .env 파일 등에서 JWT 시크릿 키를 가져옵니다.
});
}
async validate(payload: JwtPayload) {
if (!payload.sub || !payload.role_id) {
throw new UnauthorizedException('유효하지 않은 토큰입니다.');
}
// role_id를 포함한 사용자 정보를 반환합니다.
return { userId: payload.sub, roleId: payload.role_id };
}
}src/modules/auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
import { Role } from '../enums/role.enum';
const ROLE_ID_TO_NAME_MAP = {
1: Role.ADMIN,
2: Role.STUDENT,
};
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
if (!user || !user.roleId) {
return false;
}
const userRoleName = ROLE_ID_TO_NAME_MAP[user.roleId];
return requiredRoles.some((role) => role === userRoleName);
}
}src/modules/admin/admin.controller.ts
import { Controller, Put, Param, Body, UseGuards, Get } from '@nestjs/common';
import { AdminService } from './admin.service';
import { UpdateAdminDto } from './dto/update-admin.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { Role } from '../auth/enums/role.enum';
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
constructor(private readonly adminService: AdminService) {}
@Get()
@Roles(Role.ADMIN)
findAll() {
return this.adminService.findAll();
}
@Put(':id')
@Roles(Role.ADMIN)
update(@Param('id') id: string, @Body() updateAdminDto: UpdateAdminDto) {
return this.adminService.update(+id, updateAdminDto);
}
}사용자 질문: @UseGuards(JwtAuthGuard, RolesGuard) 이 과정에서 jwtAuthGuard의 리턴객체가 RolesGuard에게 값을 전달하는 방법에 대해 설명해줘
JwtAuthGuard는 RolesGuard에게 직접 값을 return하여 전달하지 않습니다. 대신 실행 컨텍스트(Execution Context)의 request 객체를 수정함으로써 정보를 공유합니다.
JwtAuthGuard가 실행됩니다.- 내부적으로
JwtStrategy를 호출합니다. JwtStrategy의validate메소드가return { userId: payload.sub, roleId: payload.role_id };와 같이 객체를 반환합니다.- Passport.js는 이 반환된 객체를
request.user에 자동으로 할당합니다. JwtAuthGuard는 다음 가드(RolesGuard)로 제어권을 넘깁니다.
RolesGuard의canActivate메소드가 동일한 실행 컨텍스트를 인자로 받아 실행됩니다.RolesGuard는context.switchToHttp().getRequest()를 통해request객체에 접근합니다.const { user } = request;와 같이 코드를 작성하여JwtAuthGuard가 저장해 둔 사용자 정보({ userId: 1, roleId: 1 })를 가져와 사용할 수 있습니다.
사용자 질문: user라는 키에 user 객체를 저장하는건 passport에서 지정한 것인지, 어떤 key에 jwtauthguard에서 리턴한 객체를 저장할지 지정할 수 있는지?
네, 맞습니다. request.user는 Passport.js의 오랜 관례(convention)이자 기본 동작 방식입니다.
네, 가능합니다. AuthGuard를 상속받는 커스텀 가드를 만들고 handleRequest 메소드를 오버라이딩하면 됩니다.
src/modules/auth/guards/custom-jwt-auth.guard.ts
import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class CustomJwtAuthGuard extends AuthGuard('jwt') {
handleRequest(err, user, info, context: ExecutionContext) {
if (err || !user) {
throw err || new UnauthorizedException('인증 정보가 유효하지 않습니다.');
}
const request = context.switchToHttp().getRequest();
// 'user' 대신 'authInfo'라는 키에 사용자 정보를 저장합니다.
request.authInfo = user;
// 반드시 user 객체를 반환해야 합니다.
return user;
}
}이후 RolesGuard에서는 request.authInfo를 사용하고, 컨트롤러에서는 @UseGuards(CustomJwtAuthGuard, RolesGuard)를 사용해야 합니다.
권장사항: 특별한 이유가 없다면 호환성과 예측 가능성을 위해 Passport.js의 기본 관례인 user 키를 그대로 사용하는 것을 권장합니다.
사용자 질문: 인증과 관련된 에러를 발생시키고 싶다묜?
JWT는 유효하나 payload의 사용자가 DB에 없거나 비활성화된 경우, JwtStrategy의 validate 메소드에서 UnauthorizedException을 발생시킵니다.
src/modules/auth/strategies/jwt.strategy.ts
// ...
import { AdminService } from 'src/modules/admin/admin.service'; // 사용자 조회를 위한 서비스 주입
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private readonly configService: ConfigService,
private readonly adminService: AdminService,
) { /* ... */ }
async validate(payload: { sub: number, roleId: number }) {
const user = await this.adminService.findOne(payload.sub);
if (!user) {
throw new UnauthorizedException('사용자를 찾을 수 없습니다.');
}
return { userId: user.id, roleId: user.roleId };
}
}인증은 통과했으나 역할(Role)이 맞지 않는 경우, RolesGuard에서 ForbiddenException을 발생시킵니다.
src/modules/auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
// ...
@Injectable()
export class RolesGuard implements CanActivate {
// ... constructor ...
canActivate(context: ExecutionContext): boolean {
const requiredRoles = /* ... */;
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
const userRoleName = ROLE_ID_TO_NAME_MAP[user?.roleId];
const hasRole = () => requiredRoles.some((role) => role === userRoleName);
if (user && hasRole()) {
return true;
}
throw new ForbiddenException('이 작업을 수행할 권한이 없습니다.');
}
}토큰이 없거나, 만료되었거나, 서명이 잘못된 경우 등은 AuthGuard('jwt')가 자동으로 401 Unauthorized 에러를 발생시킵니다.