JWT ๐Ÿณ

Jayongยท2025๋…„ 5์›” 15์ผ

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ

๋ชฉ๋ก ๋ณด๊ธฐ
1/4
post-thumbnail

JWT๋ž€?

JWT๋Š” JSON Web Token์˜ ์•ฝ์ž๋กœ, ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ์„ ์ง€์›ํ•˜๋Š” ๊ฒƒ

โ†’ ํ† ํฐ์„ ์ธ์ฆ์— ์‚ฌ์šฉ (์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋ฅผ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•œ ์œ ์ผํ•œ ๊ฐ’)

์„œ๋ฒ„์—์„œ ํ† ํฐ์„ ์ƒ์„ฑํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „๋‹ฌ โ†’ ํด๋ผ์ด์–ธํŠธ๋Š” ํ† ํฐ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๊ฐ€ ์—ฌ๋Ÿฌ ์š”์ฒญ๊ณผ ํ† ํฐ์„ ํ•จ๊ป˜ ๋ณด๋ƒ„ โ†’ ์„œ๋ฒ„์—์„œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ํ›„ ํ† ํฐ์ด ์œ ํšจํ•˜๋‹ค๋ฉด ํด๋ผ์ด์–ธํŠธ์˜ ์š”์ฒญ์„ ์„ฑ๊ณต์ ์œผ๋กœ ์ฒ˜๋ฆฌ

์–ธ์ œ ์‚ฌ์šฉํ•˜๋Š”๊ฐ€?

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

์žฅ์ 

  1. ํฌ๊ธฐ : JSON์œผ๋กœ ์ƒ์„ฑ๋œ ํ† ํฐ์€ ์šฉ๋Ÿ‰์ด ์ž‘๊ธฐ ๋•Œ๋ฌธ์— ๋งค์šฐ ๋น ๋ฅด๊ฒŒ ์ „๋‹ฌ ๊ฐ€๋Šฅ
  2. ์•ˆ์ •์„ฑ : HMAC ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๋˜๋Š” RSA or ECDSA๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ณต๊ฐœ/๊ฐœ์ธ ํ‚ค ์Œ์„ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋ช…ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฌด๊ฒฐ์„ฑ์ด ๋ณด์žฅ
  3. ๋ฌด์ƒํƒœ์„ฑ(Stateless)
    • ์„ธ์…˜ ๊ธฐ๋ฐ˜์€ ์ƒํƒœ๋ฅผ ๊ฐ–๋Š”๋‹ค
    • ํ† ํฐ์€ ์„œ๋ฒ„๊ฐ€ ์•„๋‹Œ ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ๊ด€๋ฆฌํ•œ๋‹ค. ๋ธŒ๋ผ์šฐ์ €์˜ ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ํ˜น์€ ์ฟ ํ‚ค์— ์ €์žฅ๋˜๊ณ , ์ด ํ† ํฐ์„ HTTP ์š”์ฒญ๊ณผ ํ•จ๊ป˜ ์„œ๋ฒ„ ์ธก์— ๋ณด๋‚ด๋ฉด ์„œ๋ฒ„๋Š” ์ด ํ† ํฐ์— ๋Œ€ํ•œ ์œ„๋ณ€์กฐ ๊ฒ€์‚ฌ ๋ฐ ๋งŒ๋ฃŒ ์—ฌ๋ถ€ ๋“ฑ์˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ์‹ค์‹œ
    • ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ๋ฐฉ์‹์€ ์„œ๋ฒ„ ์ธก์—์„œ ํด๋ผ์ด์–ธํŠธ์˜ ์ƒํƒœ๋ฅผ ์ง์ ‘์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ๋†’์€ ํ™•์žฅ์„ฑ์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

๊ตฌ์กฐ

header / payload / signature
์„ธ๊ฐ€์ง€๋กœ ์ด๋ฃจ์–ด์ ธ ์žˆ์œผ๋ฉฐ, ๊ฐ๊ฐ์€ โ€˜.โ€™์œผ๋กœ ๊ตฌ๋ถ„ํ•œ๋‹ค. (ํ—ค๋”, ๋‚ด์šฉ, ์„œ๋ช…)

