[TECH-QA] JWT(JSON Web Token) ์ธ์ฆ ๋ฐฉ์‹

JWT(JSON Web Token)๋Š” ์›น์—์„œ ์‚ฌ์šฉ์ž ์ธ์ฆ์„ ์œ„ํ•ด ๋„๋ฆฌ ์‚ฌ์šฉ๋˜๋Š” ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. JWT๋Š” ์„ธ ๊ฐ€์ง€ ์ฃผ์š” ๋ถ€๋ถ„์œผ๋กœ ๊ตฌ์„ฑ๋˜๋ฉฐ, ๊ฐ ๋ถ€๋ถ„์€ ์ (.)์œผ๋กœ ๊ตฌ๋ถ„๋ฉ๋‹ˆ๋‹ค
  • Header(ํ—ค๋”)
  • Payload(ํŽ˜์ด๋กœ๋“œ)
  • Signature(์„œ๋ช…)
์ด๋ฅผ Base64 URL ์ธ์ฝ”๋”ฉ ๋ฐฉ์‹์œผ๋กœ ์ง๋ ฌํ™”ํ•˜์—ฌ ํ—ค๋” . ํŽ˜์ด๋กœ๋“œ . ์„œ๋ช… ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด๋กœ ํ‘œํ˜„๋ฉ๋‹ˆ๋‹ค.

Header

  • ํ† ํฐ์˜ ์œ ํ˜•(๋ณดํ†ต "JWT")
  • ์„œ๋ช…์— ์‚ฌ์šฉ๋œ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์ •์˜(์˜ˆ: HMAC SHA256, RSA ๋“ฑ)
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload

์‚ฌ์šฉ์ž ์ธ์ฆ์— ํ•„์š”ํ•œ ์ •๋ณด(ํด๋ ˆ์ž„, Claims)๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—๋Š” ์‚ฌ์šฉ์ž ID, ๊ถŒํ•œ, ๋ฐœ๊ธ‰ ์‹œ๊ฐ„(iat), ๋งŒ๋ฃŒ ์‹œ๊ฐ„(exp) ๋“ฑ ํ‘œ์ค€ ํด๋ ˆ์ž„๊ณผ ํ•จ๊ป˜ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ถ”๊ฐ€ํ•œ ์ปค์Šคํ…€ ํด๋ ˆ์ž„์ด ๋“ค์–ด๊ฐˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
{
  "sub": "user123",
  "name": "John Doe",
  "iat": 1712016000,
  "exp": 1712023200
}

Signature

Header์™€ Payload๋ฅผ ์กฐํ•ฉํ•œ ํ›„ ์„œ๋ฒ„์˜ ๋น„๋ฐ€ ํ‚ค(secret key)๋ฅผ ์‚ฌ์šฉํ•ด ์„œ๋ช…ํ•œ ๊ฐ’์ž…๋‹ˆ๋‹ค. ์ด ์„œ๋ช…์€ ํ† ํฐ์˜ ๋ฌด๊ฒฐ์„ฑ์„ ๋ณด์žฅํ•˜๋ฉฐ, ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํ† ํฐ์„ ์œ„์กฐํ•˜๊ฑฐ๋‚˜ ๋ณ€์กฐํ–ˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์„œ๋ช… ์ƒ์„ฑ ๊ณผ์ •์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

์„œ๋ฒ„์˜ ๋น„๋ฐ€ ํ‚ค
- ์„œ๋ฒ„์˜ ๋น„๋ฐ€ ํ‚ค(secret key)๋Š” ์„œ๋ฒ„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์„ค๊ณ„๋˜๊ณ  ๋ฐฐํฌ๋˜๋Š” ๋‹จ๊ณ„์—์„œ ์ƒ์„ฑ๋˜๊ฑฐ๋‚˜ ์„ค์ •๋ฉ๋‹ˆ๋‹ค.
- JWT๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•ด ์„œ๋ฒ„ ์ธก์—์„œ ๋ฏธ๋ฆฌ ์„ค์ •๋˜๊ณ  ๊ด€๋ฆฌ๋˜๋Š” ๊ณ ์ •๋œ ๊ฐ’์ž…๋‹ˆ๋‹ค.
- ์ด ํ‚ค๋Š” JWT์˜ Signature ๋ถ€๋ถ„์„ ์ƒ์„ฑํ•˜๊ณ , ๋‚˜์ค‘์— ํ† ํฐ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•  ๋•Œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

์žฅ์ 

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

