Guards

Guards

๊ฐ€๋“œ๋Š” @Injectable() ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋กœ ์ฃผ์„์ด ๋‹ฌ๋ฆฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. ๊ฐ€๋“œ๋Š”CanActivate ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

๊ฐ€๋“œ๋Š” ๋‹จ๋… ์ฑ…์ž„์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋Ÿฐํƒ€์ž„์— ์กด์žฌํ•˜๋Š” ํŠน์ • ์กฐ๊ฑด (์˜ˆ: ๊ถŒํ•œ, ์—ญํ• , ACL ๋“ฑ)์— ๋”ฐ๋ผ ์ง€์ •๋œ ์š”์ฒญ์ด ๋ผ์šฐํŠธ ํ•ธ๋“ค๋Ÿฌ์— ์˜ํ•ด ์ฒ˜๋ฆฌ๋  ์ง€ ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์ข…์ข… ๊ถŒํ•œ์ด๋ผ๊ณ ํ•ฉ๋‹ˆ๋‹ค. ์ธ์ฆ (๋ฐ ์ผ๋ฐ˜์ ์œผ๋กœ ๊ณต๋™ ์ž‘์—… ์ธ ์ธ์ฆ)์€ ์ผ๋ฐ˜์ ์œผ๋กœ ๊ธฐ์กด Express ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์˜ ๋ฏธ๋“ค์›จ์–ด์— ์˜ํ•ด ์ฒ˜๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ฐ ์†์„ฑ์„ ์š”์ฒญ(Request)๊ฐœ์ฒด์— ์—ฐ๊ฒฐํ•˜๋Š” ๊ฒƒ๊ณผ ๊ฐ™์€ ๊ฒƒ์€ ํŠน์ • ๊ฒฝ๋กœ ์ปจํ…์ŠคํŠธ (๋ฐ ํ•ด๋‹น ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ)์™€ ๊ฐ•๋ ฅํ•˜๊ฒŒ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋ฏธ๋“ค์›จ์–ด๋Š” ์ธ์ฆ์— ์ ํ•ฉํ•œ ์„ ํƒ์ž…๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ๋ฏธ๋“ค์›จ์–ด๋Š” ๋ณธ์งˆ์ ์œผ๋กœ ๋ฐ”๋ณด์ž…๋‹ˆ๋‹ค. next()ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•œ ํ›„ ์–ด๋–ค ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์‹คํ–‰๋ ์ง€ ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด์— Guards๋Š” ExecutionContext ์ธ์Šคํ„ด์Šค์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋‹ค์Œ์— ๋ฌด์—‡์ด ์‹คํ–‰ ๋ ์ง€ ์ •ํ™•ํžˆ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์š”์ฒญ/์‘๋‹ต ์ฃผ๊ธฐ์˜ ์ •ํ™•ํ•œ ์‹œ์ ์— ์ฒ˜๋ฆฌ ๋กœ์ง์„ ์‚ฝ์ž…ํ•˜๊ณ  ์„ ์–ธ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ์˜ˆ์™ธ ํ•„ํ„ฐ, ํŒŒ์ดํ”„ ๋ฐ ์ธํ„ฐ์…‰ํ„ฐ์™€ ๊ฐ™์ด ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ฝ”๋“œ๋ฅผ ๊ฑด์กฐํ•˜๊ณ  ์„ ์–ธ์ ์œผ๋กœ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

info ํžŒํŠธ ๊ฐ€๋“œ๋Š” ๊ฐ ๋ฏธ๋“ค์›จ์–ด ํ›„์— ์‹คํ–‰๋˜์ง€๋งŒ, ์ธํ„ฐ์…‰ํ„ฐ ๋‚˜ ํŒŒ์ดํ”„๋Š” ์ „์— ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

Authorization guard