JWT๋ฅผ ๋””๋ฒ„๊น… ํ•  ์ˆ˜ ์žˆ๋Š” ์‚ฌ์ดํŠธ
https://jwt.io/#debugger-io

ํ—ค๋” (Header)

ํ† ํฐ์˜ ํƒ€์ž…(type)๊ณผ ํ•ด์‹ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜(alg)๋ฅผ ์ง€์ •

๋‚ด์šฉ (Payload)

ํ† ํฐ์— ๋‹ด์„ ์ •๋ณด

  • ํ† ํฐ์— ๋‹ด์„ ํ•˜๋‚˜์˜ ์ •๋ณด, ํ•œ ์กฐ๊ฐ์˜ ์ •๋ณด๋ฅผ ํด๋ ˆ์ž„(Claim)์ด๋ผ๊ณ  ํ•œ๋‹ค.

  • ๋“ฑ๋ก๋œ ํด๋ ˆ์ž„ (registered claim)

    • ์„œ๋น„์Šค์— ํ•„์š”ํ•œ ์ •๋ณด๊ฐ€ ์•„๋‹ˆ๋ผ ํ† ํฐ์— ๋Œ€ํ•œ ์ •๋ณด๋“ค์„ ๋‹ด๊ธฐ ์œ„ํ•ด ์ด๋ฆ„์ด ์ด๋ฏธ ์ •ํ•ด์ ธ ์žˆ๋Š” ํด๋ ˆ์ž„
    • ํ•„์ˆ˜๋Š” ์•„๋‹ˆ์ง€๋งŒ ๊ถŒ์žฅ๋˜๋Š” ํด๋ ˆ์ž„
    • iss(๋ฐœ๊ธ‰์ž), sub(์ œ๋ชฉ), aud(๋Œ€์ƒ์ž), exp(๋งŒ๋ฃŒ์‹œ๊ฐ„), nbf(ํ† ํฐ์˜ ํ™œ์„ฑ๋‚ ์งœ), iat(๋ฐœ๊ธ‰๋œ ์‹œ๊ฐ„), jti(JWT ๊ณ ์œ  ์‹๋ณ„์ž, ์ผํšŒ์šฉ ํ† ํฐ์— ์‚ฌ์šฉ)
  • ๊ณต๊ฐœ ํด๋ ˆ์ž„ (public claim)

    • ๊ณต๊ฐœ ํด๋ ˆ์ž„์€ ์‚ฌ์šฉ์ž ์ •์˜ ํด๋ ˆ์ž„์œผ๋กœ, ๊ณต๊ฐœ์šฉ ์ •๋ณด ์ „๋‹ฌ์„ ์œ„ํ•ด ์‚ฌ์šฉ. ์ถฉ๋Œ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด URI ํฌ๋งท์„ ์ด์šฉ
  • ๋น„๊ณต๊ฐœ ํด๋ ˆ์ž„ (private claim)

    • ๋น„๊ณต๊ฐœ ํด๋ ˆ์ž„์€ ๋“ฑ๋ก๋œ ํด๋ ˆ์ž„๋„ ์•„๋‹ˆ๊ณ , ๊ณต๊ฐœ ํด๋ ˆ์ž„๋„ ์•„๋‹Œ ๋‹น์‚ฌ์ž๊ฐ„์— ์ •๋ณด๋ฅผ ๊ณต์œ ํ•˜๊ธฐ ์œ„ํ•ด ๋งŒ๋“ค์–ด์ง„ ์‚ฌ์šฉ์ž ์ง€์ • ํด๋ ˆ์ž„
    • ์ด๊ณต์„ธ ์ธ์ฆ ์ •๋ณด ๋“ฑ์˜ ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ๊ฐ„ ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ๋„ฃ์–ด๋‘๋Š” ํ˜•์‹
{
  "sub": "accessToken", // ๋“ฑ๋ก๋œ ํด๋ ˆ์ž„ (์ œ๋ชฉ)
  "name": "tester", // ๋น„๊ณต๊ฐœ ํด๋ ˆ์ž„
  "email" : "test@test.com" // ๋น„๊ณต๊ฐœ ํด๋ ˆ์ž„
  "iat": 2020233022 // ๋“ฑ๋ก๋œ ํด๋ ˆ์ž„ (๋ฐœ๊ธ‰๋œ ์‹œ๊ฐ„)
}

