๐Ÿ” 06. ์ธ์ฆ ์ฒ˜๋ฆฌ โ€” ๋‹น์‹ ์€ ๋ˆ„๊ตฌ๊ณ , ์–ด๋””๊นŒ์ง€ ๋“ค์–ด๊ฐˆ ์ˆ˜ ์žˆ๋Š”๊ฐ€?

2026๋…„ 2์›” 26์ผ ์ˆ˜์ •๋จ

๐Ÿ“‹ ๊ฐœ์š”

๋ˆ„๊ฐ€ ์š”์ฒญ์„ ๋ณด๋ƒˆ๋Š”์ง€(์ธ์ฆ)์™€ ๊ทธ ์š”์ฒญ์„ ํ—ˆ๋ฝํ•  ๊ฒƒ์ธ์ง€(์ธ๊ฐ€)๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ํ†ต์ œํ•˜๋Š” Guard, Strategy, ๊ทธ๋ฆฌ๊ณ  ์ปค์Šคํ…€ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ํ™œ์šฉ๋ฒ•์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 20๋ถ„ (์ „์ฒด) / ํ•ต์‹ฌ ํŒŒํŠธ๋งŒ: 10๋ถ„

๐Ÿงณ ์ „์ œ ์ง€์‹ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • JWT(JSON Web Token)๊ฐ€ ๋ฌด์—‡์ธ์ง€ ์•„์ฃผ ๊ฐ„๋žตํ•˜๊ฒŒ๋ผ๋„ ์•Œ๊ณ  ์žˆ๋‹ค.
  • ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋ชจ๋“ˆ๊ณผ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ๋“ฑ๋กํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•ˆ๋‹ค.

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ํ๋ฆ„
์ธ์ฆ/์ธ๊ฐ€ ๊ฐœ๋… ์ •๋ฆฌ โ†’ Supabase ๊ธฐ๋ฐ˜ JWT ๊ฒ€์ฆ ์„ธํŒ… โ†’ ๊ฐ€๋“œ(Guard)๋กœ ๋ฌธ๋‹จ์†ํ•˜๊ธฐ โ†’ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ์ง์ ‘ ๋งŒ๋“ค์–ด์„œ ์šฐ์•„ํ•˜๊ฒŒ ์“ฐ๊ธฐ โ†’ ์‹ค๋ฌด ํŒ & ์—๋Ÿฌ ๋Œ€์ฒ˜๋ฒ•

๐ŸŽฏ ์ด ๋ฌธ์„œ๋ฅผ ๋‹ค ์ฝ์œผ๋ฉด ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ

  • ๋ฉด์ ‘๊ด€์ด ๋“ค์–ด์™€์„œ "์ธ์ฆ๊ณผ ์ธ๊ฐ€์˜ ์ฐจ์ด๋Š”?" ์ด๋ผ๊ณ  ๋ฌผ์–ด๋ดค์„ ๋•Œ ๋น„์œ ๋ฅผ ๋“ค์–ด 10์ดˆ ์•ˆ์— ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋‚ด API ์ „์ฒด์— ๋ฌด์  ๋ฐฉ์–ด๋ง‰(Guard)์„ ์น˜๊ณ , ํŠน์ • ๋ผ์šฐํŠธ๋งŒ ์˜ˆ์™ธ์ ์œผ๋กœ ์—ด์–ด์ค„ ์ˆ˜ ์žˆ๋‹ค.
  • req.user ๋Œ€์‹  @CurrentUser()๋ผ๋Š” ์ปค์Šคํ…€ ์Šคํ‚ฌ์„ ๋š๋”ฑ ๋งŒ๋“ค์–ด ์“ธ ์ˆ˜ ์žˆ๋‹ค.

๐Ÿค” ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€

๋ฐฑ์—”๋“œ ์„œ๋ฒ„์˜ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ์ž„๋ฌด๋Š” '๋ฐ์ดํ„ฐ๋ฅผ ์ง€ํ‚ค๋Š” ๊ฒƒ'์ด์•ผ.
ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ๋กœ์ง์ด์•ผ Supabase ๊ฐ™์€ ์™ธ๋ถ€ ํˆด์ด ๋Œ€์‹ ํ•ด์ค„ ์ˆ˜ ์žˆ์ง€๋งŒ,
"์ด ์š”์ฒญ์„ ๋ณด๋‚ธ ์‚ฌ๋žŒ์ด ์ง„์งœ ๋กœ๊ทธ์ธํ•œ ์œ ์ €์ธ์ง€?", "์ด ์œ ์ €๊ฐ€ ๊ด€๋ฆฌ์ž๋งŒ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒŒ์‹œ๊ธ€์„ ์‚ญ์ œํ•˜๋ ค๊ณ  ํ•˜๋Š” ๊ฑด ์•„๋‹Œ์ง€?"๋ฅผ ๊ฒ€์‚ฌํ•˜๋Š” ๊ฑด ์˜จ์ „ํžˆ ์šฐ๋ฆฌ ๋ฐฑ์—”๋“œ ์„œ๋ฒ„์˜ ๋ชซ์ด๊ฑฐ๋“ .
NestJS์˜ Guard์™€ Decorator๋ฅผ ์กฐํ•ฉํ•˜๋ฉด ์ด ๋ณต์žกํ•œ ๊ฒ€์‚ฌ ๋กœ์ง์„ ์ˆ˜๋ฐฑ ๊ฐœ์˜ ๋ผ์šฐํ„ฐ์— ๋‹จ 1์ค„์˜ ์ฝ”๋“œ๋กœ ๋—๋‹ค ๋ถ™์˜€๋‹ค ํ•  ์ˆ˜ ์žˆ์–ด.