์–ธ๊ธ‰ ํ•œ ๋ฐ”์™€ ๊ฐ™์ด ๊ถŒํ•œ์€ ๋ฐœ์‹ ์ž (์ผ๋ฐ˜์ ์œผ๋กœ ํŠน์ • ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž)์—๊ฒŒ ์ถฉ๋ถ„ํ•œ ๊ถŒํ•œ์ด ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ ํŠน์ • ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— Guards์˜ ํ›Œ๋ฅญํ•œ ์‚ฌ์šฉ ์‚ฌ๋ก€์ž…๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๊ฐ€ ๊ตฌ์ถ• ํ•  AuthGuard๋Š” ์ด์ œ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋ฅผ ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค (๋”ฐ๋ผ์„œ ํ† ํฐ์ด ์š”์ฒญ ํ—ค๋”์— ์ฒจ๋ถ€๋˜์–ด ์žˆ์Œ). ํ† ํฐ์„ ์ถ”์ถœํ•˜๊ณ  ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•˜๊ณ  ์ถ”์ถœ๋œ ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์š”์ฒญ์„ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.

@@filename(auth.guard)
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}
@@switch
import { Injectable } from '@nestjs/common';

@Injectable()
export class AuthGuard {
  async canActivate(context) {
    const request = context.switchToHttp().getRequest();
    return await validateRequest(request);
  }
}

validateRequest() ํ•จ์ˆ˜ ๋‚ด๋ถ€์˜ ๋…ผ๋ฆฌ๋Š” ํ•„์š”ํ•œ ๋งŒํผ ๊ฐ„๋‹จํ•˜๊ฑฐ๋‚˜ ์ •๊ตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์˜ˆ์˜ ์š”์ ์€ ๋ณดํ˜ธ์ž๊ฐ€ ์š”์ฒญ/์‘๋‹ต ์ฃผ๊ธฐ์— ์–ด๋–ป๊ฒŒ ๋งž๋Š”์ง€ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

๋ชจ๋“  ๊ฐ€๋“œ๋Š” canActivate()ํ•จ์ˆ˜๋ฅผ ๊ตฌํ˜„ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค. ์ด ํ•จ์ˆ˜๋Š” ํ˜„์žฌ ์š”์ฒญ์ด ํ—ˆ์šฉ๋˜๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์กฐ๊ฑด๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์‘๋‹ต์„ ๋™๊ธฐ์‹ ๋˜๋Š” ๋น„๋™๊ธฐ์‹์œผ๋กœ ๋ฐ˜ํ™˜ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค (Promise ๋˜๋Š” Observable์„ ํ†ตํ•ด). Nest๋Š” ๋ฆฌํ„ด ๊ฐ’์„ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์Œ ์กฐ์น˜๋ฅผ ์ œ์–ดํ•ฉ๋‹ˆ๋‹ค.

  • true๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด ์š”์ฒญ์ด ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

  • 'false'๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด Nest๋Š” ์š”์ฒญ์„ ๊ฑฐ๋ถ€ํ•ฉ๋‹ˆ๋‹ค.

canActivate()ํ•จ์ˆ˜๋Š” ExecutionContext์ธ์Šคํ„ด์Šค๋ผ๋Š” ๋‹จ์ผ ์ธ์ˆ˜๋ฅผ ์ทจํ•ฉ๋‹ˆ๋‹ค. ExecutionContext๋Š” ArgumentsHost์—์„œ ์ƒ์†๋ฐ›์Šต๋‹ˆ๋‹ค. ์˜ˆ์™ธ ํ•„ํ„ฐ ์ฑ•ํ„ฐ์—์„œ ์•ž์„œ ArgumentsHost๋ฅผ ๋ณด์•˜์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” original ํ•ธ๋“ค๋Ÿฌ๋กœ ์ „๋‹ฌ๋œ ์ธ์ˆ˜๋ฅผ ๊ฐ์‹ธ๋Š” ๋ž˜ํผ์ด๋ฉฐ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์˜ ์œ ํ˜•์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์ธ์ˆ˜ ๋ฐฐ์—ด์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ์ด ์ฃผ์ œ์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์˜ˆ์™ธ ํ•„ํ„ฐ ์ฑ•ํ„ฐ์„ ๋‹ค์‹œ ์ฐธ์กฐํ•˜์‹ญ์‹œ์˜ค.

Execution context