์„œ๋ช… (Signature)

ํ† ํฐ์„ ์ธ์ฝ”๋”ฉํ•˜๊ฑฐ๋‚˜ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์„ ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๊ณ ์œ ํ•œ ์•”ํ˜ธํ™” ์ฝ”๋“œ

์„œ๋ช…์€ ์œ„์—์„œ ๋งŒ๋“ ย ํ—ค๋”์™€ ํŽ˜์ด๋กœ๋“œ์˜ ๊ฐ’์„ ๊ฐ๊ฐ BASE64๋กœ ์ธ์ฝ”๋”ฉํ•˜๊ณ , ์ธ์ฝ”๋”ฉ ํ•œ ๊ฐ’์„ ๋น„๋ฐ€ ํ‚ค๋ฅผ ์ด์šฉํ•˜์—ฌ ํ—ค๋”์—์„œ ์ •ํ•œ ์•Œ๊ณ ๋ฆฌ์ฆ˜์œผ๋กœ ํ•ด์‹ฑ์„ ํ•œ ํ›„ ๋‹ค์‹œ BASE64๋กœ ์ธ์ฝ”๋”ฉ ํ•˜์—ฌ ์ƒ์„ฑ

ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ์˜ ๋ฌธ์ œ์ 

Stateless ์„œ๋ฒ„๋Š” ํ•œ ๊ฐ€์ง€ ๋ฌธ์ œ์ ์ด ์กด์žฌ โ†’ ์„ธ์…˜ ๊ธฐ๋ฐ˜์˜ ์ธ์ฆ๊ณผ ๊ฐ™์ด ์„œ๋ฒ„ ์ชฝ์—์„œ ๊ด€๋ฆฌ๊ฐ€ ๋˜๊ณ  ์žˆ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์˜ฌ๋ฐ”๋ฅธ ์‚ฌ์šฉ์ž์ธ์ง€, ์•„๋‹ˆ๋ฉด ๋ˆ„๊ตฐ๊ฐ€ ํ† ํฐ์„ ํƒˆ์ทจํ•ด ์•…์˜์ ์ธ ์˜๋„๋กœ ์ ‘๊ทผํ•ด ์˜จ ์‚ฌ์šฉ์ž์ธ์ง€ ์•Œ์ง€ ๋ชปํ•œ๋‹ค๋Š” ๊ฒƒ

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

โ†’ ์ด๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด Refresh Token

๋ฆฌํ”„๋ ˆ์‰ฌ ํ† ํฐ์€ ์—‘์„ธ์Šค ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์„ ๋•Œ, ์—‘์„ธ์Šค ํ† ํฐ์„ ์ƒˆ๋กœ ๋ฐœ๊ธ‰ํ•˜๊ธฐ ์œ„ํ•œ ํ† ํฐ

์—‘์„ธ์Šค ํ† ํฐ๊ณผ ๋‹ค๋ฅธ ์ ์€ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์€ ์—‘์„ธ์Šค ํ† ํฐ๋ณด๋‹ค ์œ ํšจ๊ธฐ๊ฐ„์ด ๊ธธ๊ณ , ๋ฐ์ดํ„ฐ ๋ฒ ์ด์Šค์— ์ €์žฅ