๐Ÿ—๏ธ ๋น„์œ ๋กœ ๋จผ์ € ์ดํ•ดํ•˜๊ธฐ

๐Ÿง’ 5์‚ด์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด?

๊ฑฐ๋Œ€ํ•œ ํšŒ์‚ฌ ๊ฑด๋ฌผ์„ ์ƒ์ƒํ•ด ๋ณด์ž.
์ธ์ฆ (Authentication): "์‹ ๋ถ„์ฆ ๋ฐœ๊ธ‰๊ณผ ์‹ ๋ถ„์ฆ ์œ„์กฐ ๊ฒ€์‚ฌ"
๋กœ๋น„์— ๋“ค์–ด์˜จ ์‚ฌ๋žŒ์ด ์ง์›์ด๋ผ๋Š” ๊ฑธ ์ฆ๋ช…(๋กœ๊ทธ์ธ ์•„์ด๋””/๋น„๋ฒˆ)ํ•˜๋ฉด '์‚ฌ์›์ฆ(JWT)'์„ ๋ฐœ๊ธ‰ํ•ด์ค˜. ๋งŒ์•ฝ ์‚ฌ์›์ฆ์„ ๋“ค๊ณ  ์˜ค๋ฉด, ๊ฒฝ๋น„์›(Guard)์ด ์ด๊ฒŒ ์ง„์งœ ํšŒ์‚ฌ์—์„œ ๋ฐœ๊ธ‰ํ•œ ์‚ฌ์›์ฆ์ด ๋งž๋Š”์ง€ ํ™€๋กœ๊ทธ๋žจ(์„œ๋ช…)์„ ํ™•์ธํ•ด.
์ธ๊ฐ€ (Authorization): "์ถœ์ž… ๊ถŒํ•œ ๊ฒ€์‚ฌ"
์‚ฌ์›์ฆ ๊ฒ€์‚ฌ์— ํ†ต๊ณผํ–ˆ์–ด. ๊ทธ๋Ÿฐ๋ฐ ์ด ์ง์›์ด ์‚ฌ์›์ฆ์„ ์ฐ๊ณ  '์‚ฌ์žฅ์‹ค'์ด๋‚˜ '๊ธฐ๋ฐ€๋ฌธ์„œ ๋ณด๊ด€์‹ค' ๋ฌธ์„ ์—ด๋ ค๊ณ  ํ•ด. ๊ฒฝ๋น„์›์ด ์ง์›์˜ ์ง๊ธ‰(Role)์„ ํ™•์ธํ•œ ๋’ค "์–ดํ—ˆ, ์ผ๋ฐ˜ ์‚ฌ์›์€ ์—ฌ๊ธธ ๋ชป ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค" ํ•˜๊ณ  ๋ง‰์•„์„œ๋Š” ๊ณผ์ •์ด์•ผ.


๐Ÿงฉ ์ธ์ฆ๊ณผ ์ธ๊ฐ€ โ€” ์ ˆ๋Œ€ ํ—ท๊ฐˆ๋ฆฌ๋ฉด ์•ˆ ๋˜๋Š” ๋‘ ๋‹จ์–ด ๐ŸŸก

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • AuthN๊ณผ AuthZ๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ๋ถ„๋ฆฌํ•ด์„œ ์‚ฌ๊ณ ํ•  ์ˆ˜ ์žˆ๋‹ค.

์‹ค๋ฌด์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ํ˜ผ์šฉํ•˜๋Š” ๋‹จ์–ด Top 1์œ„์•ผ. ์˜์–ด๋กœ๋Š” ์•ž ๊ธ€์ž๋ฅผ ๋”ฐ์„œ AuthN(์ธ์ฆ), AuthZ(์ธ๊ฐ€) ๋กœ ๋งŽ์ด ์ค„์—ฌ์„œ ๋ถ€๋ฅด์ง€.

๊ตฌ๋ถ„์ธ์ฆ (AuthN / Authentication)์ธ๊ฐ€ (AuthZ / Authorization)
ํ•ต์‹ฌ ์งˆ๋ฌธ"๋‹น์‹ ์€ ๋„๋Œ€์ฒด ๋ˆ„๊ตฌ ์ธ๊ฐ€?""๋‹น์‹ ์ด ์ด๊ฑธ ํ•ด๋„ ๋˜๋Š”๊ฐ€?"
์‹คํŒจ ์‹œ ์‘๋‹ต401 Unauthorized403 Forbidden
๊ตฌํ˜„ ์š”์†Œpassport-jwt, JwtStrategyRolesGuard, @Roles('admin')

๐Ÿ’ก ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ์— ๋Œ€ํ•˜์—ฌ (Supabase ์‚ฌ์šฉ ์‹œ)

