์น ์๋น์ค๋ฅผ ๊ฐ๋ฐํ ๋ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ด๋ฉด์๋ ์ค์ํ ์์๋ค. ๋จ์ํ ์์ด๋์ ๋น๋ฐ๋ฒํธ๋ฅผ ํ์ธํ๋ ๊ฒ์ ๋์ด ํน์ ์ฌ์ฉ์๊ฐ ์ฐ๋ฆฌ ์๋น์ค์ ์ ์ ๊ฐ ๋ง๋์ง ๊ฒ์ฆํ๊ณ ํด๋น ์์ฒญ์ ๋ํ ์ ๊ทผ ๊ถํ์ด ์๋์ง๊น์ง ํ๋จํด์ผ ํ๊ธฐ ๋๋ฌธ์ด๋ค.
๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ์ค๊ณํ๊ธฐ ์ ์ ์ ํ๋์ด์ผ ํ ์ธ์ฆ(Authentication)๊ณผ ์ธ๊ฐ(Authorization)์ ๊ฐ๋
์ ๋จผ์ ์ ๋ฆฌํด ๋ณด์.
์ธ์ฆ(Authentication)์ ์ฌ์ฉ์์ ์ ์์ ํ์ธํ๋ ๊ณผ์ ์ด๋ค. ๊ณตํญ์์ ์ฌ๊ถ์ ์ ์ํด ๋ณธ์ธ์ด ๋๊ตฌ์ธ์ง ์ฆ๋ช
ํ๋ ๊ฒ์ ๋น์ ํ ์ ์๋ค. ์ฌ์ฉ์๊ฐ ์
๋ ฅํ ID์ ๋น๋ฐ๋ฒํธ๊ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ๋ ์ ๋ณด์ ์ผ์นํ๋์ง๋ฅผ ๊ฒ์ฆํ๋ ๋จ๊ณ๊ฐ ์ด์ ํด๋นํ๋ค.
์ธ๊ฐ(Authorization)๋ ์ธ์ฆ์ ํต๊ณผํ ์ฌ์ฉ์๊ฐ ์ด๋ค ๊ถํ์ ๊ฐ์ง๋์ง ํ์ธํ๋ ๊ณผ์ ์ด๋ค. ์ฌ๊ถ ํ์ธ ํ ๋ฐ๊ธ๋ฐ์ ํฐ์ผ์ ๋ฐ๋ผ ํผ์คํธ ํด๋์ค ๋ผ์ด์ง์ ์
์ฅํ ์ ์๋์ง ์ผ๋ฐ ๊ฒ์ดํธ๋ก ์ด๋ํด์ผ ํ๋์ง๋ฅผ ํ๋จํ๋ ๊ฒ๊ณผ ๊ฐ๋ค. ์๋ฅผ ๋ค์ด ์ผ๋ฐ ์ฌ์ฉ์๊ฐ ๊ฒ์๋ฌผ์ ์ญ์ ํ๋ ค ํ ๊ฒฝ์ฐ ์๋ฒ๋ ํด๋น ์์ฒญ์ ๋ํ ์ญ์ ๊ถํ์ด ์๋์ง๋ฅผ ๊ฒ์ฆํ ๋ค ํ์ฉ ๋๋ ๊ฑฐ๋ถํ๋ค.
์ธ์ฆ๊ณผ ์ธ๊ฐ๋ ๋ช
ํํ ๊ตฌ๋ถ๋๋ ๊ฐ๋
์ด๋ฉฐ ์ธ์ฆ์ด ์ ํ๋์ด์ผ๋ง ์ธ๊ฐ๊ฐ ๊ฐ๋ฅํ๋ค. ์ฌ์ฉ์์ ์ ์์ด ํ์ธ๋์ง ์์ ์ํ์์๋ ์ ์ ํ ๊ถํ์ ๋ถ์ฌํ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.
์ธ์ฆ๊ณผ ์ธ๊ฐ๋ฅผ ๊ตฌํํ ๋ ํํ ๊ณ ๋ฏผํ๊ฒ ๋๋ ์ ํ์ง๋ ์ธ์
(Session)๊ณผ JWT(Json Web Token) ๋ฐฉ์์ด๋ค. ๋ ๋ฐฉ์์ ๊ฐ์ฅ ํฐ ์ฐจ์ด๋ ์ฌ์ฉ์์ ๋ก๊ทธ์ธ ์ํ(์ํ ์ ๋ณด)๋ฅผ ์ด๋์ ์ ์ฅํ๋๋์ ์๋ค.
์ธ์
(Session) ๋ฐฉ์
์๋ฒ๊ฐ ์ฌ์ฉ์ ์ํ ์ ๋ณด๋ฅผ ์ง์ ์ ์ฅํ๊ณ ๊ด๋ฆฌํ๋ ๊ตฌ์กฐ์ด๋ค. ์ผ๋ฐ์ ์ผ๋ก ๋ฉ๋ชจ๋ฆฌ ๋๋ Redis์ ๊ฐ์ ์ ์ฅ์์ ์ธ์
์ ๋ณด๊ดํ๋ค.
JWT(Token) ๋ฐฉ์
์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์๋ช
๋ ํ ํฐ์ ๋ด์ ํด๋ผ์ด์ธํธ์ ์ ๋ฌํ๊ณ ์๋ฒ๋ ์ด๋ฅผ ์ ์ฅํ์ง ์๋ ๊ตฌ์กฐ์ด๋ค. ์ฆ ์๋ฒ๊ฐ ์ํ๋ฅผ ์ ์ฅํ์ง ์๋ Stateless ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๋ค.
์ด๋ค ๋ฐฉ์์ด ์ ๋์ ์ผ๋ก ํญ์ ์ข๋ค๊ณ ๋ณด๊ธฐ๋ ์ด๋ ต๋ค. ์๋น์ค์ ๊ท๋ชจ, ํ์ฅ ๊ณํ, ์ธํ๋ผ ๊ตฌ์กฐ์ ๋ฐ๋ผ ์ ์ ํ ๋ฐฉ์์ ์ ํํด์ผ ํ๋ค. ์ด๋ฒ ๋ธ๋ก๊ทธ CMS ํ๋ก์ ํธ์์๋ ํ์ฅ์ฑ๊ณผ ๋ฌด์ํ ๊ตฌ์กฐ๋ฅผ ๊ฒฝํํด ๋ณด๊ธฐ ์ํด JWT ๋ฐฉ์์ ์ฑํํ์๋ค.
JWT(Json Web Token)๋ ์ (.)์ผ๋ก ๊ตฌ๋ถ๋ ์ธ ๋ถ๋ถ์ผ๋ก ๊ตฌ์ฑ๋๋ค.
Header.Payload.Signature
๊ฐ ๋ถ๋ถ์ด ์ด๋ค ์ญํ ์ ํ๋์ง ์ดํด๋ณด์.
Header (ํค๋)
ํ ํฐ์ ๋ํ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ๋ด๊ณ ์๋ ์์ญ์ด๋ค. ์ฃผ๋ก ์๋ช
์ ์ฌ์ฉ๋ ์๊ณ ๋ฆฌ์ฆ๊ณผ ํ ํฐ ํ์
์ ๋ณด๊ฐ ํฌํจ๋๋ค.
{
"alg": "HS256", // ์๋ช ์ ์์ฑํ ๋ ์ฌ์ฉํ ์ํธํ ์๊ณ ๋ฆฌ์ฆ
"typ": "JWT" // ํ ํฐ์ ํ์
}
Payload (ํ์ด๋ก๋)
ํ ํฐ์ ํต์ฌ ์์ญ์ผ๋ก ์ฌ์ฉ์์ ๋ํ ์ ๋ณด(Claims)๋ฅผ ๋ด๋๋ค. ์ฌ๊ธฐ์ ํฌํจ๋๋ ๊ฐ๊ฐ์ ์ ๋ณด ๋จ์๋ฅผ ํด๋ ์(Claim)์ด๋ผ๊ณ ๋ถ๋ฅธ๋ค.
[Claim์ ์ข
๋ฅ]
Registered Claims
JWT ํ์ค์์ ๋ฏธ๋ฆฌ ์ ์๋ ํค (iss, sub, exp, iat ๋ฑ)
Public/Private Claims
๊ฐ๋ฐ์๊ฐ ์์ ๋กญ๊ฒ ์ ์ํ๋ ๋ฐ์ดํฐ (userId, role ๋ฑ)
{
"sub": "1234567890",
"name": "BlueCool",
"admin": true,
"iat": 1516239022
}
์ค์ํ ์ ์ Payload๋ ์ํธํ๋ ๊ฒ์ด ์๋๋ผ Base64Url๋ก ์ธ์ฝ๋ฉ๋ ๊ฒ์ด๋ผ๋ ์ ์ด๋ค. ์ฆ ๋๊ตฌ๋ ์ฝ๊ฒ ๋์ฝ๋ฉ ํ์ฌ ๋ด์ฉ์ ํ์ธํ ์ ์๋ค. ๋ฐ๋ผ์ ๋น๋ฐ๋ฒํธ, ์ฃผ๋ฏผ๋ฑ๋ก๋ฒํธ, ์ด๋ฉ์ผ๊ณผ ๊ฐ์ ๋ฏผ๊ฐ ์ ๋ณด๋ ์ ๋ ํฌํจํด์๋ ์ ๋๋ค.
Signature (์๋ช
)
JWT์ ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฅํ๋ ๊ฐ์ฅ ์ค์ํ ๋ถ๋ถ์ด๋ค. Header์ Payload๋ฅผ ๊ฒฐํฉํ ๋ค ์๋ฒ๋ง ์๊ณ ์๋ ๋น๋ฐ ํค๋ฅผ ์ด์ฉํด ์๋ช
์ ์์ฑํ๋ค.
์๋ช
์์ฑ ๊ณผ์ ์ ๋ค์๊ณผ ๊ฐ๋ค.
HMACSHA256(
base64UrlEncode(Header) + "." +
base64UrlEncode(Payload),
secretKey
)โ
์ด ์๋ช
๋๋ถ์ ์๋ฒ๋ ๋ณ๋์ DB ์กฐํ ์์ด๋ ํ ํฐ์ ์ ํจ์ฑ์ ๊ฒ์ฆํ ์ ์๋ค. ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ๋ฐ์ ํ ํฐ์ Header์ Payload๋ฅผ ๊ฐ์ ธ์ ์์ ์ Secret Key๋ก ๋ค์ ์๋ช
์ ๊ณ์ฐํด ๋ณด๊ณ ๊ณ์ฐ๋ ์๋ช
์ด ํ ํฐ์ ๋ถ์ด์จ Signature์ ์ผ์นํ๋์ง ํ์ธํ๋ค.
๋ง์ฝ ์ค๊ฐ์ ํด์ปค๊ฐ Payload์ role์ USER์์ ADMIN์ผ๋ก ๋ฐ๊ฟจ๋ค๋ฉด ์๋ฒ์์ ๋ค์ ๊ณ์ฐํ ์๋ช
๊ณผ ์ผ์นํ์ง ์๊ฒ ๋์ด ์๋ฒ๋ ์ด ํ ํฐ์ด ๋ณ๊ฒฝ๋์๋ค๊ณ ํ๋จํ์ฌ ์์ฒญ์ ๊ฑฐ์ ํ๋ค.
1. ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ๊ตฌํ
๋ก๊ทธ์ธ API๋ ์ฌ์ฉ์์ ID์ ๋น๋ฐ๋ฒํธ๋ฅผ ๊ฒ์ฆํ๊ณ JWT Access Token๊ณผ Refresh Token์ ๋ฐ๊ธํ๋ ์ญํ ์ ํ๋ค.
์ ์ญ์ผ๋ก JwtAuthGuard๋ฅผ ์ ์ฉํ๊ธฐ ๋๋ฌธ์ ๋ก๊ทธ์ธ API๋ ์์ธ์ ์ผ๋ก ์ธ์ฆ์ด ํ์ ์๋ ๊ณต๊ฐ ์๋ํฌ์ธํธ๋ก ์ฒ๋ฆฌํด์ผ ํ๋ค. ์ด๋ฅผ ์ํด ์ปค์คํ
@Public() ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ฌ์ฉํ์๋ค.
@Public() // JWT Guard ์์ธ ์ฒ๋ฆฌ
@Post('login')
async login(
@Body() req: LoginRequest,
@Res({ passthrough: true }) res: Response,
): Promise<LoginResponse> {
// ID/PW ๊ฒ์ฆ ๋ฐ ํ ํฐ ๋ฐ๊ธ
const result = await this.authService.login(req.loginId, req.password);
// Refresh Token์ HttpOnly ์ฟ ํค์ ์ ์ฅ
this.setRefreshTokenCookie(res, result.refreshToken);
// Access Token๊ณผ ์ฌ์ฉ์ ์ ๋ณด ๋ฐํ
return LoginResponse.fromResult(result);
}
private setRefreshTokenCookie(res: Response, token: string) {
res.cookie('rt', token, {
httpOnly: true, // ์๋ฐ์คํฌ๋ฆฝํธ์์ ์ฟ ํค ์ ๊ทผ ๋ถ๊ฐ (XSS ๊ณต๊ฒฉ ๋ฐฉ์ง)
secure: true, // HTTPS ํ๊ฒฝ์์๋ง ์ ์ก (๋คํธ์ํฌ ํ์ทจ ๋ฐฉ์ง)
sameSite: 'none', // ํฌ๋ก์ค ์ฌ์ดํธ ์์ฒญ ์์๋ ์ฟ ํค ์ ์ก (CORS ๋์)
path: '/',
signed: true, // ์ฟ ํค ๋ณ์กฐ ๋ฐฉ์ง์ฉ ์๋ช ์ถ๊ฐ
maxAge: 7 * 24 * 60 * 60 * 1000, // 7์ผ ๋์ ์ ํจ
})
}
Access Token์ ์งง์ ๋ง๋ฃ ์๊ฐ์ ๊ฐ์ง๋ฉฐ API ์์ฒญ ์ Authorization ํค๋์ ํฌํจ๋์ด ์ฌ์ฉ๋๋ค.
Refresh Token์ ๊ฒฝ์ฐ ์ฅ๊ธฐ ๋ณด๊ด์ด ํ์ํ๊ณ ํ์ทจ ์ํ์ด ๋ ํฌ๋ฏ๋ก JS ์ ๊ทผ์ ์ฐจ๋จํ๊ธฐ ์ํด httpOnly + secure ์ต์
์ผ๋ก ๋ณด์์ ๊ฐํํ์๋ค.
2. Service ๊ณ์ธต ๋ก๊ทธ์ธ ์ฒ๋ฆฌ ๋ก์ง
async login(loginId: string, password: string) {
// ์ ์ ์กด์ฌ ํ์ธ
const credential = await this.userRepository.findByLoginId(loginId);
if (!credential) throw new UnauthorizedException('์์ด๋ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์ต๋๋ค.');
// ๋น๋ฐ๋ฒํธ ๊ฒ์ฆ
await credential.authenticate(password, async (raw, hashed) => bcrypt.compare(raw, hashed));
// ๋ก๊ทธ์ธ ์ํ ๊ธฐ๋ก
const snapshot = credential.getSnapshot();
await this.userRepository.saveAuthStatus(snapshot);
// Access / Refresh Token ๋ฐ๊ธ
const [accessToken, refreshToken] = await Promise.all([
this.signAccessToken({ id: snapshot.id, role: snapshot.role }),
this.signRefreshToken({ id: snapshot.id }),
]);
return {
accessToken,
refreshToken,
user: {
id: snapshot.id,
loginId: snapshot.loginId,
role: snapshot.role,
},
};
}
// Access Token ๋ฐ๊ธ
private async signAccessToken(user: { id: string; role: UserRole }) {
const secret = this.configService.getOrThrow<string>('JWT_ACCESS_SECRET');
const expiresIn = this.configService.get<string>('JWT_ACCESS_TTL') ?? '15m';
return this.jwtService.signAsync(
{ sub: String(user.id), role: user.role },
{ secret, expiresIn: expiresIn as JwtSignOptions['expiresIn'] },
);
}
// Refresh Token ๋ฐ๊ธ
private async signRefreshToken(user: { id: string }) {
const secret = this.configService.getOrThrow<string>('JWT_REFRESH_SECRET');
const expiresIn = this.configService.get<string>('JWT_REFRESH_TTL') ?? '7d';
return this.jwtService.signAsync(
{ sub: String(user.id) },
{ secret, expiresIn: expiresIn as JwtSignOptions['expiresIn'] },
);
}
์๋น์ค์์๋ ID์ PW๋ฅผ ๊ฒ์ฆํ๋ค. ์ ์ ๊ฐ ์กด์ฌํ์ง ์๋ ๊ฒฝ์ฐ์ ๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์๋ ๊ฒฝ์ฐ ๋ ๋ค ๊ฐ์ ์๋ฌ ๋ฉ์์ง๋ก ์ฒ๋ฆฌํ์ฌ ๊ณ์ ์ ๋ณด๊ฐ ๋
ธ์ถ๋์ง ์๋๋ก ๋ฐฉ์งํ์๋ค. ์ดํ ๋ก๊ทธ์ธ ์ํ๋ฅผ ๊ธฐ๋กํ๊ณ ํ ํฐ์ ๋ฐ๊ธํ๋ค.
Access Token์ ๊ฒฝ์ฐ ์งง์ TTL(Time To Live)์ ๊ฐ์ง๋ฉฐ Refresh Token์ ์๋์ ์ผ๋ก ๊ธด TTL์ ๊ฐ์ง๋ค. Access Token์ ๋ชจ๋ API ์์ฒญ์ ํฌํจ๋๋ฏ๋ก ๋
ธ์ถ ๊ฐ๋ฅ์ฑ์ด ๋๊ธฐ ๋๋ฌธ์ด๋ค.
๋ํ Access Token๊ณผ Refresh Token์ ์๋ก ๋ค๋ฅธ Secret Key๋ก ์๋ช
ํ๋๋ก ๋ถ๋ฆฌํ์๋ค. ์ด๋ฅผ ํตํด ํ์ชฝ Secret์ด ์ ์ถ๋๋๋ผ๋ ๋ค๋ฅธ ํ ํฐ๊น์ง ์์กฐ๋๋ ์ํฉ์ ๋ฐฉ์งํ ์ ์์ผ๋ฉฐ ๋ณด์ ์ฌ๊ณ ๋ฐ์ ์ ์ํฅ ๋ฒ์๋ฅผ ์ต์ํํ ์ ์๋ค.
ํ ํฐ ๋ฐ๊ธ์ ์ฌ์ฉ๋๋ jwtService๋ NestJS์์ ์ ๊ณตํ๋ ๊ณต์ JWT ๋ชจ๋์ธ @nestjs/jwt์ ์๋น์ค์ด๋ค. ํด๋น ๋ชจ๋์ ๋ด๋ถ์ ์ผ๋ก jsonwebtoken ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๊ฐ์ธ๊ณ ์์ผ๋ฉฐ JWT ์์ฑ, ๊ฒ์ฆ๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.
3. JWT ์ธ์ฆ Guard ๊ตฌํ
๋ก๊ทธ์ธ ์ดํ ๋ฐ๊ธ๋ Access Token์ ๊ฒ์ฆํ๊ณ ์์ฒญ์์ ์ธ์ฆ ์ฌ๋ถ๋ฅผ ํ๋จํ๋ JwtAuthGuard๋ฅผ ๊ตฌํํ์๋ค.
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private readonly reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
// @Public()์ด ์ ์ฉ๋ ๊ฒฝ์ฐ JWT ๊ฒ์ฆ ์๋ต
if (isPublic) return true;
return super.canActivate(context);
}
handleRequest<TUser = CurrentUserPayload>(err: unknown, user:TUser): TUser {
if (err) {
if (err instanceof Error) throw err;
throw new UnauthorizedException(err);
}
if (!user) throw new UnauthorizedException();
return user;
}
}
AuthGuard('jwt')๋ฅผ ์์๋ฐ์์ Passport JWT ์ ๋ต์ ๊ทธ๋๋ก ํ์ฉํ์๋ค. Passport๋ Node.js์์ ์ธ์ฆ ์ ๋ต์ ์ฝ๊ฒ ๊ตฌํํ๋๋ก ๋์์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค. Local, JWT, OAuth ๋ฑ ๋ค์ํ ์ ๋ต์ ์ง์ํ๋ค.
๊ทธ์ค AuthGuard('jwt')์์ 'jwt'๋ Passport์ ๋ฑ๋กํ ์ ๋ต ์ด๋ฆ์ด๋ฉฐ ์ด ์ด๋ฆ์ ํตํด ์๋์ JwtStrategy์ ์ฐ๊ฒฐ๋๋ค.
super.canActivate(context)๊ฐ ํธ์ถ๋๋ฉด ๋ค์ ๊ณผ์ ์ด ์๋์ผ๋ก ์คํ๋๋ค.
1. ์์ฒญ ํค๋์์ Authorization: Bearer <token> ์ถ์ถ
2. ์๋ฒ Secret Key๋ก JWT ๊ฒ์ฆ (์๋ช
, ๋ง๋ฃ ์๊ฐ ๋ฑ ํ์ธ)
3. ๊ฒ์ฆ ์ฑ๊ณต ์ validate() ํธ์ถ
4. ๋ฐํ๊ฐ์ request.user์ ์ฃผ์
5. handleRequest() ์คํ
์ฆ Guard๋ฅผ ํตํด ์ ๋ต์ ์คํํ๊ณ Strategy๋ฅผ ํตํด JWT๋ฅผ ๊ฒ์ฆํ๋ค.
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(private readonly config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.getOrThrow<string>('JWT_ACCESS_SECRET'),
ignoreExpiration: false,
});
}
validate(payload: JwtPayload) {
return { id: payload.sub, role: payload.role };
}
}
NestJS์์ Passport ์ ๋ต์ ์ฝ๊ฒ ๋ฑ๋กํ๋๋ก ๋์์ฃผ๋ ํด๋์ค์ธ PassportStrategy๋ฅผ ์์๋ฐ๊ณ super() ์ต์
์ ์ค์ ํ๋ค.
jwtFromRequest๋ ์ด๋์ JWT๋ฅผ ๊ฐ์ ธ์ฌ์ง ์ง์ ํ๋ ์ต์
์ด๋ค. ์ฌ๊ธฐ์๋ Authorization ํค๋์ Bearer ํ ํฐ์ ํด๋นํ๋ค.
secretOrKey๋ ํ ํฐ ์๋ช
๊ฒ์ฆ์ฉ ๋น๋ฐ ํค์ด๋ค. ํ๊ฒฝ ๋ณ์๋ฅผ ํตํด ๊ฐ์ ธ์ค๋๋ก ์ค์ ํ์๋ค. ignoreExpiration: false๋ ๋ง๋ฃ๋ ํ ํฐ์ ์๋์ผ๋ก ๊ฑฐ๋ถํ๋๋ก ์ค์ ํ๋ค.
JWT ๊ฒ์ฆ ํ Passport๋ validate() ํจ์๋ฅผ ์๋์ผ๋ก ํธ์ถํ๋๋ฐ ๊ฒ์ฆ์ด ์๋ฃ๋ payload๋ฅผ ๊ฐ๊ณตํ๋ ์ญํ ์ ํ๋ฉฐ ์ดํ ์ธ๊ฐ ๋จ๊ณ์์ ํ์ฉ๋๋ค.
4. Role ๊ธฐ๋ฐ ์ธ๊ฐ ๊ตฌํ
JWT ์ธ์ฆ์ด ์๋ฃ๋๋ฉด request.user์๋ ์์์ ์ค์ ํ ๋๋ก ๋ค์๊ณผ ๊ฐ์ ์ ๋ณด๊ฐ ๋ด๊ธฐ๊ฒ ๋๋ค.
{ id: payload.sub, role: payload.role }
์ด์ ์๋ฒ๋ ์ฌ์ฉ์์ Role(๊ถํ)์ ๊ธฐ์ค์ผ๋ก ์ ๊ทผ์ ์ ์ดํ ์ ์๋ค.
import { SetMetadata } from '@nestjs/common';
import { UserRole } from '@/user/domain/user-role.enum';
// ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ๋ ์ฌ์ฉํ ๊ณ ์ ํค ๊ฐ
export const ROLES_KEY = 'roles';
// ๋ฐ์ฝ๋ ์ดํฐ๊ฐ ์ ์ฉ๋ ํด๋์ค ๋๋ ๋ฉ์๋์ ํ์ํ ๊ถํ ๋ฐฐ์ด์ ๋ฉํ๋ฐ์ดํฐ๋ก ์ ์ฅํ๋ค.
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles);
@Roles() ๋ฐ์ฝ๋ ์ดํฐ๋ ํน์ API์ ํ์ํ ๊ถํ์ ๋ฉํ๋ฐ์ดํฐ๋ก ๋ฑ๋กํ๋ ์ญํ ์ ํ๋ค.
SetMetadata๋ ๋ด๋ถ์ ์ผ๋ก ํด๋น ํธ๋ค๋ฌ(๋๋ ํด๋์ค)์ ํค-๊ฐ ํํ์ ๋ฐ์ดํฐ๋ฅผ ๋ถ์ธ๋ค.
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { UserRole } from '@/user/domain/user-role.enum';
import { ROLES_KEY } from '@/auth/presentation/decorators/roles.decorator';โ
import { RequestWithUser } from '@/auth/presentation/types/auth-request.type';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const required = this.reflector.getAllAndOverride<UserRole[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
// ๊ถํ ์ง์ ์ด ์๋ ๊ฒฝ์ฐ ํต๊ณผ
if (!required || required.length === 0) return true;
const req = context.switchToHttp().getRequest<RequestWithUser>();
const user = req.user;
if (!user?.role) throw new ForbiddenException('๊ถํ ์ ๋ณด๊ฐ ์์ต๋๋ค.');
if (!required.includes(user.role)) throw new ForbiddenException('์ ๊ทผ ๊ถํ์ด ์์ต๋๋ค.');
return true;
}
}
RolesGuard๋ @Roles() ๋ฐ์ฝ๋ ์ดํฐ๋ก ์ ์ธ๋ ๊ถํ ์ ๋ณด๋ฅผ Reflector๋ฅผ ํตํด ์กฐํํ๊ณ request.user์ ์ ์ฅ๋ ์ฌ์ฉ์ ์ญํ ๊ณผ ๋น๊ตํ์ฌ ์ ๊ทผ ๊ฐ๋ฅ ์ฌ๋ถ๋ฅผ ๊ฒฐ์ ํ๋ค.
์ธ์ฆ๊ณผ ์ธ๊ฐ๋ฅผ Guard ๊ณ์ธต์์ ๋ถ๋ฆฌํจ์ผ๋ก์จ ์ฑ
์์ด ๋ช
ํํด์ง๊ณ ํ์ฅ ๊ฐ๋ฅํ ๊ตฌ์กฐ๋ฅผ ๋ง๋ค ์ ์์๋ค.