RefreshToken ์ธ์ฆ ๊ณผ์ •

  1. ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ์„œ๋ฒ„์— ์ธ์ฆ ์š”์ฒญ

  2. ์„œ๋ฒ„์ธก์—์„œ ์—‘์„ธ์Šค ํ† ํฐ๊ณผ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ๋‘ ๊ฐœ๋ฅผ ํด๋ผ์ด์–ธํŠธ ์ธก์— ์ „์†ก

  3. ํด๋ผ์ด์–ธํŠธ๋Š” ์ „์†ก๋ฐ›์€ ํ† ํฐ์„ ์ €์žฅ

  4. ์„œ๋ฒ„์—์„œ๋Š” ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ

  5. ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ์ธ์ฆ์ด ํ•„์š”ํ•œ API ์š”์ฒญ์„ ์„œ๋ฒ„์ธก์— ๋ณด๋‚ผ ๋•Œ๋งˆ๋‹ค ์—‘์„ธ์Šค ํ† ํฐ์„ ๊ฐ™์ด ์ „์†ก

  6. ์„œ๋ฒ„ ์ธก์—์„œ๋Š” ์ „๋‹ฌ๋ฐ›์€ ์—‘์„ธ์Šค ํ† ํฐ์— ๋Œ€ํ•œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ํ›„ ์œ ํšจํ•˜๋‹ค๋ฉด ํด๋ผ์ด์–ธํŠธ ์š”์ฒญ์„ ์ฒ˜๋ฆฌ. ๋งŒ์•ฝ ๋งŒ๋ฃŒ๋˜์—ˆ๋‹ค๋ฉด, ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ๋‹ค๋Š” ์—๋Ÿฌ๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „๋‹ฌ

  7. ์—๋Ÿฌ๋ฅผ ์ „๋‹ฌ๋ฐ›์€ ํด๋ผ์ด์–ธํŠธ๋Š” ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ๊ณผ ํ•จ๊ป˜ ์ƒˆ ์—‘์„ธ์Šค ํ† ํฐ์„ ์„œ๋ฒ„ ์ธก์— ์š”์ฒญ

  8. ์„œ๋ฒ„์—์„œ๋Š” ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ์œ„ํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ์กฐํšŒ ํ›„ ๋น„๊ต

  9. ์œ ํšจํ•˜๋‹ค๋ฉด ์ƒˆ ์—‘์„ธ์Šค ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•˜๊ณ  ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „๋‹ฌ. ํด๋ผ์ด์–ธํŠธ๋Š” ์ƒˆ ์—‘์„ธ์Šค ํ† ํฐ์„ ๊ฐ€์ง€๊ณ  ์›๋ž˜ ์š”์ฒญํ•˜๋ ค๋˜ API๋ฅผ ๋‹ค์‹œ ์š”์ฒญ

JWT ์‚ฌ์šฉ์‹œ ์ฃผ์˜์ 

์ธ์ฆ์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์ž๊ฒฉ์ฆ๋ช…์„ ์‚ฌ์šฉํ•˜์—ฌ ์„ฑ๊ณต์ ์œผ๋กœ ๋กœ๊ทธ์ธํ•˜๋ฉด JSON ์›น ํ† ํฐ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. ํ† ํฐ์€ ์ž๊ฒฉ์ฆ๋ช…์„ ์ฃผ ๋ชฉ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ๋ณด์•ˆ ๋ฌธ์ œ์— ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

  • ์ผ๋ฐ˜์ ์œผ๋กœ ํ† ํฐ์„ ํ•„์š”์ด์ƒ์œผ๋กœ ์˜ค๋ž˜ ๋ณด๊ด€ํ•˜๋ฉด ์•ˆ๋œ๋‹ค.
  • ๋ณด์•ˆ์— ์ทจ์•ฝํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฏผ๊ฐ„ํ•จ ์„ธ์…˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ธŒ๋ผ์šฐ์ € ์ €์žฅ์†Œ์— ์ €์žฅํ•ด์„œ๋Š” ์•ˆ๋œ๋‹ค.
  • ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ณดํ˜ธ๋œ ๊ฒฝ๋กœ ๋˜๋Š” ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผํ•˜๋ ค๊ณ  ํ•  ๋•Œ ๋งˆ๋‹ค, ํด๋ผ์ด์–ธํŠธ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ Bearer ์Šคํ‚ค๋งˆ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Authorization ํ—ค๋”์—์„œ JWT๋ฅผ ๋ณด๋‚ด์•ผ ํ•œ๋‹ค.

JWT ์‚ฌ์šฉํ•ด๋ณด๊ธฐ