ExecutionContext๋Š” ArgumentsHost๋ฅผ ํ™•์žฅํ•˜์—ฌ ํ˜„์žฌ ์‹คํ–‰ ํ”„๋กœ์„ธ์Šค์— ๋Œ€ํ•œ ์ถ”๊ฐ€ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ๋ชจ์Šต์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

export interface ExecutionContext extends ArgumentsHost {
  getClass<T = any>(): Type<T>;
  getHandler(): Function;
}

getHandler()๋ฉ”์†Œ๋“œ๋Š” ํ˜ธ์ถœ๋  ํ•ธ๋“ค๋Ÿฌ์— ๋Œ€ํ•œ ์ฐธ์กฐ๋ฅผ ๋ฆฌํ„ดํ•ฉ๋‹ˆ๋‹ค. getClass()๋ฉ”์†Œ๋“œ๋Š” ์ด ํŠน์ • ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์†ํ•˜๋Š” Controller ํด๋ž˜์Šค์˜ ์œ ํ˜•์„ ๋ฆฌํ„ดํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ํ˜„์žฌ ์ฒ˜๋ฆฌ ๋œ ์š”์ฒญ์ด CatsController์—์„œ create()๋ฉ”์†Œ๋“œ๋กœ ์ง€์ •๋œ POST ์š”์ฒญ ์ธ ๊ฒฝ์šฐ, getHandler()๋Š” create() ๋ฉ”์†Œ๋“œ์— ๋Œ€ํ•œ ์ฐธ์กฐ๋ฅผ ๋ฆฌํ„ดํ•˜๊ณ  getClass()๋Š” CatsController type (์ธ์Šคํ„ด์Šค ์•„๋‹˜)์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

Role-based authentication

ํŠน์ • ์—ญํ• ์„ ๊ฐ€์ง„ ์‚ฌ์šฉ์ž๋งŒ ์•ก์„ธ์Šค ํ•  ์ˆ˜ ์žˆ๋Š” ๋ณด๋‹ค ๊ธฐ๋Šฅ์ ์ธ ๋ณดํ˜ธ ๊ธฐ๋Šฅ์„ ๊ตฌ์ถ•ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ๊ฐ€๋“œ ํ…œํ”Œ๋ฆฟ์œผ๋กœ ์‹œ์ž‘ํ•˜์—ฌ ๋‹ค์Œ ์„น์…˜์—์„œ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ๋กœ์„œ๋Š” ๋ชจ๋“  ์š”์ฒญ์„ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@@filename(roles.guard)
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class RolesGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    return true;
  }
}
@@switch
import { Injectable } from '@nestjs/common';

@Injectable()
export class RolesGuard {
  canActivate(context) {
    return true;
  }
}

Binding guards

ํŒŒ์ดํ”„ ๋ฐ ์˜ˆ์™ธ ํ•„ํ„ฐ์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๊ฐ€๋“œ๋Š” ์ปจํŠธ๋กค๋Ÿฌ ๋ฒ”์œ„(controller-scoped), ๋ฐฉ๋ฒ• ๋ฒ”์œ„(method-scoped) ๋˜๋Š” ์ „์—ญ ๋ฒ”์œ„(global-scoped) ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” @UseGuards()๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ปจํŠธ๋กค๋Ÿฌ ๋ฒ”์œ„์˜ ๋ณดํ˜ธ๋Œ€๋ฅผ ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋Š” ๋‹จ์ผ ์ธ์ˆ˜ ๋˜๋Š” ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ์ธ์ˆ˜ ๋ชฉ๋ก์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ํ•œ ๋ฒˆ์˜ ์„ ์–ธ์œผ๋กœ ์ ์ ˆํ•œ ๊ฐ€๋“œ ์„ธํŠธ๋ฅผ ์‰ฝ๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@@filename()
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}

info ํžŒํŠธ @UseGuards()๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋Š” @nestjs/common ํŒจํ‚ค์ง€์—์„œ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