๋งŒ์•ฝ DB์— ์ง์ ‘ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ €์žฅํ•œ๋‹ค๋ฉด ๋ฌด์กฐ๊ฑด bcrypt ๊ฐ™์€ ํ•ด์‹ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์จ์•ผ ํ•˜์ง€๋งŒ, ์ง€๊ธˆ ์ด ๊ตฌ์กฐ์—์„œ๋Š” Supabase Auth๊ฐ€ ๊ทธ ์œ„ํ—˜ํ•œ ์ผ๋“ค์„ ์ „๋ถ€ ๋Œ€์‹  ์ฒ˜๋ฆฌํ•ด์ฃผ๊ณ  ์•ˆ์ „ํ•œ JWT ํ† ํฐ ๋งŒ ์šฐ๋ฆฌ์—๊ฒŒ ๋„˜๊ฒจ์ค˜. ์šฐ๋ฆฐ ๊ทธ ํ† ํฐ์ด ์œ„์กฐ๋˜์ง€ ์•Š์•˜๋Š”์ง€๋งŒ ๊ฒ€์‚ฌํ•˜๋ฉด ๋์ด์•ผ.


๐Ÿงฉ JWT Strategy์™€ Guard โ€” ์ž ๋“ค์ง€ ์•Š๋Š” ๊ฒฝ๋น„์› ๐ŸŸข

NestJS์—์„œ ์ด ์‚ฌ์›์ฆ ๊ฒ€์‚ฌ๊ธฐ๋ฅผ ๋“ค์ด๋Š” ๊ณผ์ •์„ ๋ณด์ž. passport๋ผ๋Š” ์œ ๋ช…ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ํž˜์„ ๋นŒ๋ฆด ๊ฑฐ์•ผ.

1. Strategy (์ „๋žต): "์ด ์‹ ๋ถ„์ฆ์„ ์ด๋ ‡๊ฒŒ ๊ฒ€์‚ฌํ•ด๋ผ"

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { passportJwtSecret } from 'jwks-rsa'; // Supabase ๊ณต๊ฐœํ‚ค๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•œ ๋งˆ๋ฒ• ์ง€ํŒก์ด
 
@Injectable()
export class SupabaseJwtStrategy extends PassportStrategy(Strategy) {
  constructor(configService: ConfigService) {
    const supabaseUrl = configService.getOrThrow('SUPABASE_URL');
    
    super({
      // "์š”์ฒญ์„ ๋ณด๋‚ผ ๋•Œ ๋จธ๋ฆฌ(Header)์— Bearer ๋ผ๊ณ  ์ ํžŒ ๊ณณ์—์„œ ํ† ํฐ์„ ๊บผ๋‚ด์„ธ์š”"
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false, // ๋งŒ๋ฃŒ๋œ ํ† ํฐ์€ ๊ฐ€์ฐจ์—†์ด ์ปท!
      
      // "Supabase ๋ณธ์‚ฌ์— ์—ฐ๋ฝํ•ด์„œ, ์ด ํ† ํฐ์ด ์ง„์งœ ์„œ๋ช…๋œ ๊ฒŒ ๋งž๋Š”์ง€ ์ธ์ฆํ‚ค๋ฅผ ๋ฐ›์•„์˜ค์„ธ์š”"
      secretOrKeyProvider: passportJwtSecret({
        cache: true,
        jwksUri: `${supabaseUrl}/auth/v1/.well-known/jwks.json`,
      }),
      algorithms: ['RS256'], // Supabase๊ฐ€ ์“ฐ๋Š” ์•”ํ˜ธํ™” ๋ฐฉ์‹
    });
  }
 
  // ๐Ÿšจ JWT ํ™€๋กœ๊ทธ๋žจ ๊ฒ€์‚ฌ๊ฐ€ ํ†ต๊ณผ๋˜๋ฉด ๋งˆ์ง€๋ง‰์œผ๋กœ ์ด ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋ผ!
  async validate(payload: any) {
    // ์—ฌ๊ธฐ์„œ ๋ฆฌํ„ดํ•˜๋Š” ๊ฐ์ฒด๊ฐ€ ๋ฐ”๋กœ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์“ธ ์ˆ˜ ์žˆ๋Š” '์œ ์ € ์ •๋ณด'๊ฐ€ ๋ผ.
    return {
      id: payload.sub,
      email: payload.email,
      role: payload.app_metadata?.provider || 'user',
    };
  }
}

2. Guard (๊ฐ€๋“œ): "๋ชจ๋“  ๋ฌธ์„ ํ‹€์–ด๋ง‰๋Š” ๊ฒฝ๋น„์› ์„  ๋ฐฐ์น˜"

์ „๋žต์€ ์งฐ์œผ๋‹ˆ, ์ด์ œ ์ด ์ „๋žต์„ ์ˆ˜ํ–‰ํ•  ์ง„์งœ ๊ฒฝ๋น„์›์„ ๋ณต๋„๋งˆ๋‹ค ์„ธ์›Œ๋ณด์ž.

// auth/guards/jwt-auth.guard.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
 
