๐ŸŒฑ JWT - Login/Logout

Dohyeon Kongยท2024๋…„ 7์›” 10์ผ
0

Spring๐ŸŒฑ

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

ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ์ด๋ž€?

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

ํ† ํฐ์„ ์ „๋‹ฌํ•˜๊ณ  ์ธ์ฆ๋ฐ›๋Š”๊ณผ์ •

  1. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ID์™€ Password๋ฅผ ์„œ๋ฒ„์—๊ฒŒ ์ „๋‹ฌํ•˜๋ฉด์„œ ์ธ์ฆ์„ ์š”์ฒญํ•œ๋‹ค.
  2. ์„œ๋ฒ„๋Š” ID์™€ Password๋ฅผ ํ™•์ธํ•ด ์œ ํšจํ•œ ์‚ฌ์šฉ์ž์ธ์ง€ ๊ฒ€์ฆํ•œ๋‹ค. ๋งŒ์•ฝ ์œ ํšจํ•œ ์‚ฌ์šฉ์ž๋ฉด ํ† ํฐ(Access token, Refresh token)์„ ์ƒ์„ฑํ•˜์—ฌ ์‘๋‹ตํ•œ๋‹ค.
  3. ํด๋ผ์ด์–ธํŠธ๋Š” ์„œ๋ฒ„์—์„œ ์ค€ ํ† ํฐ์„ ์ €์žฅํ•œ๋‹ค.
  4. ์ดํ›„ ์ธ์ฆ์ด ํ•„์š”ํ•œ API๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ํ† ํฐ(Access token)์„ ํ•จ๊ป˜ ๋ณด๋‚ธ๋‹ค.
  5. ์„œ๋ฒ„๋Š” ํ† ํฐ์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•œ๋‹ค.
  6. ํ† ํฐ์ด ์œ ํšจํ•œ๋‹ค๋ฉด ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์š”์ฒญํ•œ ๋‚ด์šฉ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.

ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ์˜ ํŠน์ง•

  • ๋ฌด์ƒํƒœ์„ฑ
  • ํ™•์žฅ์„ฑ
  • ๋ฌด๊ฒฐ์„ฑ

JWT

aaaaa(ํ—ค๋”) . bbbbb(๋‚ด์šฉ) . cccccc(์„œ๋ช…)

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

ํ—ค๋”(Header)

  • ํ—ค๋”(Header)๋Š” ํ† ํฐ์˜ ํƒ€์ž…๊ณผ ํ•ด์‹ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์ง€์ •ํ•˜๋Š” ์ •๋ณด๋ฅผ ๋‹ด๋Š”๋‹ค.
  • JWT ํ† ํฐ, HS256 ํ•ด์‹ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‚ฌ์šฉํ•œ๋‹ค๋Š” ๋‚ด์šฉ์ด๋‹ค.
// ํ† ํฐ ํƒ€์ž…๊ณผ ํ•ด์‹ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์˜ˆ
{
 	"typ" : "JWT",
	"alg" : "HS256" 
}

ํ—ค๋”(Header)์˜ ๊ตฌ์„ฑ

์ด๋ฆ„์„ค๋ช…
typํ† ํฐ์˜ ํƒ€์ž…์„ ์ง€์ •ํ•œ๋‹ค. JWT๋ผ๋Š” ๋ฌธ์ž์—ด์ด ๋“ค์–ด๊ฐ€๊ฒŒ ๋œ๋‹ค.
algํ•ด์‹ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์ง€์ •ํ•œ๋‹ค.

๋‚ด์šฉ(Payload)

  • ํ† ํฐ๊ณผ ๊ด€๋ จ๋œ ์ •๋ณด๋ฅผ ๋‹ด๋Š”๋‹ค.
  • ๋‚ด์šฉ์˜ ํ•œ ๋ฉ์–ด๋ฆฌ๋ฅผ ํด๋ ˆ์ž„(Claim)์ด๋ผ๊ณ  ๋ถ€๋ฅด๋ฉฐ, ํด๋ ˆ์ž„์€ ํ‚ค๊ฐ’์˜ ํ•œ ์Œ์œผ๋กœ ์ด๋ฃจ์–ด์ ธ ์žˆ๋‹ค.