์œ„์—์„œ ์šฐ๋ฆฌ๋Š” ์ธ์Šคํ„ด์Šค ๋Œ€์‹ ์— ๋กค ๊ฐ€๋“œ (RolesGuard) ์œ ํ˜•์„ ์ „๋‹ฌํ•˜์—ฌ ํ”„๋ ˆ์ž„ ์›Œํฌ์— ์ธ์Šคํ„ด์Šคํ™”์™€ ์˜์กด์„ฑ ์ฃผ์ž…์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ–ˆ์Šต๋‹ˆ๋‹ค. ํŒŒ์ดํ”„ ๋ฐ ์˜ˆ์™ธ ํ•„ํ„ฐ์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋‚ด๋ถ€ ์ธ์Šคํ„ด์Šค๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

@@filename()
@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}

์œ„์˜ ๊ตฌ์„ฑ์€์ด ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์„ ์–ธํ•œ ๋ชจ๋“  ํ•ธ๋“ค๋Ÿฌ์— ๊ฐ€๋“œ๋ฅผ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€๋“œ๊ฐ€ ๋‹จ์ผ ๋ฉ”์†Œ๋“œ์—๋งŒ ์ ์šฉ๋˜๋„๋ก ํ•˜๋ ค๋ฉด ๋ฉ”์†Œ๋“œ ์ˆ˜์ค€์—์„œ @UseGuards()๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.

์ „์—ญ ๊ฐ€๋“œ๋ฅผ ์„ค์ •ํ•˜๋ ค๋ฉด Nest ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ธ์Šคํ„ด์Šค์˜ useGlobalGuards()๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค.

@@filename()
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());

warning ์•Œ๋ฆผ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•ฑ์˜ ๊ฒฝ์šฐ useGlobalGuards()๋ฉ”์†Œ๋“œ๋Š” ๊ฒŒ์ดํŠธ์›จ์ด ๋ฐ ๋งˆ์ดํฌ๋กœ ์„œ๋น„์Šค์— ๋Œ€ํ•œ ๋ณดํ˜ธ๋ฅผ ์„ค์ •ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. "ํ‘œ์ค€"(ํ•˜์ด๋ธŒ๋ฆฌ๋“œ๊ฐ€ ์•„๋‹Œ) ๋งˆ์ดํฌ๋กœ ์„œ๋น„์Šค ์•ฑ์˜ ๊ฒฝ์šฐ useGlobalGuards()๋Š” ๊ฐ€๋“œ๋ฅผ ์ „์—ญ์œผ๋กœ ๋งˆ์šดํŠธํ•ฉ๋‹ˆ๋‹ค.

์ „์—ญ ๊ฐ€๋“œ๋Š” ๋ชจ๋“  ์ปจํŠธ๋กค๋Ÿฌ์™€ ๋ชจ๋“  ๊ฒฝ๋กœ ์ฒ˜๋ฆฌ๊ธฐ์— ๋Œ€ํ•ด ์ „์ฒด ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์—์„œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์˜์กด์„ฑ ์ฃผ์ž…์˜ ๊ด€์ ์—์„œ, ๋ชจ๋“ˆ ์™ธ๋ถ€์—์„œ ๋“ฑ๋ก๋œ ์ „์—ญ ๊ฐ€๋“œ (์œ„์˜ ์˜ˆ์—์„œ์™€ ๊ฐ™์ด useGlobalGuards()๋กœ)๋Š” ์˜์กด์„ฑ์ด ์ฃผ์ž…๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ด๋Š” ๋ชจ๋“ˆ์˜ ์ปจํ…์ŠคํŠธ ๋ฐ–์—์„œ ์ˆ˜ํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ ๊ตฌ์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ชจ๋“  ๋ชจ๋“ˆ์—์„œ ์ง์ ‘ ๊ฐ€๋“œ๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@@filename(app.module)
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
export class AppModule {}