๋‹จ์  ๋ฐ ๊ณ ๋ ค์‚ฌํ•ญ

  • ์ˆ˜์ • ๋ฐ ํ๊ธฐ ๋ถˆ๊ฐ€ : ํ•œ ๋ฒˆ ๋ฐœ๊ธ‰๋œ JWT๋Š” ๋งŒ๋ฃŒ ์ „๊นŒ์ง€ ์œ ํšจํ•˜๋ฉฐ ์ค‘๊ฐ„์— ๋ฌดํšจํ™”ํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ๋ณด์™„ํ•˜๋ ค๋ฉด Refresh Token์„ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ด ์งง์€ ์œ ํšจ๊ธฐ๊ฐ„์˜ Access Token์„ ์ฃผ๊ธฐ์ ์œผ๋กœ ๊ฐฑ์‹ ํ•˜๋Š” ๋ฐฉ์‹์ด ์ผ๋ฐ˜์ ์ž…๋‹ˆ๋‹ค.
  • ํ† ํฐ ํƒˆ์ทจ ์œ„ํ—˜ : ํ† ํฐ์ด ํด๋ผ์ด์–ธํŠธ์— ์ €์žฅ๋˜๋ฏ€๋กœ(์ฃผ๋กœ ๋ธŒ๋ผ์šฐ์ €์˜ localStorage๋‚˜ cookie), XSS(Cross-Site Scripting) ๊ณต๊ฒฉ์— ๋…ธ์ถœ๋  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๋ ค๋ฉด HTTPS๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ํ† ํฐ์„ HttpOnly, Secure ์†์„ฑ์ด ์„ค์ •๋œ ์ฟ ํ‚ค์— ์ €์žฅํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.
  • Payload ๋…ธ์ถœ : JWT์˜ Payload๋Š” ์•”ํ˜ธํ™”๋˜์ง€ ์•Š๊ณ  Base64๋กœ ์ธ์ฝ”๋”ฉ๋งŒ ๋˜์–ด ์žˆ์–ด ๋ˆ„๊ตฌ๋‚˜ ๋””์ฝ”๋”ฉํ•ด ๋‚ด์šฉ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋ฏผ๊ฐํ•œ ์ •๋ณด(์˜ˆ: ๋น„๋ฐ€๋ฒˆํ˜ธ)๋Š” ์ ˆ๋Œ€ ํฌํ•จ์‹œํ‚ค์ง€ ์•Š์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ํ† ํฐ ํƒˆ์ทจ ์œ„ํ—˜

HttpOnly
HttpOnly๋Š” ์ฟ ํ‚ค์— ๋ถ™์ด๋Š” ์†์„ฑ ์ค‘ ํ•˜๋‚˜๋กœ, JavaScript์—์„œ ํ•ด๋‹น ์ฟ ํ‚ค์— ์ ‘๊ทผํ•˜์ง€ ๋ชปํ•˜๊ฒŒ ๋ง‰๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค.

Secure
HTTPS์—์„œ๋งŒ ์ „์†ก๋˜๊ฒŒ ํ•˜๋Š” ์†์„ฑ ์ž…๋‹ˆ๋‹ค.

SameSite
CSRF ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ์ฟ ํ‚ค๊ฐ€ ์–ด๋–ค ์ƒํ™ฉ์—์„œ ์ž๋™์œผ๋กœ ๋ธŒ๋ผ์šฐ์ €์— ์ „์†ก๋ ์ง€๋ฅผ ์ œํ•œํ•˜๋Š” ์†์„ฑ์ž…๋‹ˆ๋‹ค.
SameSite=Strict ์„ค์ •์ผ๋•Œ์—๋Š” ์ฟ ํ‚ค๊ฐ€ ๋™์ผ ์ถœ์ฒ˜ ์š”์ฒญ์—๋งŒ ์ „์†ก๋˜๋„๋ก ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

Set-Cookie : sessionId=abc123; Secure; HttpOnly; SameSite=Strict
				