JwtService ๊ตฌํ˜„์ฒด

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
@Setter(value = AccessLevel.PRIVATE)
public class JwtServiceImpl implements JwtService{

    //== jwt.yml์— ์„ค์ •๋œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ ==//
    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.access.expiration}")
    private long accessTokenValidityInSeconds;

    @Value("${jwt.refresh.expiration}")
    private long refreshTokenValidityInSeconds;

    @Value("${jwt.access.header}")
    private String accessHeader;

    @Value("${jwt.refresh.header}")
    private String refreshHeader;

    //== 1 ==//
    private static final String ACCESS_TOKEN_SUBJECT = "AccessToken";
    private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken";
    private static final String USERNAME_CLAIM = "email";
    private static final String BEARER = "Bearer ";

    private final UserRepository usersRepository;
    private final ObjectMapper objectMapper;

    //== ๋ฉ”์„œ๋“œ ==//
    
    // AccessToken ์ƒ์„ฑ (์‚ฌ์šฉ์ž์˜ email ๊ธฐ๋ฐ˜)
    @Override
    public String createAccessToken(String email) {
        Users user = usersRepository.findByEmail(email)
                .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ์œ ์ €๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."));

        // RelationRepository๋ฅผ ํ†ตํ•ด ์œ ์ €์˜ ๊ทธ๋ฃน ์ด๋ฆ„ ์กฐํšŒ
        List<String> groupNames = relationRepository.findGroupNamesByUserId(user.getUserId());

        return JWT.create()
                // ๋นŒ๋”๋ฅผ ํ†ตํ•ด JWT์˜ Subject ์„ค์ • : AccessToken
                .withSubject(ACCESS_TOKEN_SUBJECT)
                // ๋งŒ๋ฃŒ์‹œ๊ฐ„ ์„ค์ •
                .withExpiresAt(new Date(System.currentTimeMillis() + accessTokenValidityInSeconds * 1000))
                // claim์œผ๋กœ email ์„ค์ •
                .withClaim(USERNAME_CLAIM, email)
                // ํ•ด๋‹น ์œ ์ €๊ฐ€ ์†ํ•œ ๊ทธ๋ฃน์˜ ์ด๋ฆ„๋“ค์„ ๋ฆฌ์ŠคํŠธ๋กœ ์„ค์ •
                .withClaim("groups", groupNames)
                // HMA512 ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‚ฌ์šฉํ•˜์—ฌ, secretํ‚ค๋กœ ์•”ํ˜ธํ™”
                .sign(Algorithm.HMAC512(secret));
    }

		// RefreshToken ์ƒ์„ฑ
    @Override
    public String createRefreshToken() {
        return JWT.create()
					      // ๋นŒ๋”๋ฅผ ํ†ตํ•ด JWT์˜ Subject ์„ค์ • : RefreshToken
                .withSubject(REFRESH_TOKEN_SUBJECT)
                // ๋งŒ๋ฃŒ์‹œ๊ฐ„ ์„ค์ •
                .withExpiresAt(new Date(System.currentTimeMillis() + refreshTokenValidityInSeconds * 1000))
                // HMA512 ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‚ฌ์šฉํ•˜์—ฌ, secretํ‚ค๋กœ ์•”ํ˜ธํ™”
                .sign(Algorithm.HMAC512(secret));
    }

		// RefreshToken ์œ ์ €์—๊ฒŒ Update
    @Override
    public void updateRefreshToken(String email, String refreshToken) {
		    // ํ•ด๋‹น email์„ ๊ฐ€์ง€๋Š” ์œ ์ € ์ฐพ๊ธฐ
        usersRepository.findByEmail(email)
                .ifPresentOrElse(
					              // ์กด์žฌํ•˜๋ฉด refreshToken ์—…๋ฐ์ดํŠธ
                        users -> users.updateRefreshToken(refreshToken),
                        () -> new Exception("ํšŒ์› ์กฐํšŒ ์‹คํŒจ")
                );
    }

		// RefreshToken ์‚ญ์ œ
    @Override
    public void destroyRefreshToken(String email) {
    		// ํ•ด๋‹น email์„ ๊ฐ€์ง€๋Š” ์œ ์ € ์ฐพ๊ธฐ
        usersRepository.findByEmail(email)
                .ifPresentOrElse(
				        			  // ์กด์žฌํ•˜๋ฉด refreshToken ์—…๋ฐ์ดํŠธ
                        users -> users.destroyRefreshToken(),
                        () -> new Exception("ํšŒ์› ์กฐํšŒ ์‹คํŒจ")
                );
    }

		// AccessToken๊ณผ RefreshToken์„ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „๋‹ฌ
    @Override
    public void sendAccessAndRefreshToken(HttpServletResponse response, String accessToken, String refreshToken) {
        // ์‘๋‹ต ๋ฉ”์‹œ์ง€์˜ ์ƒํƒœ๋ฅผ 200 OK๋กœ ์„ค์ •
        response.setStatus(HttpServletResponse.SC_OK);

				// AccessToken, RefreshToken์„ ํ—ค๋”์— ๋‹ด์Œ
        setAccessTokenHeader(response, accessToken);
        setRefreshTokenHeader(response, refreshToken);

				// tokenMap์— ๋‘ ๊ฐœ์˜ ํ† ํฐ ๋‚ด์šฉ์„ ์ €์žฅ
        Map<String, String> tokenMap = new HashMap<>();
        tokenMap.put(ACCESS_TOKEN_SUBJECT, accessToken);
        tokenMap.put(REFRESH_TOKEN_SUBJECT, refreshToken);

    }

		// AccessToken์„ ํด๋ผ๋ฆฌ์–ธํŠธ์—๊ฒŒ ์ „๋‹ฌ
    @Override
    public void sendAccessToken(HttpServletResponse response, String accessToken) {
        response.setStatus(HttpServletResponse.SC_OK);

        setAccessTokenHeader(response, accessToken);

        Map<String, String> tokenMap = new HashMap<>();
        tokenMap.put(ACCESS_TOKEN_SUBJECT, accessToken);
    }

		// ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ์„œ ์ „๋‹ฌ๋ฐ›์€ AccessToken์„ HTTP ํ—ค๋”์—์„œ ์ถ”์ถœ
    @Override
    public Optional<String> extractAccessToken(HttpServletRequest request) {
        return Optional.ofNullable(request.getHeader(accessHeader)).filter(
				        // BEARER ์ ‘๋‘์‚ฌ๋กœ ์‹œ์ž‘ํ•˜๋Š” AccessToken์„ ํ™•์ธ
                accessToken -> accessToken.startsWith(BEARER)
                // ์ฐพ์œผ๋ฉด BEARER ๋ถ€๋ถ„์„ ์ œ์™ธํ•œ AccessToken์„ ๋ฐ˜ํ™˜
        ).map(accessToken -> accessToken.replace(BEARER, ""));
    }

		// ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ์„œ ์ „๋‹ฌ๋ฐ›์€ RefreshToken์„ HTTP ํ—ค๋”์—์„œ ์ถ”์ถœ
    @Override
    public Optional<String> extractRefreshToken(HttpServletRequest request) {
        return Optional.ofNullable(request.getHeader(refreshHeader)).filter(
        				// BEARER ์ ‘๋‘์‚ฌ๋กœ ์‹œ์ž‘ํ•˜๋Š” RefreshToken์„ ํ™•์ธ
                refreshToken -> refreshToken.startsWith(BEARER)
                // ์ฐพ์œผ๋ฉด BEARER ๋ถ€๋ถ„์„ ์ œ์™ธํ•œ RefreshToken์„ ๋ฐ˜ํ™˜
        ).map(refreshToken -> refreshToken.replace(BEARER, ""));
    }

		// AccessToken์—์„œ email์„ claim์„ ์ถ”์ถœํ•˜์—ฌ ๋งž๋Š”์ง€ ํ™•์ธ 
    @Override
    public Optional<String> extractEmail(String accessToken) {
        try {
            return Optional.ofNullable(
					          // JWT ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•ด accessToken ์† email์„ ์ถ”์ถœ (USERNAME_CLAIM)
                    JWT.require(Algorithm.HMAC512(secret)).build().verify(accessToken).getClaim(USERNAME_CLAIM)
                            .asString());
        } catch (Exception e) {
            log.error(e.getMessage());
            return Optional.empty();
        }
    }

    @Override
    public void setAccessTokenHeader(HttpServletResponse response, String accessToken) {
        response.setHeader(accessHeader, accessToken);
    }

    @Override
    public void setRefreshTokenHeader(HttpServletResponse response, String refreshToken) {
        response.setHeader(refreshHeader, refreshToken);
    }

		// ํ•ด๋‹น ํ† ํฐ์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์‚ฌ
    @Override
    public boolean isTokenValid(String token) {
        try {
            JWT.require(Algorithm.HMAC512(secret)).build().verify(token);
            return true;
        } catch (Exception e) {
            log.error("์œ ํšจํ•˜์ง€ ์•Š์€ Token์ž…๋‹ˆ๋‹ค", e.getMessage());
            return false;
        }
    }

}