ํด๋ ˆ์ž„(Claim)

  • ๋“ฑ๋ก๋œ ํด๋ ˆ์ž„, ๊ณต๊ฐœ ํด๋ ˆ์ž„, ๋น„๊ณต๊ฐœ ํด๋ ˆ์ž„์œผ๋กœ ๋‚˜๋ˆˆ๋‹ค.

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

๋“ฑ๋ก๋œ ํด๋ ˆ์ž„์€ ํ† ํฐ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๋‹ด๋Š”๋ฐ ์‚ฌ์šฉํ•œ๋‹ค.

์ด๋ฆ„์„ค๋ช…
issํ† ํฐ ๋ฐœ๊ธ‰์ž(issue)
subํ† ํฐ ์ œ๋ชฉ(subject)
audํ† ํฐ ๋Œ€์ƒ์ž(audience)
expํ† ํฐ์˜ ๋งŒ๋ฃŒ ์‹œ๊ฐ„(expiration), ์‹œ๊ฐ„์€ NumericDate ํ˜•์‹์œผ๋กœ ํ•˜๋ฉฐ, ํ•ญ์ƒ ํ˜„์žฌ ์‹œ๊ฐ„ ์ดํ›„๋กœ ์„ค์ •ํ•œ๋‹ค.
nbfํ† ํฐ์˜ ํ™œ์„ฑ ๋‚ ์งœ์™€ ๋น„์Šทํ•œ ๊ฐœ๋…์œผ๋กœ, nbf๋Š” Not Before๋ฅผ ์˜๋ฏธํ•œ๋‹ค. NumericDate ํ˜•์‹์œผ๋กœ ๋‚ ์งœ๋ฅผ ์ง€์ •ํ•˜๋ฉฐ, ์ด ๋‚ ์งœ๊ฐ€ ์ง€๋‚˜๊ธฐ ์ „๊นŒ์ง€๋Š” ํ† ํฐ์ด ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š๋Š”๋‹ค.
iatํ† ํฐ์ด ๋ฐœ๊ธ‰๋œ ์‹œ๊ฐ„์œผ๋กœ iat์€ issued at์„ ์˜๋ฏธํ•œ๋‹ค.
jtijwt์˜ ๊ณ ์œ  ์‹๋ณ„์ž๋กœ์„œ ์ฃผ๋กœ ์ผํšŒ์šฉ ํ† ํฐ์— ์‚ฌ์šฉํ•œ๋‹ค.

๊ณต๊ฐœ ํด๋ ˆ์ž„(public claim)

๊ณต๊ฐœ ํด๋ ˆ์ž„ ์€ ๊ณต๊ฐœ๋˜์–ด๋„ ์ƒ๊ด€์—†๋Š” ํด๋ ˆ์ž„์„ ์˜๋ฏธํ•œ๋‹ค.

  • ์ถฉ๋Œ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋Š” ์ด๋ฆ„์„ ๊ฐ€์ ธ์•ผ ํ•œ๋‹ค.
  • ๋ณดํ†ต ํด๋ ˆ์ž„ ์ด๋ฆ„์„ URI๋กœ ์ง“๋Š”๋‹ค.

๋น„๊ณต๊ฐœ ํด๋ ˆ์ž„(private claim)