//JavaScript๋กœ ์ ‘๊ทผ ๋ถˆ๊ฐ€
console.log(document.cookie); // "" (๋นˆ ๋ฌธ์ž์—ด ์ถœ๋ ฅ)
JWT๋ฅผ ์ฟ ํ‚ค์— ์ €์žฅํ•  ๋•Œ : ํด๋ผ์ด์–ธํŠธ๋Š” ์ด ํ† ํฐ์„ JavaScript๋กœ ์ฝ์„ ์ˆ˜ ์—†๊ณ , ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž๋™์œผ๋กœ ์„œ๋ฒ„ ์š”์ฒญ์— ํฌํ•จ์‹œ์ผœ ์ธ์ฆ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
Set-Cookie: token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMTIzIn0.์„œ๋ช…; Path=/; Secure; HttpOnly
HTTPOnly๋Š” ์ฟ ํ‚ค๋ฅผ ํด๋ผ์ด์–ธํŠธ ์ธก ์Šคํฌ๋ฆฝํŠธ๋กœ๋ถ€ํ„ฐ ๋ณดํ˜ธํ•˜๋Š” ๊ฐ„๋‹จํ•˜๋ฉด์„œ๋„ ๊ฐ•๋ ฅํ•œ ๋ณด์•ˆ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ž…๋‹ˆ๋‹ค. ํŠนํžˆ ์ธ์ฆ ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋ฅผ ์ฟ ํ‚ค์— ์ €์žฅํ•  ๋•Œ ํ•„์ˆ˜์ ์œผ๋กœ ๊ณ ๋ คํ•ด์•ผ ํ•˜๋ฉฐ, Secure์™€ SameSite ์†์„ฑ๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ณด์•ˆ์„ ๋”์šฑ ๊ฐ•ํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ, ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ํ† ํฐ์„ ์ง์ ‘ ๋‹ค๋ค„์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ๋Œ€์•ˆ(์˜ˆ: Authorization ํ—ค๋”)์„ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์œ ํšจ๊ธฐ๊ฐ„ ์„ค์ •์˜ ์ค‘์š”์„ฑ

  • ์œ ํšจ๊ธฐ๊ฐ„(exp)์„ ์งง๊ฒŒ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์€ ๋ณด์•ˆ์„ ๊ฐ•ํ™”ํ•˜๋Š” ํ•ต์‹ฌ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, Access Token์€ 15๋ถ„~1์‹œ๊ฐ„, Refresh Token์€ ํ•˜๋ฃจ ์ •๋„๋กœ ์„ค์ •ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค. ์œ ํšจ๊ธฐ๊ฐ„์ด ๊ธธ๋ฉด ํ† ํฐ์ด ์œ ์ถœ๋˜์—ˆ์„ ๋•Œ ์•…์šฉ๋  ์‹œ๊ฐ„์ด ๋Š˜์–ด๋‚˜๋ฏ€๋กœ, ์งง์€ ์ฃผ๊ธฐ๋กœ ๊ฐฑ์‹ ํ•˜๋ฉฐ ๋ณด์•ˆ๊ณผ ํŽธ์˜์„ฑ์„ ๊ท ํ˜• ์žˆ๊ฒŒ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.
JWT๋Š” ์ธ์ฆ ์™ธ์—๋„ ๊ถŒํ•œ ๋ถ€์—ฌ(Authorization)๋‚˜ ์ •๋ณด ๊ตํ™˜(์˜ˆ: SSO, Single Sign-On)์—๋„ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, OAuth 2.0 ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ID ํ† ํฐ์œผ๋กœ ํ™œ์šฉ๋˜๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค. ๊ฒฐ๋ก ์ ์œผ๋กœ, JWT๋Š” ์„œ๋ฒ„ ๋ถ€ํ•˜๋ฅผ ์ค„์ด๊ณ  ํšจ์œจ์ ์ธ ์ธ์ฆ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜์ง€๋งŒ, ๋ณด์•ˆ ์„ค๊ณ„(์งง์€ ์œ ํšจ๊ธฐ๊ฐ„, Refresh Token ๋„์ž…, HTTPS ํ•„์ˆ˜ ๋“ฑ)๋ฅผ ์‹ ์ค‘ํžˆ ๊ณ ๋ คํ•ด์•ผ ํšจ๊ณผ์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋กœ๊ทธ์ธ ๊ณผ์ •์—์„œ์˜ JWT

  • ์‚ฌ์šฉ์ž์˜ ์ž๊ฒฉ ์ฆ๋ช…(์˜ˆ: ID, ๋น„๋ฐ€๋ฒˆํ˜ธ)์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  • ์ธ์ฆ์ด ์„ฑ๊ณตํ•˜๋ฉด, ์„œ๋ฒ„๋Š” ๋ฏธ๋ฆฌ ์„ค์ •๋œ ๋น„๋ฐ€ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•ด JWT๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • ์ƒ์„ฑ๋œ JWT(ํ—ค๋”.ํŽ˜์ด๋กœ๋“œ.์„œ๋ช… ํ˜•ํƒœ)๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค.
์—ฌ๊ธฐ์„œ ๋ฐœํ–‰๋˜๋Š” ๊ฒƒ์€ JWT ์ž์ฒด์ด๋ฉฐ, ๋น„๋ฐ€ ํ‚ค๋Š” ๊ทธ ๊ณผ์ •์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋„๊ตฌ์ผ ๋ฟ์ž…๋‹ˆ๋‹ค.
  • ๋น„๋ฐ€ ํ‚ค: my-secret-key (์„œ๋ฒ„์— ๊ณ ์ •์ ์œผ๋กœ ์ €์žฅ๋จ)
  • JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIn0.์„œ๋ช…๊ฐ’