@Injectable()
export class JwtAuthGuard extends AuthGuard('supabase-jwt') {
  // ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์ปค์Šคํ…€ ๋ฉ”์‹œ์ง€๋ฅผ ๋„์šฐ๊ณ  ์‹ถ๋‹ค๋ฉด ์š” ํ•จ์ˆ˜๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•ด!
  handleRequest(err: any, user: any, info: any) {
    if (err || !user) {
      throw err || new UnauthorizedException('์‚ฌ์›์ฆ(๋กœ๊ทธ์ธ)์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค!');
    }
    return user; // ๐Ÿ‘ˆ validate()์—์„œ ๋ฆฌํ„ดํ–ˆ๋˜ ๋ฐ”๋กœ ๊ทธ ๊ฐ์ฒด
  }
}

[์‹ค๋ฌด ํŒ] ๊ธ€๋กœ๋ฒŒ๋กœ ๊ฒฝ๋น„์› ๊น”์•„๋ฒ„๋ฆฌ๊ธฐ
๋ผ์šฐํ„ฐ ๋ฐฑ ๊ฐœ๋งˆ๋‹ค @UseGuards(JwtAuthGuard)๋ฅผ ๋‹ฌ๋‹ค ๋ณด๋ฉด ๊ผญ ํ•˜๋‚˜์”ฉ ๋นผ๋จน์–ด์„œ ๋ณด์•ˆ ๊ตฌ๋ฉ์ด ๋ฐœ์ƒํ•ด. ์ฐจ๋ผ๋ฆฌ "๊ฑด๋ฌผ ์ „์ฒด๋ฅผ ํ์‡„ํ•˜๊ณ , ํ•„์š”ํ•œ ๊ณณ๋งŒ ์—ด์–ด์ฃผ๋Š”" ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๋ฐฉ์‹์ด ์‹ค๋ฌด์—์„  ์ •์„์ด์•ผ.

// app.module.ts
@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard, // ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋‹จ 1์ค„๋กœ ๋‚ด ์•ฑ์˜ ๋ชจ๋“  API๊ฐ€ ๋กœ๊ทธ์ธ ํ•„์ˆ˜๊ฐ€ ๋จ!
    },
  ],
})
export class AppModule {}

๐Ÿงฉ ์ปค์Šคํ…€ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ โ€” ๊ฒฝ๋น„์› ์กฐ์ข…ํ•˜๊ธฐ ๐ŸŸข

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • ๋‚˜๋งŒ์˜ @Public() ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.
  • ๋‚˜๋งŒ์˜ @CurrentUser() ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

๊ฑด๋ฌผ ์ „์ฒด๋ฅผ ์ž ๊ฐ€๋ฒ„๋ ธ์œผ๋‹ˆ, ๋กœ๊ทธ์ธ ํ™”๋ฉด์ด๋‚˜ ์ƒํ’ˆ ๊ตฌ๊ฒฝ ํ™”๋ฉด ๊ฐ™์ด "ํ† ํฐ ์—†์–ด๋„ ์ ‘์†ํ•ด์•ผ ํ•˜๋Š” ๊ณณ" ์— ๊ตฌ๋ฉ์„ ๋šซ์–ด์ค˜์•ผ๊ฒ ์ง€?
"๊ฒฝ๋น„์› ์•„์ €์”จ, ์ด ๋ฌธ ์•ž์—๋Š” ์ด ์Šคํ‹ฐ์ปค๊ฐ€ ๋ถ™์–ด์žˆ์œผ๋ฉด ๊ฑ ํ†ต๊ณผ์‹œ์ผœ์ฃผ์„ธ์š”" ๋ผ๊ณ  ์จ ๋ถ™์ผ ์Šคํ‹ฐ์ปค๋ฅผ ์ง์ ‘ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ. ์ด๊ฒƒ์ด ๋ฐ”๋กœ ์ปค์Šคํ…€ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ์•ผ.

1. @Public(): ๋ฌด์‚ฌํ†ต๊ณผ ์Šคํ‹ฐ์ปค ๋งŒ๋“ค๊ธฐ

// decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
 
// 'isPublic' ์ด๋ผ๋Š” ๋ผ๋ฒจ์— true๋ผ๋Š” ๊ฐ’์„ ์ฐ์–ด๋‚ด๋Š” ์Šคํ‹ฐ์ปค๋ฅผ ๋งŒ๋“ค์—ˆ์Œ!
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

์ด์ œ ์•„๊นŒ ๋งŒ๋“  ๊ฒฝ๋น„์›(JwtAuthGuard)์—๊ฒŒ ์ด ์Šคํ‹ฐ์ปค๋ฅผ ๋ณด๋Š” ๋ˆˆ์„ ๋‹ฌ์•„์ฃผ์ž.

// auth/guards/jwt-auth.guard.ts
import { Reflector } from '@nestjs/core'; // ์Šคํ‹ฐ์ปค ํŒ๋ณ„๊ธฐ
 
@Injectable()
export class JwtAuthGuard extends AuthGuard('supabase-jwt') {
  constructor(private reflector: Reflector) { super(); }
 
