• ABOUT
  • POSTS
  • GUESTBOOK

ยฉ 2025 BlueCool12 All rights reserved.

2026.02.16NestJS

๐Ÿ” ์›น ์„œ๋น„์Šค ์ธ์ฆ/์ธ๊ฐ€ ์ดํ•ด์™€ JWT ๋กœ๊ทธ์ธ ๊ตฌํ˜„ (ft. NestJS)

1. ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ ๊ตฌํ˜„

์›น ์„œ๋น„์Šค๋ฅผ ๊ฐœ๋ฐœํ•  ๋•Œ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์€ ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ด๋ฉด์„œ๋„ ์ค‘์š”ํ•œ ์š”์†Œ๋‹ค. ๋‹จ์ˆœํžˆ ์•„์ด๋””์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฒƒ์„ ๋„˜์–ด ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์šฐ๋ฆฌ ์„œ๋น„์Šค์˜ ์œ ์ €๊ฐ€ ๋งž๋Š”์ง€ ๊ฒ€์ฆํ•˜๊ณ  ํ•ด๋‹น ์š”์ฒญ์— ๋Œ€ํ•œ ์ ‘๊ทผ ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€๊นŒ์ง€ ํŒ๋‹จํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ์„ค๊ณ„ํ•˜๊ธฐ ์ „์— ์„ ํ–‰๋˜์–ด์•ผ ํ•  ์ธ์ฆ(Authentication)๊ณผ ์ธ๊ฐ€(Authorization)์˜ ๊ฐœ๋…์„ ๋จผ์ € ์ •๋ฆฌํ•ด ๋ณด์ž.


2. ์ธ์ฆ & ์ธ๊ฐ€

์ธ์ฆ(Authentication)์€ ์‚ฌ์šฉ์ž์˜ ์‹ ์›์„ ํ™•์ธํ•˜๋Š” ๊ณผ์ •์ด๋‹ค. ๊ณตํ•ญ์—์„œ ์—ฌ๊ถŒ์„ ์ œ์‹œํ•ด ๋ณธ์ธ์ด ๋ˆ„๊ตฌ์ธ์ง€ ์ฆ๋ช…ํ•˜๋Š” ๊ฒƒ์— ๋น„์œ ํ•  ์ˆ˜ ์žˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ID์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋œ ์ •๋ณด์™€ ์ผ์น˜ํ•˜๋Š”์ง€๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ๋‹จ๊ณ„๊ฐ€ ์ด์— ํ•ด๋‹นํ•œ๋‹ค.

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

์ธ์ฆ๊ณผ ์ธ๊ฐ€๋Š” ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„๋˜๋Š” ๊ฐœ๋…์ด๋ฉฐ ์ธ์ฆ์ด ์„ ํ–‰๋˜์–ด์•ผ๋งŒ ์ธ๊ฐ€๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค. ์‚ฌ์šฉ์ž์˜ ์‹ ์›์ด ํ™•์ธ๋˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ๋Š” ์ ์ ˆํ•œ ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.


3. ์„ธ์…˜ vs JWT

์ธ์ฆ๊ณผ ์ธ๊ฐ€๋ฅผ ๊ตฌํ˜„ํ•  ๋•Œ ํ”ํžˆ ๊ณ ๋ฏผํ•˜๊ฒŒ ๋˜๋Š” ์„ ํƒ์ง€๋Š” ์„ธ์…˜(Session)๊ณผ JWT(Json Web Token) ๋ฐฉ์‹์ด๋‹ค. ๋‘ ๋ฐฉ์‹์˜ ๊ฐ€์žฅ ํฐ ์ฐจ์ด๋Š” ์‚ฌ์šฉ์ž์˜ ๋กœ๊ทธ์ธ ์ƒํƒœ(์ƒํƒœ ์ •๋ณด)๋ฅผ ์–ด๋””์— ์ €์žฅํ•˜๋А๋ƒ์— ์žˆ๋‹ค.