๋น„๊ณต๊ฐœ ํด๋ ˆ์ž„ ์€ ๊ณต๊ฐœ๋˜๋ฉด ์•ˆ๋˜๋Š” ํด๋ ˆ์ž„์„ ์˜๋ฏธํ•œ๋‹ค.

  • ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๊ฐ„์˜ ํ†ต์‹ ์— ์‚ฌ์šฉ๋œ๋‹ค.
{
	"iss" : "kkongdo@gmail.com", // ๋“ฑ๋ก๋œ ํด๋ ˆ์ž„
  	"iat" : 1622370878, // ๋“ฑ๋ก๋œ ํด๋ ˆ์ž„
	"exp" : 1622372678, // ๋“ฑ๋ก๋œ ํด๋ ˆ์ž„
	"https://kkongdo.com/jwt_claims/is_admin" : true,
  	"email" : "kkongdo@gmail.com",
	"hello" : "์•ˆ๋…•ํ•˜์„ธ์š”!"
}

iss, iat, exp๋Š” JWT ์ž์ฒด์—์„œ ๋“ฑ๋ก๋œ ํด๋ ˆ์ž„์ด๊ณ , URL๋กœ ๋„ค์ด๋ฐ๋œ https://kkongdo.com/jwt_claims/is_admin์€ ๊ณต๊ฐœ ํด๋ ˆ์ž„์ด๋‹ค. ๊ทธ ์™ธ์— ๋“ฑ๋ก๋œ ํด๋ ˆ์ž„๋„, ๊ณต๊ฐœ ํด๋ ˆ์ž„๋„ ์•„๋‹Œ email๊ณผ hello๋Š” ๋น„๊ณต๊ฐœ ํด๋ ˆ์ž„ ๊ฐ’์ด๋‹ค.


์„œ๋ช…(Signature)

์„œ๋ช…(Signature)๋Š” ํ•ด๋‹น ํ† ํฐ์ด ์กฐ์ž‘๋˜์—ˆ๊ฑฐ๋‚˜ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์•˜์Œ์„ ํ™•์ธํ•˜๋Š” ์šฉ๋„๋กœ ์‚ฌ์šฉํ•˜๋ฉฐ, ํ—ค๋”(Header)์˜ ์ธ์ฝ”๋”ฉ ๊ฐ’๊ณผ ๋‚ด์šฉ(Payload)์˜ ์ธ์ฝ”ํŒ…๊ฐ’์„ ํ•ฉ์นœ ํ›„์— ์ฃผ์–ด์ง„ ๋น„๋ฐ€ํ‚ค๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ•ด์‹œ๊ฐ’์„ ์ƒ์„ฑํ•œ๋‹ค.


๋ฆฌํ”„๋ ˆ์‰ฌ ํ† ํฐ(Refresh token)

๋ฆฌํ”„๋ ˆ์‰ฌ ํ† ํฐ(Refresh token)์€ ์‚ฌ์šฉ์ž๋ฅผ ์ธ์ฆํ•˜๊ธฐ ์œ„ํ•œ ์šฉ๋„๊ฐ€ ์•„๋‹Œ ์•ก์„ธ์Šค ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์„ ๋•Œ ์ƒˆ๋กœ์šด ์•ก์„ธ์Šค ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ํ† ํฐ์ด๋‹ค.