  canActivate(context: ExecutionContext) {
    // 1. ๋ฐฉ๊ธˆ ์š”์ฒญ์ด ๋“ค์–ด์˜จ ๋ผ์šฐํ„ฐ์— 'isPublic' ์Šคํ‹ฐ์ปค๊ฐ€ ๋ถ™์–ด์žˆ๋Š”์ง€ ํ™•์ธ!
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    
    // 2. ์Šคํ‹ฐ์ปค๊ฐ€ ์žˆ์œผ๋ฉด ์‹ ๋ถ„์ฆ ๊ฒ€์‚ฌ(JWT ๊ฒ€์‚ฌ) ์Šคํ‚ต ํ›„ ํ†ต๊ณผ!
    if (isPublic) return true;
    
    // 3. ์Šคํ‹ฐ์ปค๊ฐ€ ์—†์œผ๋ฉด ์–„์งค์—†์ด ์›๋ž˜ ํ•˜๋˜ ์‹ ๋ถ„์ฆ ๊ฒ€์‚ฌ ์ง„ํ–‰
    return super.canActivate(context);
  }
}

์ด์ œ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ด๋ ‡๊ฒŒ ์“ธ ์ˆ˜ ์žˆ์–ด.

@Get('health')
@Public() // ๐Ÿ‘ˆ ๋กœ๊ทธ์ธ ์•ˆํ•ด๋„ ์ ‘์† ๊ฐ€๋Šฅ!
checkHealth() { return 'OK'; }

2. @CurrentUser(): ์œ ์ € ์ •๋ณด ํŽธํ•˜๊ฒŒ ๋นผ์˜ค๊ธฐ

์›๋ž˜ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์œ ์ € ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋ ค๋ฉด @Req() req๋ฅผ ํ†ต์งธ๋กœ ๊ฐ€์ ธ์™€์„œ req.user.id๋กœ ์ ‘๊ทผํ•ด์•ผ ํ•ด. ๋„ˆ๋ฌด ๊ท€์ฐฎ๊ณ  ์ง€์ €๋ถ„ํ•˜์ง€.
์š”์ฒญ ๊ฐ์ฒด ๊นŠ์ˆ™์ด ์ˆจ์–ด์žˆ๋Š” user ์ •๋ณด๋งŒ ์™์™ ๋ฝ‘์•„์ฃผ๋Š” ๊ธฐ๊ณ„๋ฅผ ๋งŒ๋“ค์ž.

// decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
 
export const CurrentUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    // ์‹ ๋ถ„์ฆ ๊ฒ€์‚ฌ๋ฅผ ํ†ต๊ณผํ•˜๋ฉฐ ์ €์žฅ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ๋Œ๋ ค์คŒ
    return request.user; 
  },
);
 
// ------------ ์‚ฌ์šฉ ์”ฌ ------------
@Get('my-profile')
// ๐Ÿ‘ˆ ๊น”ใ…ก๋”
getProfile(@CurrentUser() user: { id: string; email: string }) { 
  return this.usersService.findOne(user.id);
}

๐Ÿงช ๋”ฐ๋ผํ•ด๋ณด๊ธฐ: ์‹ค์ „ ์—ญํ•  ๊ธฐ๋ฐ˜ ์ธ๊ฐ€(RBAC) ๊ตฌํ˜„

๋งˆ์ง€๋ง‰ ํ•˜์ด๋ผ์ดํŠธ. ์‹ ๋ถ„์ฆ ๊ฒ€์‚ฌ(AuthN)๋Š” ํ†ต๊ณผํ–ˆ๋Š”๋ฐ, ๊ทธ ์ง์›์ด ์ผ๋ฐ˜ ์‚ฌ์›์ธ์ง€ ๊ด€๋ฆฌ์ž(Admin)์ธ์ง€ ๊ฒ€์‚ฌํ•ด์„œ ์ธ๊ฐ€(AuthZ) ๋ฅผ ๋•Œ๋ ค๋ณด์ž.
์ด๊ฒƒ๋„ @Roles('admin') ์Šคํ‹ฐ์ปค๋ฅผ ๋งŒ๋“ค๊ณ , ๊ทธ๊ฑธ ๊ฐ์‹œํ•˜๋Š” 2๋ฒˆ์งธ ๊ฒฝ๋น„์›์„ ์„ธ์šฐ๋ฉด ๋๋‚˜!

1. ๊ถŒํ•œ ์Šคํ‹ฐ์ปค ๋งŒ๋“ค๊ธฐ

export const ROLES_KEY = 'roles';
// ๊ด„ํ˜ธ ์•ˆ์— ์ ์€ ๋ฌธ์ž์—ด๋“ค('admin', 'user')์„ ํ†ต์งธ๋กœ ๋ฐฐ์—ด๋กœ ์ €์žฅ!
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

2. ๊ถŒํ•œ ๊ฒฝ๋น„์›(RolesGuard) ์„ธ์šฐ๊ธฐ

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
 
  canActivate(context: ExecutionContext): boolean {
    // 1. ์ด ์ปจํŠธ๋กค๋Ÿฌ์— ๋“ค์–ด์˜ค๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ๊ถŒํ•œ ์Šคํ‹ฐ์ปค ๋ชฉ๋ก์„ ๊ฐ€์ ธ์™€
    const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
        context.getHandler(),
        context.getClass(),
    ]);
    
    // 2. ์•„๋ฌด ๊ถŒํ•œ ์Šคํ‹ฐ์ปค ์•ˆ ๋ถ™์–ด์žˆ์œผ๋ฉด ํ†ต๊ณผ (Public ๊ฐ™์€ ๊ณณ)
    if (!requiredRoles) return true;
    
    // 3. ์œ ์ €์˜ ์‹ ๋ถ„์ฆ์—์„œ ์ง๊ธ‰(role)์„ ๊บผ๋‚ด
    const { user } = context.switchToHttp().getRequest();
    
    // 4. ์œ ์ € ๊ถŒํ•œ์ด ํ•„์š”ํ•œ ๊ถŒํ•œ ๋ฐฐ์—ด์— ์†ํ•ด์žˆ๋Š”์ง€ ๊ฒ€์‚ฌ!
    return requiredRoles.includes(user.role);
  }
}