info ํžŒํŠธ ๊ฐ€๋“œ์— ๋Œ€ํ•œ ์˜์กด์„ฑ ์ฃผ์ž…์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด์ด ์ ‘๊ทผ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•  ๋•Œ, ์ด ๊ตฌ์„ฑ์ด ์‚ฌ์šฉ๋˜๋Š” ๋ชจ๋“ˆ์— ๊ด€๊ณ„์—†์ด, ๊ฐ€๋“œ๋Š” ์‹ค์ œ๋กœ ์ „์—ญ์ž…๋‹ˆ๋‹ค. ์–ด๋””์—์„œ ํ•ด์•ผ ํ•ฉ๋‹ˆ๊นŒ? ๊ฐ€๋“œ (์œ„ ์˜ˆ์—์„œRolesGuard)๊ฐ€ ์ •์˜๋œ ๋ชจ๋“ˆ์„ ์„ ํƒํ•˜์‹ญ์‹œ์˜ค. ๋˜ํ•œ ์ปค์Šคํ…€ ํ”„๋กœ ๋ฐ”์ด๋” ๋“ฑ๋ก์„ ๋‹ค๋ฃจ๋Š” ์œ ์ผํ•œ ๋ฐฉ๋ฒ•์€ useClass๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์— ๋Œ€ํ•ด ์ž์„ธํžˆ ์•Œ์•„๋ณด์‹ญ์‹œ์˜ค.

Reflection

์šฐ๋ฆฌ์˜ RolesGuard๊ฐ€ ์ž‘๋™ํ•˜๊ณ  ์žˆ์ง€๋งŒ ์•„์ง ๋˜‘๋˜‘ํ•˜์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ์•„์ง ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ณดํ˜ธ ๊ธฐ๋Šฅ์ธ ์‹คํ–‰ ์ปจํ…์ŠคํŠธ(execution context)๋ฅผ ํ™œ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์•„์ง ์—ญํ• ์ด๋‚˜ ๊ฐ ์ฒ˜๋ฆฌ๊ธฐ์— ํ—ˆ์šฉ๋˜๋Š” ์—ญํ• ์— ๋Œ€ํ•ด์„œ๋Š” ์•„์ง ์•Œ์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด CatsController๋Š” ๊ฒฝ๋กœ๋งˆ๋‹ค ๋‹ค๋ฅธ ๊ถŒํ•œ ์ฒด๊ณ„๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ผ๋ถ€๋Š” ๊ด€๋ฆฌ์ž๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ณ  ๋‹ค๋ฅธ ์ผ๋ถ€๋Š” ๋ชจ๋“  ์‚ฌ๋žŒ์—๊ฒŒ ๊ณต๊ฐœ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์œ ์—ฐํ•˜๊ณ  ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ฐฉ์‹์œผ๋กœ ์—ญํ• ๊ณผ ๊ฒฝ๋กœ๋ฅผ ์–ด๋–ป๊ฒŒ ์ผ์น˜์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ?

์‚ฌ์šฉ์ž ์ง€์ • ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ๊ฐ€ ์‚ฌ์šฉ๋˜๋Š” ๊ณณ์ž…๋‹ˆ๋‹ค. Nest๋Š” @SetMetadata()๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ํ†ตํ•ด ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋ผ์šฐํŠธํ•˜๊ธฐ ์œ„ํ•ด ์ปค์Šคํ…€ ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒจ๋ถ€ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ๋Š” ์Šค๋งˆํŠธ ๊ฐ€๋“œ๊ฐ€ ๊ฒฐ์ •์„ ๋‚ด๋ ค์•ผ ํ•˜๋Š” ๋ˆ„๋ฝ๋œ ์—ญํ•  ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. @SetMetadata()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ด…์‹œ๋‹ค :

@@filename(cats.controller)
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
@@switch
@Post()
@SetMetadata('roles', ['admin'])
@Bind(Body())
async create(createCatDto) {
  this.catsService.create(createCatDto);
}

info ํžŒํŠธ @SetMetadata()๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋Š”@nestjs/common ํŒจํ‚ค์ง€์—์„œ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

