๐ 06. ์ธ์ฆ ์ฒ๋ฆฌ โ ๋น์ ์ ๋๊ตฌ๊ณ , ์ด๋๊น์ง ๋ค์ด๊ฐ ์ ์๋๊ฐ?
๐ ๊ฐ์
๋๊ฐ ์์ฒญ์ ๋ณด๋๋์ง(์ธ์ฆ)์ ๊ทธ ์์ฒญ์ ํ๋ฝํ ๊ฒ์ธ์ง(์ธ๊ฐ)๋ฅผ ์๋ฒฝํ๊ฒ ํต์ ํ๋ Guard, Strategy, ๊ทธ๋ฆฌ๊ณ ์ปค์คํ ๋ฐ์ฝ๋ ์ดํฐ ํ์ฉ๋ฒ์ ๋ค๋ฃน๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐งฉ ์ธ์ฆ๊ณผ ์ธ๊ฐ โ ์ ๋ ํท๊ฐ๋ฆฌ๋ฉด ์ ๋๋ ๋ ๋จ์ด ๐ก
- ๐งฉ JWT Strategy์ Guard โ ์ ๋ค์ง ์๋ ๊ฒฝ๋น์ ๐ข
- ๐งฉ ์ปค์คํ ๋ฐ์ฝ๋ ์ดํฐ โ ๊ฒฝ๋น์ ์กฐ์ข ํ๊ธฐ ๐ข
- ๐งช ๋ฐ๋ผํด๋ณด๊ธฐ: ์ค์ ์ญํ ๊ธฐ๋ฐ ์ธ๊ฐ(RBAC) ๊ตฌํ
- ๐ผ ๋ฒ ์คํธ ํ๋ํฐ์ค์ ์ค๋ฌด ํ
- ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐๏ธ ์นํธ์ํธ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 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 Unauthorized | 403 Forbidden |
| ๊ตฌํ ์์ | passport-jwt, JwtStrategy | RolesGuard, @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 ์ํ์ธ ๊ฑฐ์ง. ๋ก๊ทธ์ธ ์ ์ ๋ผ์ฐํฐ์๋ ๋ฐ๋์ ๊ฐ๋๋ฅผ ๊น์.