3. ๊ธ€๋กœ๋ฒŒ ๊ฒฝ๋น„์› + ์ปจํŠธ๋กค๋Ÿฌ ์ ์šฉ

// app.module.ts (๊ฒฝ๋น„์›์€ 1๋ฒˆ ๊ฐ€๋“œ๋ถ€ํ„ฐ ์ˆœ์ฐจ์ ์œผ๋กœ ๊ฒ€์‚ฌํ•ด)
providers: [
  { provide: APP_GUARD, useClass: JwtAuthGuard }, // 1. ๋„ˆ ๋กœ๊ทธ์ธ ํ–ˆ์–ด?
  { provide: APP_GUARD, useClass: RolesGuard },   // 2. ๋„ˆ ๊ด€๋ฆฌ์ž ์ง๊ธ‰ ๋งž์•„?
]
 
// ์ปจํŠธ๋กค๋Ÿฌ
@Delete(':id')
@Roles('admin') // ๐Ÿ‘ˆ ๊ด€๋ฆฌ์ž๋งŒ ์‚ญ์ œ ๊ฐ€๋Šฅ
remove(@Param('id') id: string) { ... }

๐Ÿ’ผ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค์™€ ์‹ค๋ฌด ํŒ ๐ŸŸก

ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์˜ Request ๊ฐ์ฒด ํƒ€์ž… ํ™•์žฅํ•˜๊ธฐ

๋งค๋ฒˆ req.user๋ฅผ ์“ธ ๋•Œ๋งˆ๋‹ค TypeScript๊ฐ€ "Request ์•ˆ์— user๊ฐ€ ์–ด๋”จ์–ด ํƒ€์ž… ์—๋Ÿฌ์•ผ!" ๋ผ๊ณ  ํ™”๋‚ผ ๊ฑฐ์•ผ. ์–ด์ฉ” ์ˆ˜ ์—†์ด (req as any).user ๋กœ ํ‰์น˜์ง€ ๋ง๊ณ , ๊ทผ๋ณธ์ ์ธ ํƒ€์ž…์„ ๋„“ํ˜€์ค˜.

// src/types/express.d.ts (ํŒŒ์ผ์„ ์ƒˆ๋กœ ํ•˜๋‚˜ ๋งŒ๋“ค์–ด)
declare namespace Express {
  export interface Request {
    user?: User; // ๋„ค๊ฐ€ ํ”„๋กœ์ ํŠธ์—์„œ ์“ฐ๋Š” User ํƒ€์ž…์„ ๊ฝ‚์•„๋„ฃ์œผ๋ฉด ๋ผ!
  }
}

์„ ํƒ์  ์ธ์ฆ ๊ฐ€๋“œ (Optional Auth)

"๋น„ํšŒ์›์ด๋ฉด ์ผ๋ฐ˜ ๊ธ€๋ชฉ๋ก ๋ณด์ด๊ณ , ๋กœ๊ทธ์ธํ•œ ํšŒ์›์ด๋ฉด ์ถ”์ฒœ ๊ธ€๋ชฉ๋ก๋„ ์„ž์—ฌ์„œ ๋ณด์ด๋Š”" ๊ฒŒ์‹œํŒ API๋ฅผ ๋งŒ๋“ค ๋•Œ ์“ฐ๋Š” ๊ผผ์ˆ˜ ๊ฐ€๋“œ.
๋กœ๊ทธ์ธ์„ ์•ˆ ํ–ˆ์–ด๋„ 401 ์—๋Ÿฌ๋ฅผ ํŠ•๊ธฐ์ง€ ์•Š๊ณ  null์„ ๋„˜๊ฒจ์ฃผ๊ฒŒ ์กฐ์ž‘ํ•ด!

@Injectable()
export class OptionalJwtAuthGuard extends AuthGuard('supabase-jwt') {
  handleRequest(err: any, user: any) {
    // ์—๋Ÿฌ๋ฅผ ๋‚ด๋ฟœ์ง€ ์•Š๊ณ  ๊ทธ๋ƒฅ null ๋ฆฌํ„ด
    return user || null; 
  }
}

๐Ÿ’ฅ ์—๋Ÿฌ ํ•ด๊ฒฐ ์นดํƒˆ๋กœ๊ทธ

์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ๋œจ๋ฉด Ctrl+F๋กœ ๊ฒ€์ƒ‰ํ•ด๋ด.

โŒ 401 Unauthorized - JwtStrategy validation failed

ํ˜„์ƒ:

{ "statusCode": 401, "message": "Unauthorized" }