AccessToken์„ ์ด์šฉํ•œ ์ธ์ฆ ํ•„ํ„ฐ

ํ•„ํ„ฐ ์š”๊ตฌ์‚ฌํ•ญ

  • /login ์œผ๋กœ ์˜ค๋Š” ์š”์ฒญ์— ๋Œ€ํ•ด์„œ๋Š” ํ•„ํ„ฐ ์ž‘๋™ X โ†’ JsonUsernamePasswordAuthenticationFilter์— ์œ„์ž„
  • ๊ทธ ์™ธ์— ๋“ค์–ด์˜ค๋Š” ๋ชจ๋“  ์š”์ฒญ์— ๋Œ€ํ•ด์„œ ์ž‘๋™
  • RefreshToken์„ ํฌํ•จํ•˜์—ฌ ์š”์ฒญ์ด ์ „์†ก๋˜๋Š” ๊ฒฝ์šฐ๋Š” 4๊ฐ€์ง€ ์ƒํ™ฉ์ด ์กด์žฌ
    1. ๋‘˜๋‹ค ์œ ํšจ โ†’ AccessToken ์žฌ๋ฐœ๊ธ‰, ์ธ์ฆ ์ง„ํ–‰ X
    2. RefreshToken๋งŒ ์œ ํšจ โ†’ AccessToken ์žฌ๋ฐœ๊ธ‰
    3. AccessToken๋งŒ ์œ ํšจ โ†’ ์ธ์ฆ์€ ์„ฑ๊ณต๋˜๋‚˜, RefreshToken ์žฌ๋ฐœ๊ธ‰ X
    4. RefreshToken๊ณผ AccessToken ๋ชจ๋‘ ์—†๊ฑฐ๋‚˜ ์œ ํšจํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ โ†’ ์ธ์ฆ ์‹คํŒจ