๋‹ค์Œ ์ ˆ์ฐจ๋ฅผ ์ฐจ๊ทผํžˆ ๋”ฐ๋ผ์™€ ๋ณด์ž๐Ÿฆพ

  • 1๋ฒˆ : ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„์—๊ฒŒ ์ธ์ฆ์„ ์š”์ฒญํ•œ๋‹ค.
  • 2๋ฒˆ : ์„œ๋ฒ„๋Š” ํด๋ผ์ด์–ธํŠธ์—์„œ ์ „๋‹ฌํ•œ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ธ์ฆ ์ •๋ณด๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธํ•œ ๋’ค, ์•ก์„ธ์Šค ํ† ํฐ๊ณผ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ๋งŒ๋“ค์–ด์„œ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „๋‹ฌํ•œ๋‹ค. ์ „๋‹ฌ๋ฐ›์€ ํด๋ผ์ด์–ธํŠธ๋Š” ์ €์žฅํ•œ๋‹ค.
  • 3๋ฒˆ : ์„œ๋ฒ„์—์„œ ์ƒ์„ฑํ•œ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์€ DB์—๋„ ์ €์žฅํ•ด๋‘”๋‹ค.
  • 4๋ฒˆ : ์ธ์ฆ์„ ํ•„์š”๋กœ ํ•˜๋Š” API๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ ํด๋ผ์ด์–ธํŠธ์— ์ €์žฅ๋œ ์•ก์„ธ์Šค ํ† ํฐ๊ณผ ํ•จ๊ป˜ API๋ฅผ ์š”์ฒญํ•œ๋‹ค.
  • 5๋ฒˆ : ์„œ๋ฒ„๋Š” ์ „๋‹ฌ๋ฐ›์€ ์•ก์„ธ์Šค ํ† ํฐ์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์‚ฌํ•œ ๋’ค์— ์œ ํšจํ•˜๋‹ค๋ฉด ํด๋ผ์ด์–ธํŠธ์—์„œ ์š”์ฒญํ•œ ๋‚ด์šฉ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.
  • 6๋ฒˆ : ์‹œ๊ฐ„์ด ์ง€๋‚˜๊ณ  ์•ก์„ธ์Šค ํ† ํฐ์ด ๋งŒ๋ฃŒ๋œ ๋’ค์—๋Š” ํด๋ผ๋ฆฌ์–ธํŠธ์—์„œ ์›ํ•˜๋Š” ์ •๋ณด๋ฅผ ์–ป๊ธฐ ์œ„ํ•ด ์„œ๋ฒ„์—๊ฒŒ API ์š”์ฒญ์„ ๋ณด๋‚ธ๋‹ค.
  • 7๋ฒˆ : ์„œ๋ฒ„๋Š” ์•ก์„ธ์Šค ํ† ํฐ์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์‚ฌํ•œ๋‹ค. ๋งŒ์•ฝ ๋งŒ๋ฃŒ๋œ ํ† ํฐ์ด๋ฉด ์œ ํšจํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ๋‹ค๋Š” ์—๋Ÿฌ๋ฅผ ์ „๋‹ฌํ•œ๋‹ค.
  • 8๋ฒˆ : ํด๋ผ๋ฆฌ์–ธํŠธ๋Š” ์ด ์‘๋‹ต์„ ๋ฐ›๊ณ  ์ €์žฅํ•ด๋‘” ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ๊ณผ ํ•จ๊ป˜ ์ƒˆ๋กœ์šด ์•ก์„ธ์Šค ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•˜๋Š” ์š”์ฒญ์„ ์ „์†กํ•œ๋‹ค.
  • 9๋ฒˆ : ์„œ๋ฒ„๋Š” ์ „๋‹ฌ๋ฐ›์€ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ด ์œ ํšจํ•œ์ง€, DB์—์„œ ๋ฆฌํ”„์„ธ๋ฆฌ ํ† ํฐ์„ ์กฐํšŒํ•œ ํ›„ ์ €์žฅํ•ด๋‘” ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ๊ณผ ๊ฐ™์€์ง€ ํ™•์ธํ•œ๋‹ค.
  • 10๋ฒˆ : ์œ ํšจํ•œ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ด๋ผ๋ฉด ์ƒˆ๋กœ์šด ์•ก์„ธ์Šค ํ† ํฐ์„ ์ƒ์„ฑํ•œ ๋’ค์— ์‘๋‹ตํ•œ๋‹ค. ๊ทธ ์ดํ›„ ํด๋ผ์ด์–ธํŠธ๋Š” 4๋ฒˆ๊ณผ ๊ฐ™์ด ๋‹ค์‹œ API๋ฅผ ์š”์ฒญํ•œ๋‹ค.

๊ตฌํ˜„ ๋ฐฉ๋ฒ•

1. ์˜์กด์„ฑ ์ถ”๊ฐ€ํ•˜๊ธฐ (build.gradle)

	// jwt ์„ค์ •ํ•˜๊ธฐ ์œ„ํ•œ dependencies
	implementation 'io.jsonwebtoken:jjwt:0.9.1'
	implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
	runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
	runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
	runtimeOnly 'io.jsonwebtoken:jjwt-gson:0.11.5'