์œ„์˜ ๊ตฌ์„ฑ์—์„œ ์šฐ๋ฆฌ๋Š” roles ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ (roles ๊ฐ€ ํ•ต์‹ฌ์ด๊ณ ['admin']์ด ํŠน์ • ๊ฐ’์ž„)๋ฅผ create()๋ฉ”์†Œ๋“œ์— ์ฒจ๋ถ€ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ์ž‘๋™ํ•˜๋Š” ๋™์•ˆ ๊ฒฝ๋กœ์—์„œ ์ง์ ‘ @SetMetadata()๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ์ข‹์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋Œ€์‹  ์•„๋ž˜์™€ ๊ฐ™์ด ์ž์‹  ๋งŒ์˜ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ๋งŒ๋“œ์‹ญ์‹œ์˜ค.

@@filename(roles.decorator)
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
@@switch
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles) => SetMetadata('roles', roles);

์ด ์ ‘๊ทผ ๋ฐฉ์‹์€ ํ›จ์”ฌ ๊นจ๋—ํ•˜๊ณ  ์ฝ๊ธฐ ์‰ฝ๊ณ  ๊ฐ•๋ ฅํ•˜๊ฒŒ ์ž…๋ ฅ๋ฉ๋‹ˆ๋‹ค. ์ด์ œ ์ปค์Šคํ…€ @Roles()๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ ์ด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ create()๋ฉ”์†Œ๋“œ๋ฅผ ์žฅ์‹ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@@filename(cats.controller)
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
@@switch
@Post()
@Roles('admin')
@Bind(Body())
async create(createCatDto) {
  this.catsService.create(createCatDto);
}

Putting it all together

์ด์ œ ๋Œ์•„๊ฐ€์„œ ์ด๊ฒƒ์„ RolesGuard์™€ ํ•จ๊ป˜ ๋ฌถ์–ด ๋ด…์‹œ๋‹ค. ํ˜„์žฌ๋Š” ๋ชจ๋“  ๊ฒฝ์šฐ์— ๋‹จ์ˆœํžˆ true๋ฅผ ๋ฐ˜ํ™˜ํ•˜์—ฌ ๋ชจ๋“  ์š”์ฒญ์„ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ• ๋‹น ๋œ ์—ญํ• ์„ ์ฒ˜๋ฆฌ์ค‘์ธ ํ˜„์žฌ ๊ฒฝ๋กœ์— ํ•„์š”ํ•œ ์‹ค์ œ ์—ญํ• ๊ณผ ๋น„๊ตํ•˜์—ฌ ๋ฐ˜ํ™˜ ๊ฐ’์„ ์กฐ๊ฑด๋ถ€๋กœ ๋งŒ๋“ค๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. ๊ฒฝ๋กœ์˜ ์—ญํ•  (์ปค์Šคํ…€ ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ)์— ์•ก์„ธ์Šคํ•˜๊ธฐ ์œ„ํ•ด ํ”„๋ ˆ์ž„ ์›Œํฌ์— ์˜ํ•ด ์ œ๊ณต๋˜๊ณ @nestjs/core ํŒจํ‚ค์ง€์—์„œ ์ œ๊ณต๋˜๋Š” Reflector ํ—ฌํผ ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

@@filename(roles.guard)
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    const hasRole = () => user.roles.some((role) => roles.includes(role));
    return user && user.roles && hasRole();
  }
}
@@switch
import { Injectable, Dependencies } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
@Dependencies(Reflector)
export class RolesGuard {
  constructor(reflector) {
    this.reflector = reflector;
  }