JwtAuthenticationProcessingFilter

OncePerRequestFilter ์ƒ์† ๊ตฌํ˜„ ํด๋ž˜์Šค โ†’ ๋ชจ๋“  ์„œ๋ธ”๋ฆฟ ์ปจํ…Œ์ด๋„ˆ์—์„œ ์š”์ฒญ ๋””์ŠคํŒจ์น˜๋‹น ๋‹จ์ผ ์‹คํ–‰์„ ๋ณด์žฅํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•˜๋Š” ํ•„ํ„ฐ ๊ธฐ๋ณธ ํด๋ž˜์Šค

@RequiredArgsConstructor
public class JwtAuthenticationProcessingFilter extends OncePerRequestFilter {

    private final JwtService jwtService;

    private final UserRepository userRepository;

    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    private final String NO_CHECK_URL = "/login";

     /**
     * 1. ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ด ์˜ค๋Š” ๊ฒฝ์šฐ -> ์œ ํšจํ•˜๋ฉด AccessToken ์žฌ๋ฐœ๊ธ‰ํ›„, ํ•„ํ„ฐ ์ง„ํ–‰ X, ๋ฐ”๋กœ ํŠ•๊ธฐ๊ธฐ
     *
     * 2. ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์€ ์—†๊ณ  AccessToken๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ -> ์œ ์ €์ •๋ณด ์ €์žฅํ›„ ํ•„ํ„ฐ ๊ณ„์† ์ง„ํ–‰
     */

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // /login์œผ๋กœ ์ ‘๊ทผ์‹œ ํ•„ํ„ฐ ์ ์šฉ X
        if(request.getRequestURI().equals(NO_CHECK_URL)) {
            filterChain.doFilter(request, response);
            return; //์•ˆํ•ด์ฃผ๋ฉด ์•„๋ž˜๋กœ ๋‚ด๋ ค๊ฐ€์„œ ๊ณ„์† ํ•„ํ„ฐ๋ฅผ ์ง„ํ–‰ํ•˜๊ฒŒ๋จ
        }
				