2. ํ† ํฐ ์ œ๊ณต์ž ์ถ”๊ฐ€ํ•˜๊ธฐ

2-1. application.properties์— ์ฝ”๋“œ ์ถ”๊ฐ€ํ•˜๊ธฐ

jwt.issuer=kkongdo@gmail.com
jwt.secret_key=study-springboot

2-2 JwtProperties ์ƒ์„ฑ (ํ•ด๋‹น ๊ฐ’๋“ค์„ ๋ณ€์ˆ˜๋กœ ์ ‘๊ทผํ•˜๋Š”๋ฐ ์‚ฌ์šฉ)

package com.springboot.springsecurity.config.jwt;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Setter
@Getter
@Component
@ConfigurationProperties("jwt") // ์ž๋ฐ” ํด๋ž˜์Šค์— ํ”„๋กœํผํ‹ฐ๊ฐ’์„ ๊ฐ€์ ธ์™€์„œ ์‚ฌ์šฉํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜์ด๋‹ค.
public class JwtProperties {
    private String issuer; // kkongdo@gmail.com์ด ๋“ค์–ด๊ฐ„๋‹ค.
    private String secretKey; // study-springboot๊ฐ€ ๋“ค์–ด๊ฐ„๋‹ค.
}

2-3 TokenProvider ์ƒ์„ฑ (ํ† ํฐ์„ ์ƒ์„ฑํ•˜๊ณ  ์˜ฌ๋ฐ”๋ฅธ ํ† ํฐ์ธ์ง€ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ฐ ํ† ํฐ์—์„œ ํ•„์š”ํ•œ ์ •๋ณด ์ถ”์ถœ)

package com.springboot.springsecurity.config.jwt;

import io.jsonwebtoken.*;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import com.springboot.springsecurity.domain.User;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.Collections;
import java.util.Date;
import java.util.Set;

// ํ† ํฐ์„ ์ƒ์„ฑํ•˜๊ณ  ์˜ฌ๋ฐ”๋ฅธ ํ† ํฐ์ธ์ง€ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ํ•˜๊ณ , ํ† ํฐ์—์„œ ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ํด๋ž˜์Šค
@RequiredArgsConstructor
@Service
public class TokenProvider {
    private final JwtProperties jwtProperties;

    public String generateToken(User user, Duration expiredAt){
        Date now = new Date();
        return makeToken(new Date(now.getTime() + expiredAt.toMillis()), user);
    }

    // 1. JWT ํ† ํฐ ์ƒ์„ฑ ๋ฉ”์„œ๋“œ
    private String makeToken(Date expiry, User user){
        Date now = new Date();

        return Jwts.builder()
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
                .setIssuer(jwtProperties.getIssuer()) // ๋ฐœ๊ธ‰์ž
                .setIssuedAt(now) //๋ฐœ๊ธ‰๋‚ ์งœ
                .setExpiration(expiry) // ๋งŒ๋ฃŒ์ผ
                .setSubject(user.getEmail()) // ํ† ํฐ ์ œ๋ชฉ
                .claim("id", user.getId()) // ํด๋ ˆ์ž„์— ์œ ์ € id์ €์žฅ
                .signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
                .compact();
    }
    // 2. JWT ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๋ฉ”์„œ๋“œ
    public boolean validToken(String token){
        try{
            Jwts.parser()
                    .setSigningKey(jwtProperties.getSecretKey()) //๋น„๋ฐ€ํ‚ค๋กœ ๋ณตํ˜ธํ™”
                    .parseClaimsJws(token);
            return true;
        }catch (Exception e){
            return false;
        }
    }
    // 3. ํ† ํฐ ๊ธฐ๋ฐ˜์œผ๋กœ ์ธ์ฆ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ
    public Authentication getAuthentication(String token){
        Claims claims = getClaims(token);
        Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));
        return new UsernamePasswordAuthenticationToken(new org.springframework.security.core.userdetails.User(
                claims.getSubject(), "", authorities),token, authorities);
    }
    
    // 4. ํ† ํฐ ๊ธฐ๋ฐ˜์œผ๋กœ ์œ ์ € ID๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ
    public Long getUserId(String token){
        Claims claims = getClaims(token);
        return claims.get("id",Long.class);
    }

    private Claims getClaims(String token) {
        return Jwts.parser() // ํด๋ ˆ์ž„ ์กฐํšŒ
                .setSigningKey(jwtProperties.getSecretKey())
                .parseClaimsJws(token)
                .getBody();
    }


}