์›์ธ:
ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํ—ค๋”์— Authorization: Bearer <token> ์„ ์ œ๋Œ€๋กœ ์•ˆ ๋ณด๋ƒˆ๊ฑฐ๋‚˜, ํ† ํฐ ๊ธฐํ•œ์ด ๋งŒ๋ฃŒ๋˜์—ˆ๊ฑฐ๋‚˜, ํ† ํฐ์˜ ์„œ๋ช…(Signature)์ด ๋ณ€์กฐ๋˜์—ˆ์„ ๋•Œ!
ํ•ด๊ฒฐ์ฑ…:
ํด๋ผ์ด์–ธํŠธ ํ”„๋ก ํŠธ ๊ฐœ๋ฐœ์ž์—๊ฒŒ "์Šคํ† ๋ฆฌ์ง€์—์„œ ํ† ํฐ ๊บผ๋‚ด์„œ ํ—ค๋”์— ์ž˜ ๋„ฃ์–ด๋‹ฌ๋ผ"๊ณ  ์š”์ฒญํ•ด. ํ˜น์‹œ ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ๋‹ค๋ฉด Refresh Token์„ ๋Œ๋ฆฌ๋Š” ๋กœ์ง์ด ํ”„๋ก ํŠธ์— ๊ตฌ๋น„๋˜์–ด์•ผ ํ•ด.

โŒ 403 Forbidden ๋ฐœ์ƒ

ํ˜„์ƒ:
๋กœ๊ทธ์ธ์€ ์ž˜ ๋˜์–ด ๋‚ด ์ •๋ณด ์กฐํšŒ๋Š” ๋˜๋Š”๋ฐ, ์‚ญ์ œ API๋งŒ ๋ˆ„๋ฅด๋ฉด 403์ด ๋œธ.
์›์ธ:
๊ฒฝ๋น„์› 1๋ฒˆ(JwtAuthGuard)์€ ํ†ต๊ณผํ–ˆ์œผ๋‚˜, 2๋ฒˆ(RolesGuard)์—์„œ ๋ง‰ํž˜. ๋‚ด ํ† ํฐ์— ์ฐํ˜€์žˆ๋Š” role ํ”„๋กœํผํ‹ฐ๊ฐ€ "user" ์ธ๋ฐ ๋ฐฑ์—”๋“œ๋Š” @Roles('admin')์„ ์š”๊ตฌํ•˜๊ณ  ์žˆ์Œ.
ํ•ด๊ฒฐ์ฑ…:
์ •์ƒ์ ์ธ ๋ฐฉ์–ด ์‹คํŒจ ํ™”๋ฉด. ๊ด€๋ฆฌ์ž ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธํ•ด์•ผ ํ•จ.


๐Ÿ—‚๏ธ ์น˜ํŠธ์‹œํŠธ โ€” ์‹ค๋ฌด ์š”์•ฝ ์นด๋“œ

์ƒํ™ฉ์‚ฌ์šฉ ํŒจํ„ด์„ค๋ช…
ํ† ํฐ์—์„œ ์ •๋ณด ๋นผ๋‚ด๊ธฐjwtFromRequest: ExtractJwt...Strategy ๋‚ด๋ถ€ ๋ฌธ๋ฒ•
๋กœ๊ทธ์ธ ํ•„์ˆ˜ (๊ฐœ๋ณ„)@UseGuards(JwtAuthGuard)์ปจํŠธ๋กค๋Ÿฌ/๋ฉ”์„œ๋“œ์— ๋ถ€์ฐฉ
๋กœ๊ทธ์ธ ํŒจ์Šค (์˜ˆ์™ธ)@Public()์ „์—ญ ๊ฐ€๋“œ ๋šซ๊ธฐ ์Šคํ‹ฐ์ปค
์œ ์ € ์ •๋ณด ๋ฝ‘๊ธฐ@CurrentUser() user์ปค์Šคํ…€ ํŒŒ๋ผ๋ฏธํ„ฐ ํ…Œ์ฝ”๋ ˆ์ดํ„ฐ
๊ด€๋ฆฌ์ž ์ „์šฉ ์„ค์ •@Roles('admin', 'super')์ธ๊ฐ€(AuthZ) ์Šคํ‹ฐ์ปค

๐Ÿ“ ๋งˆ๋ฌด๋ฆฌ ํ€ด์ฆˆ

Q1. ๋‹น์‹ ์ด ๊ฐœ๋ฐœ ์ค‘์ธ deleteAccount API๋ฅผ ํ˜ธ์ถœํ•œ ์œ ์ €๊ฐ€ ์ธ์ฆ ํ† ํฐ(์‚ฌ์›์ฆ) ์—†์ด ๋ฌด๋‹จ ์นจ์ž…์„ ์‹œ๋„ํ–ˆ๋‹ค. ์ด๋•Œ NestJS์˜ ๋ฐฑ์—”๋“œ ๊ฒฝ๋น„์›(JwtAuthGuard)์ด ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•˜๋Š” HTTP ์ƒํƒœ ์ฝ”๋“œ์™€ ๋‹จ์–ด๋Š” ๋ฌด์—‡์ธ๊ฐ€?

  • A) 400 Bad Request
  • B) 401 Unauthorized
  • C) 403 Forbidden
  • D) 404 Not Found