				// refreshToken์„ ์š”์ฒญ HTTP์—์„œ ์ฐพ์•„ ๊ฒ€์ฆ
        String refreshToken = jwtService
                .extractRefreshToken(request)
                .filter(jwtService::isTokenValid)
                .orElse(null); // RefreshToken์ด ์—†๊ฑฐ๋‚˜ ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฉด null์„ ๋ฐ˜ํ™˜

				// refreshToken์ด ์กด์žฌํ•  ๊ฒฝ์šฐ 
        if(refreshToken != null){
			      // refreshToken์ด ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ณ , ์‚ฌ์šฉ์ž์—๊ฒŒ AccessToken ์žฌ๋ฐœ๊ธ‰
            checkRefreshTokenAndReIssueAccessToken(response, refreshToken);
            return; // ์ธ์ฆ์„ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด return
        }

				// refreshToken์ด ์—†๋‹ค๋ฉด AccessToken์„ ๊ฒ€์‚ฌํ•˜๋Š” ๋กœ์ง ์ˆ˜ํ–‰
        checkAccessTokenAndAuthentication(request, response, filterChain); 
    }

    private void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        jwtService.extractAccessToken(request).filter(jwtService::isTokenValid).ifPresent(
                // AccessToken์ด ์กด์žฌํ•˜๊ณ  ์œ ํšจํ•˜๋‹ค๋ฉด
                // AccessToken์—์„œ email์„ ์ถ”์ถœํ•˜๊ณ , email๋กœ ์œ ์ € ์ •๋ณด๋ฅผ ์ฐพ์•„์„œ ์ธ์ฆ์ฒ˜๋ฆฌ
                accessToken -> jwtService.extractEmail(accessToken).ifPresent(
			
                        email -> userRepository.findByEmail(email).ifPresent(
																// ์ธ์ฆ ์ฒ˜๋ฆฌ
                                users -> saveAuthentication(users)
                        )
                )
        );

        filterChain.doFilter(request,response);
    }

    private void saveAuthentication(Users users) {
        UserDetailsImpl userDetails = new UserDetailsImpl(users);

        Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null,authoritiesMapper.mapAuthorities(userDetails.getAuthorities()));

        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authentication);
        SecurityContextHolder.setContext(context);
    }

    private void checkRefreshTokenAndReIssueAccessToken(HttpServletResponse response, String refreshToken) {
        // ํ•ด๋‹นํ•˜๋Š” refreshToken์ด DB์— ์กด์žฌํ•˜๋ฉด, user์—๊ฒŒ AccessToken ๋ฐœ๊ธ‰
        userRepository.findByRefreshToken(refreshToken).ifPresent(
                users -> jwtService.sendAccessToken(response, jwtService.createAccessToken(users.getEmail()))
        );

    }
}

์ฐธ๊ณ 

JWT๋ž€ ๋ฌด์—‡์ธ๊ฐ€?

profile
๋‹ค์–‘ํ•œ ๊ฒฝํ—˜์„ ์ถ”๊ตฌํ•˜๋Š” ๊ฐœ๋ฐœ์ž

0๊ฐœ์˜ ๋Œ“๊ธ€