3. ๋ฆฌํ”„๋ ˆ์‰ฌ ํ† ํฐ ๋„๋ฉ”์ธ ์ƒ์„ฑ

package com.springboot.springsecurity.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class RefreshToken {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private Long id;
    @Column(name = "user_id", nullable = false, unique = true)
    private Long userId;
    @Column(name = "refresh_token", nullable = false)
    private String refreshToken;

    public RefreshToken(Long userId, String refreshToken){
        this.userId = userId;
        this.refreshToken = refreshToken;
    }
    public RefreshToken update(String newRefreshToken){
        this.refreshToken = newRefreshToken;
        return this;
    }
}

4. ํ† ํฐ ํ•„ํ„ฐ ์ƒ์„ฑ

package com.springboot.springsecurity.config;

import com.springboot.springsecurity.config.jwt.TokenProvider;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    private final TokenProvider tokenProvider;
    private final static String HEADER_AUTHORIZATION = "Authorization";
    private final static String TOKEN_PREFIX = "Bearer";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
		// ์š”์ฒญ ํ—ค๋”์˜ Authorization ํ‚ค์™€ ๊ฐ’์„ ์กฐํšŒํ•œ๋‹ค.
        String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);
		// ๊ฐ€์ ธ์˜จ ๊ฐ’์—์„œ ์ ‘๋‘์‚ฌ ์ œ๊ฑฐํ•œ๋‹ค.
        String token = getAccessToken(authorizationHeader);
		// ๊ฐ€์ ธ์˜จ ํ† ํฐ์ด ์œ ํšจํ•œ์ง€ ํ™•์ธ ๋ฐ ์œ ํšจํ•œ ๋•Œ๋Š” ์ธ์ฆ ์ •๋ณด ์„ค์ •์„ ํ•œ๋‹ค.
        if(tokenProvider.validToken(token)){
            Authentication authentication = tokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }

    private String getAccessToken(String authorizationHeader) {
        if(authorizationHeader != null && authorizationHeader.startsWith(TOKEN_PREFIX)){
            return authorizationHeader.substring(TOKEN_PREFIX.length());
        }
        return null;
    }
}

5. ํ† ํฐ API ๊ตฌํ˜„

  • ๋ฆฌํ”„๋ ˆ์‰ฌ ํ† ํฐ์„ ์ „๋‹ฌ๋ฐ›์•„ ๊ฒ€์ฆํ•˜๊ณ , ์œ ํšจํ•œ ๋ฆฌํ”„์„ธ์‹œ ํ† ํฐ์ด๋ผ๋ฉด ์ƒˆ๋กœ์šด ์•ก์„ธ์Šค ํ† ํฐ์„ ์ƒ์„ฑํ•˜๋Š” ํ† ํฐ API๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด๊ฒ ๋‹ค.

5-1. UserService์— ์œ ์ € ID๋กœ ์œ ์ €๋ฅผ ๊ฒ€์ƒ‰ํ•ด์„œ ์ „๋‹ฌํ•˜๋Š” findById() ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€

package com.springboot.springsecurity.service;