์„ธ์…˜(Session) ๋ฐฉ์‹
์„œ๋ฒ„๊ฐ€ ์‚ฌ์šฉ์ž ์ƒํƒœ ์ •๋ณด๋ฅผ ์ง์ ‘ ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ๊ตฌ์กฐ์ด๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ ๋ฉ”๋ชจ๋ฆฌ ๋˜๋Š” Redis์™€ ๊ฐ™์€ ์ €์žฅ์†Œ์— ์„ธ์…˜์„ ๋ณด๊ด€ํ•œ๋‹ค.

  • ํ๋ฆ„
    ๋กœ๊ทธ์ธ ์„ฑ๊ณต -> ์„œ๋ฒ„๊ฐ€ ์„ธ์…˜ ์ €์žฅ์†Œ์— ์‚ฌ์šฉ์ž ์ •๋ณด ์ €์žฅ -> Session ID ๋ฐœ๊ธ‰ -> ํด๋ผ์ด์–ธํŠธ๋Š” ์ฟ ํ‚ค์— Session ID ์ €์žฅ -> ์š”์ฒญ ์‹œ ์ฟ ํ‚ค ์ „์†ก -> ์„œ๋ฒ„๊ฐ€ ์ €์žฅ์†Œ์—์„œ ID ์กฐํšŒ ๋ฐ ๊ฒ€์ฆ
  • ์žฅ์ 
    ์„œ๋ฒ„๊ฐ€ ์ƒํƒœ๋ฅผ ์ง์ ‘ ๊ด€๋ฆฌํ•˜๋ฏ€๋กœ ํ†ต์ œ๋ ฅ์ด ๋†’๋‹ค.
    ํŠน์ • ๊ธฐ๊ธฐ ๋กœ๊ทธ์•„์›ƒ, ์„ธ์…˜ ๊ฐ•์ œ ๋งŒ๋ฃŒ ๋“ฑ์˜ ์ฒ˜๋ฆฌ๊ฐ€ ์šฉ์ดํ•˜๋‹ค.
  • ๋‹จ์ 
    ์‚ฌ์šฉ์ž๊ฐ€ ์ฆ๊ฐ€ํ• ์ˆ˜๋ก ์„œ๋ฒ„ ๋ฉ”๋ชจ๋ฆฌ ๋ถ€๋‹ด์ด ์ปค์ง„๋‹ค.
    ์„œ๋ฒ„๊ฐ€ ์—ฌ๋Ÿฌ ๋Œ€์ผ ๊ฒฝ์šฐ ์„ธ์…˜ ๊ณต์œ ๋ฅผ ์œ„ํ•œ ์ถ”๊ฐ€ ๊ตฌ์„ฑ(Session Clustering)์ด ํ•„์š”ํ•˜๋‹ค.
    ์ˆ˜ํ‰ ํ™•์žฅ(Scale-out)์ด ๋น„๊ต์  ๋ณต์žกํ•˜๋‹ค.


JWT(Token) ๋ฐฉ์‹
์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์„œ๋ช…๋œ ํ† ํฐ์— ๋‹ด์•„ ํด๋ผ์ด์–ธํŠธ์— ์ „๋‹ฌํ•˜๊ณ  ์„œ๋ฒ„๋Š” ์ด๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š๋Š” ๊ตฌ์กฐ์ด๋‹ค. ์ฆ‰ ์„œ๋ฒ„๊ฐ€ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š๋Š” Stateless ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง„๋‹ค.

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


์–ด๋–ค ๋ฐฉ์‹์ด ์ ˆ๋Œ€์ ์œผ๋กœ ํ•ญ์ƒ ์ข‹๋‹ค๊ณ  ๋ณด๊ธฐ๋Š” ์–ด๋ ต๋‹ค. ์„œ๋น„์Šค์˜ ๊ทœ๋ชจ, ํ™•์žฅ ๊ณ„ํš, ์ธํ”„๋ผ ๊ตฌ์กฐ์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ๋ฐฉ์‹์„ ์„ ํƒํ•ด์•ผ ํ•œ๋‹ค. ์ด๋ฒˆ ๋ธ”๋กœ๊ทธ CMS ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ํ™•์žฅ์„ฑ๊ณผ ๋ฌด์ƒํƒœ ๊ตฌ์กฐ๋ฅผ ๊ฒฝํ—˜ํ•ด ๋ณด๊ธฐ ์œ„ํ•ด JWT ๋ฐฉ์‹์„ ์ฑ„ํƒํ•˜์˜€๋‹ค.


4. 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์œผ๋กœ ๋ฐ”๊ฟจ๋‹ค๋ฉด ์„œ๋ฒ„์—์„œ ๋‹ค์‹œ ๊ณ„์‚ฐํ•œ ์„œ๋ช…๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š๊ฒŒ ๋˜์–ด ์„œ๋ฒ„๋Š” ์ด ํ† ํฐ์ด ๋ณ€๊ฒฝ๋˜์—ˆ๋‹ค๊ณ  ํŒ๋‹จํ•˜์—ฌ ์š”์ฒญ์„ ๊ฑฐ์ ˆํ•œ๋‹ค.


5. NestJS ๊ธฐ๋ฐ˜ ๋กœ๊ทธ์ธ ๋ฐ JWT ์ธ์ฆ/์ธ๊ฐ€ ๊ตฌํ˜„

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 ๊ณ„์ธต์—์„œ ๋ถ„๋ฆฌํ•จ์œผ๋กœ์จ ์ฑ…์ž„์ด ๋ช…ํ™•ํ•ด์ง€๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์—ˆ๋‹ค.

์ด์ „ ๊ธ€
โ˜•๏ธ JS๊ฐ€ ์‹ฑ๊ธ€ ์Šค๋ ˆ๋“œ๋กœ ๋™์‹œ์„ฑ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ• (์ด๋ฒคํŠธ ๋ฃจํ”„ & ๋ธ”๋กœํ‚น)
๋‹ค์Œ ๊ธ€
๋‹ค์Œ ๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค ( ฮ‡ . ฮ‡)
์žฅ์‹์šฉ ๋กœ๊ณ