โœ… ์ •๋‹ต: B

๐Ÿ’ก ์„ค๋ช…: ์‹ ๋ถ„์ฆ ์ž์ฒด๊ฐ€ ์—†๊ฑฐ๋‚˜ ์œ„์กฐ๋œ ์ƒํƒœ. ์ฆ‰ "๋‹น์‹  ๋ˆ„๊ตฐ์ง€ ๋ชฐ๋ผ(Un-authenticated)!" ์ƒํ™ฉ์ด๋ฏ€๋กœ 401์ด์•ผ. 403์€ ์‹ ๋ถ„์ฆ์€ ์ง„์งœ์—ฌ์„œ ๋“ค๋ ค๋Š” ๋ณด๋ƒˆ๋Š”๋ฐ, ๊ทธ ๋ฐฉ์— ๋“ค์–ด๊ฐˆ ๊ถŒํ•œ ์ง๊ธ‰์ด ์•„๋‹ ๋•Œ(์ธ๊ฐ€ ์‹คํŒจ) ๋„์šฐ๋Š” ๊ฑฐ ๊ธฐ์–ตํ•˜์ž!

Q2. UseGuards(JwtAuthGuard)๋ฅผ ๋ชจ๋“  ์ปจํŠธ๋กค๋Ÿฌ ์ƒ๋‹จ์— ์ผ์ผ์ด ๋ณต์‚ฌ+๋ถ™์—ฌ๋„ฃ๊ธฐ ํ•˜์ง€ ์•Š๊ณ , ํ•œ ๋ฒˆ์˜ ์„ค์ •๋งŒ์œผ๋กœ ํ”„๋กœ์ ํŠธ ๋‚ด์˜ ๋ชจ๋“  API๋ฅผ ์ž ๊ฐ€๋ฒ„๋ฆด ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์„ ์„ค๋ช…ํ•ด๋ด.

โœ… ์ •๋‹ต: ๋ฃจํŠธ ๋ชจ๋“ˆ์ธ app.module.ts์˜ providers ๋ฐฐ์—ด ์•ˆ์— { provide: APP_GUARD, useClass: JwtAuthGuard } ๋ฅผ ๋“ฑ๋กํ•˜๋ฉด ์ „์—ญ์œผ๋กœ ํ™œ์„ฑํ™”๋ผ!

Q3. ๋””๋ฒ„๊น… ํ€ด์ฆˆ: ์•ผ์‹ฌ ์ฐจ๊ฒŒ @CurrentUser() ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ๋งŒ๋“ค๊ณ  ์•„๋ž˜์ฒ˜๋Ÿผ ์ปจํŠธ๋กค๋Ÿฌ์— ๋‹ฌ์•˜๋‹ค. ๊ทธ๋Ÿฐ๋ฐ user ๋ณ€์ˆ˜๊ฐ€ ์ž๊พธ undefined๋กœ ๋–จ์–ด์ง„๋‹ค. ์ด์œ ๊ฐ€ ๋ญ˜๊นŒ?

@Get('my-data')
// (์•—, ๋ญ”๊ฐ€ ๋น ์กŒ๋‹ค..!)
getMyData(@CurrentUser() user: any) {
  return this.service.getData(user.id);
}

โœ… ์ •๋‹ต: ์ „์—ญ ๊ฐ€๋“œ๋ฅผ ๊น”์ง€ ์•Š์€ ์ƒํƒœ๋ผ๋ฉด, ์ด ๋ผ์šฐํ„ฐ์— @UseGuards(JwtAuthGuard)๋ฅผ ์•ˆ ๋‹ฌ์•„์ค˜์„œ ๊ทธ๋ž˜!
์„ค๋ช…: ๋ฐฑ์—”๋“œ์— ์‹ ๋ถ„์ฆ(ํ† ํฐ) ์กฐ์ฐจ ๋“ค์ด๋ฐ€ ์—ฌ์ง€๋ฅผ ์•ˆ ์ค€ ์ฑ„ ๊ทธ๋ƒฅ ์ปจํŠธ๋กค๋Ÿฌ๋กœ ํ”„๋ฆฌํŒจ์Šค ์ž…์žฅํ•ด๋ฒ„๋ ธ์–ด. ์• ์ดˆ์— ์‹ ๋ถ„์ฆ ๊ฒ€์‚ฌ๋ฅผ ํ•˜๋Š” ๊ฒฝ๋น„์›(JwtStrategy)์ด ๋™์ž‘ํ•ด์•ผ req.user ๊ฐ์ฒด๊ฐ€ ์ƒ๊ฒจ๋‚˜๋Š”๋ฐ, ๊ฒฝ๋น„์›์ด ์—†์œผ๋‹ˆ ๋‹น์—ฐํžˆ ๋ฝ‘์•„์˜ฌ user๋„ undefined ์ƒํƒœ์ธ ๊ฑฐ์ง€. ๋กœ๊ทธ์ธ ์ „์ œ ๋ผ์šฐํ„ฐ์—๋Š” ๋ฐ˜๋“œ์‹œ ๊ฐ€๋“œ๋ฅผ ๊น”์ž.


๐Ÿ”— ๋” ์•Œ์•„๋ณด๊ธฐ