import com.springboot.springsecurity.domain.User;
import com.springboot.springsecurity.dto.AddUserRequest;
import com.springboot.springsecurity.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public Long save(AddUserRequest dto) {
        return userRepository.save(User.builder()
                .email(dto.getEmail())
                // ํŒจ์Šค์›Œ๋“œ ์•”ํ˜ธํ™”
                .password(bCryptPasswordEncoder.encode(dto.getPassword()))
                .build()).getId();
    }
	
    public User findById(Long userId) {
        return userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("UnExpected user"));
    }
}

5-2. RefreshTokenService ์ƒ์„ฑ

  • ์ „๋‹ฌ๋ฐ›์€ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์œผ๋กœ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ๊ฐ์ฒด๋ฅผ ๊ฒ€์ƒ‰ํ•ด์„œ ์ „๋‹ฌํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ๋งŒ๋“ ๋‹ค.
package com.springboot.springsecurity.service;

import com.springboot.springsecurity.domain.RefreshToken;
import com.springboot.springsecurity.repository.RefreshTokenRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class RefreshTokenService {
    private final RefreshTokenRepository refreshTokenRepository;

    public RefreshToken findByRefreshToken(String refreshToken) {
        return refreshTokenRepository.findByRefreshToken(refreshToken).orElseThrow(() -> new IllegalArgumentException("Unexpected toekn"));
    }
}

5-3. TokenService ์ƒ์„ฑ

  • createNewAccessToken() ๋ฉ”์„œ๋“œ๋ฅผ ๋งŒ๋“ค์–ด์„œ ์ „๋‹ฌ๋ฐ›์€ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์œผ๋กœ ํ† ํฐ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•˜๊ณ  ์œ ํšจํ•œ ํ† ํฐ์ผ ๋•Œ๋Š” ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์œผ๋กœ ์‚ฌ์šฉ์ž ID๋ฅผ ์ฐพ๋Š”๋‹ค.
  • ์ดํ›„ ์‚ฌ์šฉ์ž ID๋กœ ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์€ ํ›„์— ํ† ํฐ ์ œ๊ณต์ž generateToken() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ด์„œ ์ƒˆ๋กœ์šด ์•ก์„ธ์Šค ํ† ํฐ์„ ์ƒ์„ฑํ•œ๋‹ค.
package com.springboot.springsecurity.service;

import com.springboot.springsecurity.config.jwt.TokenProvider;
import com.springboot.springsecurity.domain.User;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.time.Duration;

@Service
@RequiredArgsConstructor
public class TokenService {
    private final TokenProvider tokenProvider;
    private final RefreshTokenService refreshTokenService;
    private final UserService userService;

    public String createNewAccessToken(String refreshToken) throws IllegalAccessException {
        // ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์— ์‹คํŒจํ•˜๋ฉด ์˜ˆ์™ธ ๋ฐœ์ƒ
        if(!tokenProvider.validToken(refreshToken)){
            throw new IllegalAccessException("Unexpected token");
        }
        Long userId = refreshTokenService.findByRefreshToken(refreshToken).getUserId();
        User user = userService.findById(userId);

        return  tokenProvider.generateToken(user, Duration.ofHours(2));
    }
}

6. ํ† ํฐ์„ ๋ฐœ๊ธ‰๋ฐ›๋Š” API ์ƒ์„ฑ

6-1. ํ† ํฐ ์ƒ์„ฑ ์š”์ฒญ CreateAccessTokenRequest์™€ ํ† ํฐ ์ƒ์„ฑ ์‘๋‹ต CreateAccessTokenResponse๋ฅผ ๋งŒ๋“ ๋‹ค.

package com.springboot.springsecurity.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class CreateAccessTokenRequest {
    private String refreshToken;
}
package com.springboot.springsecurity.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class CreateAccessTokenResponse {
    private String accessToken;
}

6-2. TokenApiController ๊ตฌํ˜„

  • Post ์š”์ฒญ์ด ์˜ค๋ฉด ํ† ํฐ ์„œ๋น„์Šค์—์„œ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ƒˆ๋กœ์šด ์•ก์„ธ์Šค ํ† ํฐ์„ ๋งŒ๋“ค์–ด์ค€๋‹ค.
