[NestJS] Custom Decorator + Guard로 세분화된 권한 체크하기

som·2024년 7월 23일

NestJS

목록 보기
2/2

배경

현 서비스에는 권한이 계층형입니다.
① 전역: 비회원 ↔ 회원(JWT)
② 커뮤니티: 여러 커뮤니티 중 “요청된 커뮤니티에 가입된 멤버인지”
③ 역할: 그 커뮤니티에서의 일반 유저 / 아티스트 / 매니저
컨트롤러에서 매번 if–else로 검사하면 중복·누락이 생기고, 정책이 코드 곳곳으로 흩어집니다.
그래서 권한은 선언, 검증은 가드에서 처리하도록 변경했습니다.

현재 문제 → 개선 방안

문제

  • Service 내부에서 ② 커뮤니티 권한 검사 → 코드 중복
  • 권한 이슈를 Service에서 예외로 처리하기보다 API 입구에서 차단하는 게 적절

개선 방안

  • 컨트롤러에 @UseGuards(JwtAuthGuard, CommunityUserGuard) 표준화
  • 가드에서 ② 커뮤니티 회원 + ③ 역할(ARTIST/MANAGER) 검사
  • Custom Decorator @CommunityUserRoles(...) 로 허용 role 선언

전체 흐름 요약

Controller (선언)
  └─ @CommunityUserRoles(…optional)  +  @UserInfo()  // 권한 역할 선언 + 유저 주입
      ↓
JwtAuthGuard (① 전역: 로그인/회원)
      ↓
CommunityUserGuard
  ├─ ② 커뮤니티 멤버인지 검사
  └─ ③ (필요시) 커뮤니티 역할(ARTIST/MANAGER) 검사

구현

아래 코드는 흐름을 설명하기 위해 실제 구현한 코드를 간소화한 버전입니다.

1) 역할(Role) Enum

// community-user-role.type.ts
export enum CommunityUserRole {
  ARTIST  = 'ARTIST',
  MANAGER = 'MANAGER',
  // 필요시 다른 역할 확장
}

2) Custom Decorator — 허용할 역할 선언

// community-user-roles.decorator.ts

import { SetMetadata } from '@nestjs/common';
import { CommunityUserRole } from './community-user-role.type';

export const COMMUNITY_ROLES_KEY = 'communityRoles';
export const CommunityUserRoles = (...roles: CommunityUserRole[]) =>
  SetMetadata(COMMUNITY_ROLES_KEY, roles);

위 Enum만 CommunityUser의 허용 role로 선언합니다.
@CommunityUserRoles() 내부에 들어간 값을 읽어 가드에서 실제 검증에 사용합니다.

3) Custom Decorator — UserInfo()로 유저 정보 받기

// user-info.decorator.ts
import { ExecutionContext, createParamDecorator } from '@nestjs/common';

export const UserInfo = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);

매번 req.user 를 사용해 직접 꺼내오지 않고 @UserInfo 데코레이터를 만들어 유저 정보를 가져올 수 있도록 했습니다.

  • 읽기 쉽고, 구조 변경 시 데코레이터 내부만 수정하면 된다.
  • user만 필요해도 매번 req 전체를 받아야 했음 -> 필요한 정보만 가져오도록 변경

Before/After

// Before
@Post()
create(@Req() req: Request, @Body() dto: CreateDto) {
  const user = req.user; // 매번 이렇게 꺼냄
  ...
}

// After
@Post()
create(@UserInfo() user: PartialUser, @Body() dto: CreateDto) {
  ...
}

4) Guard - 역할 검사

// community-user.guard.ts
@Injectable()
export class CommunityUserGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector,
  	// ...
  ) {}

  async canActivate(ctx: ExecutionContext): Promise<boolean> {
    const req = ctx.switchToHttp().getRequest();

    // 1) ① 로그인 여부 확인
    const userId = req.user?.id;
    if (!userId) throw new UnauthorizedException();
    
    // ...

    // 2) ② 해당 커뮤니티 유저인지 검사
     const existedCommunityUser =
      await this.communityUserService.findByCommunityIdAndUserId(
        communityId,
        userId,
      );
    
    // ...

    // 3) @CommunityUserRoles 데코레이터 안 roles를 가져오는 역할
	// ex) @CommunityUserRoles(ARTIST,MANAGER) → roles [ARTIST,MANAGER]
    const roles: CommunityUserRole[] = this.reflector.get(
      context.getHandler(),
    );
   
    // 4) roles 데코레이터 없으면 : 멤버면 통과
    if (roles.length === 0) return true;

    // 5) ③ 역할 검사(지정된 roles 중 맞으면 통과)
    const communityUserid = existedCommunityUser.communityUserId;
	
    // ③ 커뮤니티 유저이면서 ARTIST roles를 가진 경우
    if (roles.includes(CommunityUserRole.ARTIST)) {
      // ...
    }
    
    // ③ 커뮤니티 유저이면서 MANAGER roles를 가진 경우
    if (roles.includes(CommunityUserRole.MANAGER)) {
      // ...
    }
    
    // ...

    throw new ForbiddenException('존재하지 않는 역할입니다.');
  }
}

컨트롤러 적용 예시

// 1) 해당 커뮤니티 “멤버면” 접근
@UseGuards(JwtAuthGuard, CommunityUserGuard)
@Post(':communityId/assets')
createAsset(
  @UserInfo() user: PartialUser, 
  @Body() dto: CreateAssetDto,
) { /* ... */ }

// 2) “아티스트 또는 매니저”만 접근
@CommunityUserRoles(CommunityUserRole.ARTIST, CommunityUserRole.MANAGER) 
@UseGuards(JwtAuthGuard, CommunityUserGuard)
@Post(':communityId/publish')
publish(
 @UserInfo() user: PartialUser, 
  @Body() dto: PublishDto,
) { /* ... */ }

// 3) “매니저만” 접근
@CommunityUserRoles(CommunityUserRole.MANAGER)
@UseGuards(JwtAuthGuard, CommunityUserGuard)
@Delete(':communityId/posts/:postId')
removePost(
  @Param('postId') postId: string,
  @UserInfo() user: PartialUser,
) { /* ... */ }

마무리

기존 서비스 로직에 있던 코드들을 Guard를 사용하는 식으로 변경하고 나니 개발한 사람 외에는 팀원들이 사용할 수도 없을거란 생각을 했고, 쉽게 이해/사용할 수 있도록 가이드문서가 필요하다는 생각을 했습니다. 위 내용과 많이 중복되기는 하지만, 혹시 가이드 문서가 필요하다고 생각하신 분들이 참고할 수 있도록 짧게 사진 넣습니다 🥹

Reference


코드의 대부분은 NestJS 공식 문서에 관련 내용이 적혀있습니다!
Guards | NestJS - A progressive Node.js framework
Custom decorators | NestJS - A progressive Node.js framework

profile
개인 기록용 블로그

0개의 댓글