  canActivate(context) {
    const roles = this.reflector.get('roles', context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    const hasRole = () => user.roles.some((role) => roles.includes(role));
    return user && user.roles && hasRole();
  }
}

info ํžŒํŠธ node.js ์„ธ๊ณ„์—์„œ๋Š” ๊ถŒํ•œ์ด ์žˆ๋Š” ์‚ฌ์šฉ์ž๋ฅผ request ์˜ค๋ธŒ์ ํŠธ์— ์ฒจ๋ถ€ํ•˜๋Š” ๊ฒƒ์ด ์ผ๋ฐ˜์ ์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์œ„์˜ ์ƒ˜ํ”Œ ์ฝ”๋“œ์—์„œ request.user์— ์‚ฌ์šฉ์ž ์ธ์Šคํ„ด์Šค์™€ ํ—ˆ์šฉ๋œ ์—ญํ• ์ด ํฌํ•จ๋˜์–ด ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค. ์•ฑ์—์„œ ์•„๋งˆ๋„ ์‚ฌ์šฉ์ž ์ง€์ • ์ธ์ฆ ๊ฐ€๋“œ(authentication guard) (๋˜๋Š” ๋ฏธ๋“ค์›จ์–ด)์—์„œ ํ•ด๋‹น ์—ฐ๊ฒฐ์„ ๋งŒ๋“ค ๊ฒƒ์ž…๋‹ˆ๋‹ค.

Reflector ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ง€์ •๋œ key์— ์˜ํ•ด ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ์— ์‰ฝ๊ฒŒ ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค (์ด ๊ฒฝ์šฐ ํ‚ค๋Š” roles์ž…๋‹ˆ๋‹ค. roles.decorator.tsํŒŒ์ผ๊ณผ ๊ฑฐ๊ธฐ์—์„œ ๋งŒ๋“ค์–ด์ง„ SetMetadata()ํ˜ธ์ถœ์„ ๋‹ค์‹œ ์ฐธ์กฐํ•˜์‹ญ์‹œ์˜ค). ์œ„์˜ ์˜ˆ์—์„œ ํ˜„์žฌ ์ฒ˜๋ฆฌ ๋œ ์š”์ฒญ ๋ฉ”์†Œ๋“œ์˜ ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”์ถœํ•˜๊ธฐ ์œ„ํ•ด context.getHandler()๋ฅผ ์ „๋‹ฌํ–ˆ์Šต๋‹ˆ๋‹ค. getHandler()๋Š” ๊ฒฝ๋กœ ํ•ธ๋“ค๋Ÿฌ ํ•จ์ˆ˜์— ์ฐธ์กฐ๋ฅผ ์ œ๊ณตํ•œ๋‹ค๋Š” ๊ฒƒ์„ ๊ธฐ์–ตํ•˜์‹ญ์‹œ์˜ค.

์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”์ถœํ•˜๊ณ  ์ด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ˜„์žฌ ์‚ฌ์šฉ์ž ์—ญํ• ์„ ๊ฒฐ์ •ํ•จ์œผ๋กœ์จ ์ด ๊ฐ€๋“œ๋ฅผ ๋ณด๋‹ค ์ผ๋ฐ˜์ ์œผ๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”์ถœํ•˜๊ธฐ ์œ„ํ•ด ์šฐ๋ฆฌ๋Š” context.getHandler() ๋Œ€์‹  context.getClass()๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค:

@@filename()
const roles = this.reflector.get<string[]>('roles', context.getClass());
@@switch
const roles = this.reflector.get('roles', context.getClass());

๊ถŒํ•œ์ด ์ถฉ๋ถ„ํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๊ฐ€ ์—”๋“œ ํฌ์ธํŠธ๋ฅผ ์š”์ฒญํ•˜๋ฉด Nest๋Š” ๋‹ค์Œ ์‘๋‹ต์„ ์ž๋™์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

{
  "statusCode": 403,
  "message": "Forbidden resource"
}

๋’ค์—์„œ ๊ฐ€๋“œ๊ฐ€ false๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด ํ”„๋ ˆ์ž„ ์›Œํฌ์—์„œ ForbiddenException์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์˜ค๋ฅ˜ ์‘๋‹ต์„ ๋ฆฌํ„ดํ•˜๋ ค๋ฉด ๊ณ ์œ ํ•œ ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

throw new UnauthorizedException();

๊ฐ€๋“œ์— ์˜ํ•ด ๋ฐœ์ƒ ๋œ ์˜ˆ์™ธ๋Š” ์˜ˆ์™ธ ๊ณ„์ธต(์ „์—ญ ์˜ˆ์™ธ ํ•„ํ„ฐ ๋ฐ ํ˜„์žฌ ์ปจํ…์ŠคํŠธ์— ์ ์šฉ๋˜๋Š” ๋ชจ๋“  ์˜ˆ์™ธ ํ•„ํ„ฐ)์— ์˜ํ•ด ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

Last updated

Was this helpful?