package com.springboot.springsecurity.controller;

import com.springboot.springsecurity.dto.CreateAccessTokenRequest;
import com.springboot.springsecurity.dto.CreateAccessTokenResponse;
import com.springboot.springsecurity.service.TokenService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class TokenApiController {
    private final TokenService tokenService;

    @PostMapping("/api/token")
    public ResponseEntity<CreateAccessTokenResponse> createNewAccessToken(@RequestBody CreateAccessTokenRequest request) throws IllegalAccessException {
        String newAccessToken = tokenService.createNewAccessToken(request.getRefreshToken());
        return ResponseEntity.status(HttpStatus.CREATED).body(new CreateAccessTokenResponse(newAccessToken));
    }
}

๊ทธ๋Ÿผ ์–ด๋””์— JWT๋Š” ์–ด๋””์—์„œ ์–ธ์ œ ์‚ฌ์šฉ์ด ๋ ๊นŒ?๐Ÿง

JWT(JSON Web Token)๋Š” ์ฃผ๋กœ ์ธ์ฆ ๋ฐ ์ •๋ณด ๊ตํ™˜ ๋ชฉ์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ํ† ํฐ ๊ธฐ๋ฐ˜์˜ ์ธ์ฆ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด๊ธฐ ๋•Œ๋ฌธ์— ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๊ฐ„์˜ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ์ •๋ณด ๊ตํ™˜์„ ์œ„ํ•ด ์‚ฌ์šฉ๋œ๋‹ค.

์‚ฌ์šฉ์ž ์ธ์ฆ (Authentication)

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

๊ถŒํ•œ ๋ถ€์—ฌ (Authorization)

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

์ •๋ณด ๊ตํ™˜

JWT๋Š” ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ์ •๋ณด ๊ตํ™˜์„ ์œ„ํ•ด ์‚ฌ์šฉ๋œ๋‹ค.
๊ทธ๋Ÿฌ๋ฏ€๋กœ ์„œ๋ฒ„ ๊ฐ„์— ์‚ฌ์šฉ์ž ์ •๋ณด๋‚˜ ์„ค์ • ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ  ๋ฐ›์„ ๋•Œ JWT๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฐ์ดํ„ฐ์˜ ๋ฌด๊ฒฐ์„ฑ์„ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ๋‹ค.


๊ฒฐ๋ก 

JWT๋Š” ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ์ •๋ณด ๊ตํ™˜์„ ์œ„ํ•ด ์‚ฌ์šฉ๋œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์„œ๋ฒ„ ๊ฐ„์— ์‚ฌ์šฉ์ž ์ •๋ณด๋‚˜ ์„ค์ • ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ  ๋ฐ›์„ ๋•Œ JWT๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฐ์ดํ„ฐ์˜ ๋ฌด๊ฒฐ์„ฑ์„ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™๋‹ค.


์ฐธ๊ณ ๋ฌธํ—Œ ๋ฐ ๊ต์žฌ๐Ÿ“’

https://joie-kim.github.io/JWT-Auth/
์Šคํ”„๋ง ๋ถ€ํŠธ 3 ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž ๋˜๊ธฐ: ์ž๋ฐ” ํŽธ - ์‹ ์„ ์˜ -

profile
์ฒœ์ฒœํžˆ, ๊พธ์ค€ํžˆ, ๊ทธ๋ฆฌ๊ณ  ๋๊นŒ์ง€

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

comment-user-thumbnail
2024๋…„ 7์›” 13์ผ

ํ•˜... ์ด๋ฉ”์ผ ๊ฐ€์ž… ๋”ฑ ๊ธฐ๋‹ค๋ ค ์ด๊ฑฐ ๋ณด๊ณ  ๊ตฌํ˜„ํ•œ๋‹ค.

1๊ฐœ์˜ ๋‹ต๊ธ€