๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
[Spring]/Spring Security

๋‹น์‹ ์˜ ์ฒซ ํ”„๋กœ์ ํŠธ๋ฅผ ์œ„ํ•œ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ(5) -JwtTokenizer์™€ Redis ํด๋ž˜์Šค ์ž‘์„ฑํ•˜๊ธฐ

by ํŒกํŽ‘ํ 2025. 3. 6.
728x90
๐Ÿ“Œ ์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ์ง€๋‚œ ์‹œ๊ฐ„์— ์ด์–ด JwtTokenizer์™€ Redis ํด๋ž˜์Šค ์ƒ์„ฑํ•˜์—ฌ jwt ๋ฐœ๊ธ‰๊ณผ redis์— ํ† ํฐ์„ ์ €์žฅํ•˜๋Š” ๊ณผ์ •์— ๋Œ€ํ•œ ์ค€๋น„๋ฅผ ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

๐Ÿค— ์ €์˜ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ๊ตฌํ˜„์€ ์•„๋ž˜์™€ ๊ฐ™์€ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํ•ฉ๋‹ˆ๋‹ค.
- ํ”„๋ก ํŠธ ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ๊ฐ€ ๋‚˜๋‰˜์–ด ์ง„ํ–‰๋˜๋Š” ํ”„๋กœ์ ํŠธ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜์—ฌ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ์„ค์ •์—์„œ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€์— ๋Œ€ํ•œ ์„ค์ •์„ ๋”ฐ๋กœ ํ•˜์ง€ ์•Š์Œ
- JWT ํ† ํฐ ์ธ์ฆ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•จ
- ํ† ํฐ ๊ด€๋ฆฌ์— redis๋ฅผ ์ด์šฉํ•จ

 

๐Ÿ™‰ ์ด์ „ ๊ธ€ ๋ณด๊ธฐ

์ฒซ ๋ฒˆ์งธ ๊ธ€๋ถ€ํ„ฐ ์ •๋…ํ•˜์‹œ๋ฉด ๋ณด๋‹ค ์‰ฝ๊ฒŒ ์ดํ•ดํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!

https://suzuworld.tistory.com/438 - ๋‹น์‹ ์˜ ์ฒซ ํ”„๋กœ์ ํŠธ๋ฅผ ์œ„ํ•œ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ํ†บ์•„๋ณด๊ธฐ

์ด์ „ ๊ธ€

https://suzuworld.tistory.com/442 - ๋กœ๊ทธ์ธ ํ…Œ์ŠคํŠธ ๋ฐ JWT, Redis ๊ฐœ๋… ์ •๋ฆฌ

 

๐Ÿ“– ๋ชฉ์ฐจ

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ํ†บ์•„๋ณด๊ธฐ

SecurityConfig ๊ตฌ์„ฑํ•˜๊ธฐ

์ธ์ฆ ๋ฐฉ์‹ ๊ฐœ๋…๊ณผ AuthenticationFilter 

AuthenticationManager, AuthenticationProvider, UserDetailsService, UserDetails

๋กœ๊ทธ์ธ ํ…Œ์ŠคํŠธ ๋ฐ JWT, redis ๊ฐœ๋… ์ •๋ฆฌ

JwtTokenizer์™€ Redis ํด๋ž˜์Šค ์ž‘์„ฑํ•˜๊ธฐ (ํ˜„์žฌ ๊ธ€)

 

์ง€๋‚œ ๊ธ€๊นŒ์ง€์˜ ๋‚ด์šฉ์„ ๋‹ค์‹œ ์ •๋ฆฌํ•ด ๋ด…์‹œ๋‹ค. ์šฐ๋ฆฌ๋Š” ๋กœ๊ทธ์ธ ๊ตฌํ˜„์— ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜, ์•„์ง๊นŒ์ง€๋Š” ๋กœ๊ทธ์ธ์„ ํ•œ๋‹ค๊ณ  ํ•ด์„œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์–ด๋– ํ•œ ๊ถŒํ•œ๋„ ๋ถ€์—ฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ง์ „ ๊ธ€์—์„œ JWT์™€ redis์— ๋Œ€ํ•ด ์„ค๋ช…ํ–ˆ์ฃ .

 

์ด์ œ ์šฐ๋ฆฌ๋Š” ์ง€๋‚œ ์‹œ๊ฐ„๊นŒ์ง€ ๊ตฌํ˜„ํ•œ ๋กœ๊ทธ์ธ ๋กœ์ง์— JWT์„ ๋ฐœ๊ธ‰ํ•˜๊ณ , redis์— ์ €์žฅํ•˜๋Š” ๋ถ€๋ถ„์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ํด๋ž˜์Šค๋ฅผ ์ž‘์„ฑํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

๐Ÿค‘ JWT

 JWT ๋ฐœ๊ธ‰์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์ „์— ํ† ํฐ์˜ ์ƒ์„ฑ๊ณผ ๊ฒ€์ฆ ๋ฐฉ์‹์— ๋Œ€ํ•ด ์ดํ•ดํ•ด ๋ด…์‹œ๋‹ค. JWT์— ๋Œ€ํ•œ ๊ธฐ๋ณธ์ ์ธ ๊ฐœ๋…์€ ์ง€๋‚œ ๊ธ€์„ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”. ์ง€๋‚œ ๊ธ€์—์„œ JWT์˜ ์˜ˆ์‹œ ๊ตฌ์กฐ๋ฅผ ๋ณด์…จ์ฃ ? ๋ฌด์Šจ ์ƒ๊ฐ์ด ๋“œ์…จ๋‚˜์š”? ๋ฌด์Šจ ์˜๋ฏธ ์—†๋Š” ๋ฌธ์ž์—ด ์•„๋‹Œ๊ฐ€ ์‹ถ์œผ์‹คํ…๋ฐ์š”. ์‚ฌ์‹ค ํŠน์ • ๊ทœ์น™์— ์˜ํ•ด ๋ณ€ํ˜•๋œ ๋ฌธ์ž์—ด์ž…๋‹ˆ๋‹ค. ์ •ํ™•ํžˆ ์„ค๋ช…ํ•˜๋ฉด JWT๋Š” Base64๋กœ ์ธ์ฝ”๋”ฉ๋œ ์ŠคํŠธ๋ง ๊ฐ’์— ๋ถˆ๊ณผํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ์ฃผ์˜ํ•  ์ ์€ Base64 ์ธ์ฝ”๋”ฉ ๋ฐฉ์‹์ด ์•”ํ˜ธํ™”์˜ ์ผ์ข…์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜์‹œ๋ฉด ์•ˆ๋œ๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค. Base64 ์ธ์ฝ”๋”ฉ์€ ๋ฐ์ดํ„ฐ๋ฅผ Base64์˜ ๊ทœ์น™์— ๋”ฐ๋ผ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•œ ๊ฒƒ ๋ฟ์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ JWT๋ฅผ ๋””์ฝ”๋”ฉํ•œ๋‹ค๋Š” ๊ฒƒ์€ ๋ณ€ํ™˜๋œ ๋ฌธ์ž์—ด์„ ์›๋ž˜์˜ JSON์œผ๋กœ ๋˜๋Œ๋ฆฌ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

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

 

๐Ÿ–‹๏ธ ์„œ๋ช… ์•Œ๊ณ ๋ฆฌ์ฆ˜?

 ๊ทธ๋Ÿผ ์—ฌ๊ธฐ์„œ ์˜๋ฌธ์ด ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ช… ์•Œ๊ณ ๋ฆฌ์ฆ˜์ด ์‚ฌ์šฉ๋œ๋‹ค๊ณ  ํ•˜๋Š”๋ฐ ๊ทธ๊ฑด ๋ฌด์Šจ ๋ง์ธ๊ฐ€? ์œ„ ๊ทธ๋ฆผ๊ณผ ๊ฐ™์ด JWT ์•ˆ์˜ ์œ ์ € ์ •๋ณด ๋“ฑ์€ payload์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค. ์ด payload์—๋Š” ํ•ด๋‹น ์œ ์ €์˜ ์ •๋ณด ๋“ฑ์ด ๋“ค์–ด์žˆ๋Š”๋ฐ ์œ„์— ์–ธ๊ธ‰ํ–ˆ๋“ฏ์ด JWT๋Š” ๋ˆ„๊ตฌ๋‚˜ ๋””์ฝ”๋”ฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ๋ˆ„๊ตฌ๋‚˜ ์ •๋ณด๋ฅผ ์กฐ์ž‘ํ•˜๊ณ  ๋‹ค์‹œ Base64๋กœ ์ธ์ฝ”๋”ฉํ•˜์—ฌ ์กฐ์ž‘๋œ JWT๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค๋Š” ์ด์•ผ๊ธฐ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ•ด๋‹น ์ •๋ณด๊ฐ€ ์กฐ์ž‘๋˜์ง€ ์•Š์•˜๋‹ค๋Š” ๊ฒƒ์„ ์ฆ๋ช…ํ•˜๊ธฐ ์œ„ํ•ด ์„œ๋ช… ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์ด์šฉํ•˜์—ฌ payload๋ฅผ ํ•ด์‹ฑํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•ด์‹œํ•œ ๊ฐ’์„ ์‹œ๊ทธ๋‹ˆ์ฒ˜์— ํฌํ•จ์‹œํ‚ค๊ณ , ์–ด๋–ค ํ•ด์‹œ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‚ฌ์šฉํ–ˆ๋Š”์ง€๋Š” header์— ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.

 

๐Ÿƒ๐Ÿฝ‍โ™‚๏ธ‍โžก๏ธ ์ƒ์„ฑ๋ถ€ํ„ฐ ๊ฒ€์ฆ ๊ณผ์ •

 ํ•ด์‹ฑ์„ ์œ„ํ•ด์„œ๋Š” ๋น„๋ฐ€ํ‚ค(secret key)๊ฐ€ ํ•„์š”ํ•œ๋ฐ์š”. ์„œ๋ฒ„์—์„œ๋Š” JWT๋ฅผ ๋งŒ๋“ค ๋•Œ ๋น„๋ฐ€ํ‚ค๋ฅผ ๋ฏธ๋ฆฌ ์ค€๋น„ํ•ด ๋‘๊ณ  ์ด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ payload๋ฅผ ํ•ด์‹ฑํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ ๋‚˜์˜จ ํ•ด์‹œ๊ฐ’์€ JWT์˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜์— ์ €์žฅ๋˜๋ฉฐ ํ•ด๋‹น JWT๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฐœ๊ธ‰ํ•ฉ๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ๋Š” ์ดํ›„ ์ด JWT๋ฅผ ์ธ์ฆ์ด ํ•„์š”ํ•œ ์š”์ฒญ์— ์‹ฃ์–ด ๋ณด๋ƒ…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ๋‘ ๊ฐ€์ง€ ์‹คํŒจ ๊ฒฝ์šฐ๊ฐ€ ๋‚˜์˜ต๋‹ˆ๋‹ค.

 

๐Ÿฅท๐Ÿป JWT๊ฐ€ ์กฐ์ž‘๋œ ๊ฒฝ์šฐ

โŒ payload ๊ฐ’์„ ์กฐ์ž‘

 JWT์˜ payload ๊ฐ’์„ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์กฐ์ž‘ํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ฉ์‹œ๋‹ค. ํด๋ผ์ด์–ธํŠธ๋Š” ์กฐ์ž‘๋œ payload๊ฐ’์œผ๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด ์„œ๋ฒ„์—์„œ ํ•ด๋‹น payload ๊ฐ’์„ ์„œ๋ฒ„์—์„œ ์ €์žฅ ์ค‘์ธ ๋น„๋ฐ€ํ‚ค๋กœ ํ•ด์‹œํ•˜์—ฌ ์‹œ๊ทธ๋‹ˆ์ฒ˜์— ์ ํžŒ ํ•ด์‹œ๊ฐ’๊ณผ ๋น„๊ตํ•˜์—ฌ payload๊ฐ€ ์กฐ์ž‘๋œ์ง€๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. ๋‹น์—ฐํžˆ ํ•ด์‹œ๊ฐ’์ด ๋‹ค๋ฅผํ…Œ๋‹ˆ ์ธ์ฆ์„ ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

โŒ payload ๊ฐ’์„ ์กฐ์ž‘ํ•˜๋ฉด์„œ ์‹œ๊ทธ๋‹ˆ์ฒ˜์— ์ €์žฅ๋œ ํ•ด์‹œ๊ฐ’ ์—ญ์‹œ ์กฐ์ž‘ํ•œ ๊ฒฝ์šฐ

 ํด๋ผ์ด์–ธํŠธ์—์„œ payload๋ฅผ ์กฐ์ž‘ํ•จ๊ณผ ๋™์‹œ์— ์ด๋ฅผ header์— ์žˆ๋Š” ํ•ด์‹œ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์ด์šฉํ•ด ํ•ด์‹ฑํ•˜๊ณ  ์ด ํ•ด์‹œ๊ฐ’๋„ ์‹œ๊ทธ๋‹ˆ์ฒ˜์— ์‹ฃ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜, ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํ•ด์‹œ๊ฐ’์„ ๋งŒ๋“ค ๋•Œ ์‚ฌ์šฉํ•œ ๋น„๋ฐ€ํ‚ค์™€ ์„œ๋ฒ„์—์„œ ๊ฒ€์ฆ์— ์‚ฌ์šฉํ•˜๋Š” ๋น„๋ฐ€ํ‚ค๊ฐ€ ๋‹ฌ๋ผ ํ•ด์‹œ๊ฐ’์ด ์ผ์น˜ํ•˜์ง€ ์•Š์„ ๊ฒƒ์ž„์œผ๋กœ ์ธ์ฆ์„ ํ†ต๊ณผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

 

โญ•๏ธ JWT๊ฐ€ ์กฐ์ž‘๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ

 ์„œ๋ฒ„์—์„œ ๊ฒ€์ฆํ•  ๋•Œ payload๋ฅผ ์„œ๋ฒ„์˜ ๋น„๋ฐ€ํ‚ค๋กœ ํ•ด์‹œํ•˜์—ฌ ๊ฐ’์„ ์‹œ๊ทธ๋‹ˆ์ฒ˜์˜ ํ•ด์‹œ๊ฐ’๊ณผ ๋น„๊ตํ•˜๋Š” ๊ณผ์ •์„ ๊ฑฐ์น˜๋Š”๋ฐ ์ด๋•Œ, ์‹œ๊ทธ๋‹ˆ์ฒ˜์˜ ํ•ด์‹œ๊ฐ’ ์—ญ์‹œ ์„œ๋ฒ„์˜ ๋น„๋ฐ€ํ‚ค๋กœ ๋งŒ๋“  ํ•ด์‹œ๊ฐ’์ผ ๊ฒƒ์ž„์œผ๋กœ ํ•ด์‹œ๊ฐ’์ด ์ผ์น˜ํ•ด ๊ฒ€์ฆ์ด ํ†ต๊ณผ๋ฉ๋‹ˆ๋‹ค.

 

๐Ÿ‘จ๐Ÿป‍๐Ÿญ JWT ๊ด€๋ฆฌ

 ์ด์ œ ์‹ค๋ฌด์—์„œ ์ ์šฉํ•  ๋•Œ ๊ณ ๋ คํ•˜๋Š” ์‚ฌํ•ญ์„ ์ถ”๊ฐ€๋กœ ์ƒ๊ฐํ•ด๋ด…๋‹ˆ๋‹ค. ์„œ๋ฒ„์—์„œ๋Š” JWT์˜ ์žฌ๋ฐœ๊ธ‰์— ๋Œ€ํ•ด์„œ๋„ ๊ณ ๋ คํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ๊ฐ™์€ JWT๋ฅผ ๋ฌดํ•œํžˆ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๋งŒ์•ฝ ํด๋ผ์ด์–ธํŠธ์˜ ๊ด€๋ฆฌ ์‹ค์ˆ˜๋กœ JWT๊ฐ€ ํƒˆ์ทจ๋œ๋‹ค๋ฉด ์ด๋Š” ๋‹ค๋ฅธ ์‚ฌ๋žŒ์— ์˜ํ•ด ์•…์šฉ๋  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ผ๋ฐ˜์ ์œผ๋กœ JWT์˜ ์œ ํšจ์‹œ๊ฐ„์€ ๊ต‰์žฅํžˆ ์งง๊ฒŒ ๊ฐ€์ ธ๊ฐ‘๋‹ˆ๋‹ค. 5-10๋ถ„์ •๋„์ฃ . ๊ทธ๋ฆฌ๊ณ  ์ด ์œ ํšจ์‹œ๊ฐ„์ด ๋‹ค ๋˜๋ฉด ์žฌ๋ฐœ๊ธ‰์„ ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด ์ž‘์—…์„ ํšจ์œจ์ ์œผ๋กœ ํ•˜๊ธฐ ์œ„ํ•ด AccessToken๊ณผ RefreshToken ๋‘˜๋กœ ๋‚˜๋‰˜์–ด ๊ด€๋ฆฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

๐Ÿ’งํ† ํฐ์˜ ์ƒ๋ช…์ฃผ๊ธฐ

 ์„œ๋ฒ„๋Š” ์ตœ์ดˆ์— ๋‘ JWT(AccessToken, RefreshToken)๋ฅผ ๋ฐœ๊ธ‰ํ•ฉ๋‹ˆ๋‹ค. AccessToken์˜ ๊ฒฝ์šฐ ์œ ํšจ ์‹œ๊ฐ„์ด ๋งค์šฐ ์งง์Šต๋‹ˆ๋‹ค. RefreshToken์˜ ๊ฒฝ์šฐ AccessToken์— ๋น„ํ•ด ์œ ํšจ ์‹œ๊ฐ„์ด ๊น๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ๋Š” ๋‘ ํ† ํฐ์„ ์ด์šฉํ•ด ์ธ์ฆ์ด ํ•„์š”ํ•œ ์š”์ฒญ์„ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

 

 ์ด๋•Œ AccessToken์˜ ์œ ํšจ ์‹œ๊ฐ„์ด ์ „๋ถ€ ๋๋‚˜๋ฒ„๋ฆฐ๋‹ค๋ฉด? ํด๋ผ์ด์–ธํŠธ๋Š” ๊ฐ€์ง€๊ณ  ์žˆ๋Š” RefreshToken์„ ์ด์šฉํ•˜์—ฌ ์„œ๋ฒ„์—๊ฒŒ AccessToken์˜ ์žฌ๋ฐœ๊ธ‰์„ ์š”์ฒญํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„์—์„œ๋Š” RefreshToken์„ ๊ฒ€์ฆํ•˜์—ฌ ํ†ต๊ณผ๋˜๋ฉด AccessToken์„ ์žฌ๋ฐœ๊ธ‰ํ•ด์ค๋‹ˆ๋‹ค.

 

๋งŒ์•ฝ RefreshToken๋งˆ์ € ์œ ํšจ ์‹œ๊ฐ„์ด ๋๋‚˜๋ฉด ์žฌ๋กœ๊ทธ์ธ์„ ์ง„ํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

RefreshToken Rotation(RTR)

 ์ €๋Š” ๋ณด๋‹ค ๋ณด์•ˆ์ ์ธ ์„ค๊ณ„๋ฅผ ์œ„ํ•จ๊ณผ ์ž๋™ ๋กœ๊ทธ์ธ์˜ ๋ถˆํ•„์š”ํ•œ ์š”์ฒญ์„ ์ค„์ด๊ธฐ ์œ„ํ•ด RTR ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•  ์ƒ๊ฐ์ž…๋‹ˆ๋‹ค. RefreshToken Rotation์ด๋ž€ AccessToken์ด ๋งŒ๋ฃŒ๋  ๊ฒฝ์šฐ, RefreshToken ๊ฒ€์ฆ์„ ํ†ตํ•ด AccessToken๋งŒ ์žฌ๋ฐœ๊ธ‰ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹Œ RefreshToken ์—ญ์‹œ ์ƒˆ๋กœ ๋ฐœ๊ธ‰ํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ์ด ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ ๋‘ ๊ฐ€์ง€ ์žฅ์ ์„ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

1. RefreshToken ์—ญ์‹œ ๊ณ„์† ๊ฐฑ์‹ ๋˜๋ฉฐ ๋ณด์•ˆ์— ์ข€ ๋” ์œ ๋ฆฌํ•ด์ง„๋‹ค.

2. ๋งŒ์ผ ์‚ฌ์šฉ์ž๊ฐ€ ๊ณ„์† ์ ‘์†ํ•˜์—ฌ ํ™œ๋™์ค‘์ด๋ผ๋ฉด ์ธ์ฆ์ด ํ•„์š”ํ•œ ์š”์ฒญ์ด ๊ณ„์†๋  ๊ฒƒ์ด๊ณ , ์ด ์š”์ฒญ์œผ๋กœ RefreshToken์˜ ์œ ํšจ์‹œ๊ฐ„์ด ๋๋‚˜๊ธฐ ์ „์— ์ง€์†์ ์ธ ์žฌ๋ฐœ๊ธ‰์ด ๋  ๊ฒƒ์ด๋ฏ€๋กœ ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ๊ธธ๊ฒŒ ์œ ์ง€ํ•˜๋„๋ก ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ฌผ๋ก , ๊ธˆ์œต ๋“ฑ ์ค‘์š” ๋ณด์•ˆ ๊ด€๋ จ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ฒฝ์šฐ ์žฌ๋กœ๊ทธ์ธ์„ ํ•˜๋„๋ก ๊ฐ•์ œํ•˜๋Š” ๊ฒƒ์ด ๋” ์•ˆ์ „ํ•œ ๋ฐฉ๋ฒ•์ผ ๊ฒƒ์ด๋‹ค.

 

๐Ÿง‘๐Ÿป‍๐Ÿ’ป ํด๋ž˜์Šค ์ž‘์„ฑํ•˜๊ธฐ

build.gradle ์˜์กด์„ฑ ์ถ”๊ฐ€

//์ถ”๊ฐ€!
// JJWT
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.12.6'	

// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
  • jjwt๋Š” ๊ฒฝ์šฐ ์ตœ์‹  ๋ฒ„์ „์ด ์‹œ๊ฐ„์ด ์ง€๋‚˜ ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt ์ด ๋งํฌ๋ฅผ ํ†ตํ•ด ๋ฒ„์ „์„ ํ™•์ธํ•˜๋ฉด๋ฉ๋‹ˆ๋‹ค.

 

JwtTokenizer ํด๋ž˜์Šค ์ถ”๊ฐ€

  • Jwt ๋ฐœ๊ธ‰๋ถ€ํ„ฐ ๊ฒ€์ฆ์„ ๋‹ด๋‹นํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ๋ชจ์•„๋†“์€ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
  • ์ฝ”๋“œ๋ฅผ ์œ„์—์„œ๋ถ€ํ„ฐ ์•„๋ž˜๋กœ ๋‚ด๋ ค๊ฐ€๋ฉฐ ๋ถ„์„ํ•ด๋ด…์‹œ๋‹ค.
#application.yml์— ์ž‘์„ฑ

jwt-secret-key: testsecretcodexxx...
access-token-expiration-minutes: 10 # accesstoken ์œ ํšจ ์‹œ๊ฐ„
refresh-token-expiration-minutes: 60 # refreshtoken ์œ ํšจ ์‹œ๊ฐ„

#############################################################    

import org.springframework.beans.factory.annotation.Value;

@Slf4j
@Getter
@Component
public class JwtTokenizer {

    @Value("${jwt-secret-key}")
    private String secretKey;

    @Value("${access-token-expiration-minutes}")
    private int accessTokenExpirationMinutes;

    @Value("${refresh-token-expiration-minutes}")
    private int refreshTokenExpirationMinutes;

	...

@Component

  • ์Šคํ”„๋ง์ด ์ด ์• ๋„ˆํ…Œ์ด์…˜์ด ๋ถ™์€ ํด๋ž˜์Šค๋ฅผ ์Šคํ”„๋ง ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.

@Value

  • application.yml ํŒŒ์ผ์˜ ๊ฐ’์„ ์ฝ์–ด์™€ ์ดˆ๊ธฐํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์˜ˆ๋ฅผ ๋“ค์–ด ์œ„ ์˜ˆ์‹œ์™€ ๊ฐ™์ด application.yml์— ์žˆ๋Š” ๊ฐ’์„ ๊ฐ€์ ธ์™€ ๊ฐ ๋ณ€์ˆ˜๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
    • jwt์—์„œ ์‚ฌ์šฉํ•  ๋น„๋ฐ€ํ‚ค(secret key)๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
    • access, refresh token์˜ ์œ ํšจ ์‹œ๊ฐ„์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
  • ์ด๋Ÿฐ ๋ณด์•ˆ ์ •๋ณด๊ฐ€ ๋“ค์–ด๊ฐ„ ์ˆœ๊ฐ„๋ถ€ํ„ฐ ๊นƒํ—ˆ๋ธŒ ๋“ฑ ์™ธ๋ถ€์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ๊ฒฝ๋กœ์— ๋ณด์•ˆ ์ •๋ณด๋ฅผ ์—…๋กœ๋“œํ•˜์ง€ ์•Š๋„๋ก ์ฃผ์˜ํ•ฉ์‹œ๋‹ค.
    • git์—์„œ ํด๋” ๋ฐ ํŒŒ์ผ์„ ignoreํ•˜๋„๋ก ์ถ”๊ฐ€ ํ•ฉ์‹œ๋‹ค.

 

generateAccessToken ๋ฉ”์„œ๋“œ

public String generateAccessToken(String email,
                                  Map<String, Object> claims,
                                  int expirationMinute,
                                  String secretKey) {

    return Jwts.builder()
            .subject(email)  // ํ•ด๋‹น ํ† ํฐ์˜ ์ฃผ์ฒด(๊ตฌ๋ถ„์ž ์—ญํ• ์ด๋ฏ€๋กœ ์ด๋ฉ”์ผ์„ ์‚ฌ์šฉ)
            .claims(claims) // ํ† ํฐ์— ํฌํ•จ๋˜์–ด์žˆ๋Š” ๊ธฐ๋ณธ ์ •๋ณด(memberId, grade, status ๋“ฑ)
            .issuedAt(Date.from(Instant.now())) // ํ† ํฐ ๋ฐœํ–‰ ์‹œ๊ฐ„
            .expiration(getTokenExpiration(expirationMinute)) // ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ๊ฐ„
            .signWith(createSignKey(secretKey)) // ํ† ํฐ์„ ์„œ๋ช…ํ•  ๋•Œ ํ•„์š”ํ•œ ๋น„๋ฐ€ํ‚ค๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํ•˜์—ฌ header์™€ payload๋ฅผ ํ•ด์‹ฑํ•จ
            .compact();
}
  • Access Token์„ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค.
  • Jwt๋ฅผ ๋นŒ๋” ํŒจํ„ด์„ ์ด์šฉํ•˜์—ฌ ์‰ฝ๊ฒŒ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์—ฌ๊ธฐ์„œ claims์— ๋Œ€ํ•ด ์„ค๋ช…์„ ํ•˜๊ณ  ๋„˜์–ด๊ฐ€๋Š” ๊ฒƒ์ด ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.
    • claims๋Š” ํ† ํฐ์— ํฌํ•จ๋œ ์œ ์ €์˜ ์ •๋ณด๋กœ ํ† ํฐ ์ธ์ฆ ๋ฐ ์ธ์ฆ ํ›„ ์œ ์ €์˜ ๊ถŒํ•œ๊ณผ ์ƒํƒœ ์—ฌ๋ถ€ ๋“ฑ์„ ํ™•์ธํ•˜๋Š” ๋ฐ์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. 
    • ์œ„์—์„œ ์–ธ๊ธ‰ํ•œ payload ์•ˆ์— ๋“ค์–ด๊ฐ€์žˆ๋Š” ์œ ์ € ์ •๋ณด๊ฐ€ ๋ฐ”๋กœ ์ด claims์ž…๋‹ˆ๋‹ค.
  • JWT์˜ ์„œ๋ช…์— ์‚ฌ์šฉ๋˜๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ ์ฃผ๋กœ HMAC-SHA ๊ณ„์—ด์ž…๋‹ˆ๋‹ค.
    • HMAC-SHA256์˜ ๊ฒฝ์šฐ SHA-256 ํ•ด์‹œ ํ•จ์ˆ˜์™€ SecretKey๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์—ฌ ํ•ด์‹ฑํ•˜๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜์ž…๋‹ˆ๋‹ค.

 

generateRefreshToken ๋ฉ”์„œ๋“œ

public String generateRefreshToken(String email,
                                   int expirationMinute,
                                   String secretKey) {

    // AccessToken๊ณผ ๋™์ผ
    return Jwts.builder()
            .subject(email)
            .issuedAt(Date.from(Instant.now()))
            .expiration(getTokenExpiration(expirationMinute))
            .signWith(createSignKey(secretKey))
            .compact();
}
  • Refresh Token์„ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค.
  • ๋‚ด์šฉ์€ AccessToken๊ณผ ๊ฑฐ์˜ ๊ฐ™์Šต๋‹ˆ๋‹ค.
  • ์ฐจ์ด์ ์€ Access Token์˜ ์žฌ๋ฐœ๊ธ‰์— ๋ชฉ์ ์„ ๋‘๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— claims์ด ๋”ฐ๋กœ ์—†์Šต๋‹ˆ๋‹ค.

 

createAccessToken ๋ฉ”์„œ๋“œ

public String createAccessToken(Member member) {
    // Map์„ ํ†ตํ•ด JWT์— ํฌํ•จํ•  claims ์ƒ์„ฑ.
    // ์ฃผ์˜ํ•  ์ ์€ ์•”ํ˜ธํ™”๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ค‘์š” ์ •๋ณด(๋น„๋ฐ€๋ฒˆํ˜ธ ๋“ฑ)์„ ๋„ฃ์–ด์„œ๋Š” ์•ˆ๋จ.
    Map<String, Object> claims = new HashMap<>();
    claims.put("memberId", member.getMemberId());

    // ํ•ด๋‹น ํ† ํฐ์—์„œ ๊ตฌ๋ถ„์ž ์—ญํ• ์„ ํ•  ์ฃผ์ฒด ๊ฐ’(์—ฌ๊ธฐ์„œ๋Š” email๋กœ ์‚ฌ์šฉ)
    String subject = member.getUsername();

    return generateAccessToken(subject, claims, accessTokenExpirationMinutes, secretKey);
}
  • AccessToken์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ๋ฉ”์„œ๋“œ๋กœ claim๊ณผ subject๋ฅผ ๋”ฐ๋กœ ๋งŒ๋“ค๊ธฐ ์œ„ํ•˜์—ฌ generateAccessToken๊ณผ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ์—ฌ๊ธฐ์„œ๋Š” ๊ฐ„๋‹จํ•œ ์˜ˆ์ œ๋ฅผ ์œ„ํ•ด claims์— member์˜ ID ๊ฐ’๋งŒ ํฌํ•จ์‹œ์ผฐ์Šต๋‹ˆ๋‹ค.
    • ์ถ”๊ฐ€ ์ •๋ณด๋Š” ๊ทธ๋Œ€๋กœ Map์— ์ถ”๊ฐ€ํ•˜๋Š” ์‹์œผ๋กœ ์ž‘์„ฑํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

 

createRefreshToken ๋ฉ”์„œ๋“œ

public String createRefreshToken(Member member) {

    String subject = member.getUsername();

    return generateRefreshToken(subject, refreshTokenExpirationMinutes, secretKey);
}
  • RefreshToken์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ๋ฉ”์„œ๋“œ๋กœ subject๋ฅผ ์œ„ํ•ด generateRefreshToken๊ณผ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

 

getTokenExpiration ๋ฉ”์„œ๋“œ

private Date getTokenExpiration(int expirationMinute) {
    // ํ˜„์žฌ ์‹œ๊ฐ„์„ ๋‚˜ํƒ€๋‚ด๋Š” Calendar ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•จ.
    Calendar calendar = Calendar.getInstance();

    // Calendar ๊ฐ์ฒด์˜ ํ˜„์žฌ ์‹œ๊ฐ„์— expirationMinute ๋งŒํผ์˜ ๋ถ„์„ ๋”ํ•จ.
    calendar.add(Calendar.MINUTE, expirationMinute);

    // ์ˆ˜์ •๋œ Calendar ๊ฐ์ฒด์˜ ์‹œ๊ฐ„์„ Date ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ฐ˜ํ™˜.
    return calendar.getTime();
}
  • ํ˜„์žฌ ์‹œ๊ฐ„์— ํ† ํฐ์˜ ์œ ํšจ ์‹œ๊ฐ„ ๋งŒํผ์„ ๋”ํ•ด ๋งŒ๊ธฐ ์‹œ๊ฐ„์„ ๋ฆฌํ„ดํ•ฉ๋‹ˆ๋‹ค.

 

CreateSignKey ๋ฉ”์„œ๋“œ

private SecretKey createSignKey(String secretKey) {

    // String secretKey๋ฅผ Base64๋กœ ๋””์ฝ”๋”ฉํ•˜์—ฌ byte ๋ฐฐ์—ด๋กœ ๋งŒ๋“ฌ
    byte[] decodedSecretKey = Base64.getDecoder().decode(secretKey);

    // hmacShaKeyFor() ๋ฉ”์„œ๋“œ๋Š” ์ „๋‹ฌ๋ฐ›์€ ๋ฐ”์ดํŠธ ๋ฐฐ์—ด์„ ์‚ฌ์šฉํ•˜์—ฌ
    // HMAC-SHA256 ๋˜๋Š” HMAC-SHA512 ๋“ฑ์˜ ์•Œ๊ณ ๋ฆฌ์ฆ˜์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” SecretKey ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•จ.
    // ์˜ˆ๋ฅผ ๋“ค์–ด, HMAC-SHA256์—์„œ๋Š” ์ตœ์†Œ 256๋น„ํŠธ(32๋ฐ”์ดํŠธ) ์ด์ƒ์˜ ํ‚ค๊ฐ€ ํ•„์š”ํ•จ.
    // ๋งŒ์•ฝ ํ‚ค ๊ธธ์ด๊ฐ€ ์งง์œผ๋ฉด ์˜ˆ์™ธ(InvalidKeyException)๋ฅผ ๋ฐœ์ƒ์‹œํ‚ด.
    return Keys.hmacShaKeyFor(decodedSecretKey);
}
  • Jwt์˜ ์„œ๋ช…ํ‚ค๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค.
  • ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์„œ๋ฒ„์—์„œ ์ง€์ •ํ•œ secretKey๋ฅผ ์ž…๋ ฅ๋ฐ›์Šต๋‹ˆ๋‹ค.
    • generateAccessToken ๋ฉ”์„œ๋“œ ์ฐธ๊ณ 

 

verifyAccessJws ๋ฉ”์„œ๋“œ

public Claims verifyAccessJws(HttpServletRequest request) {

    // ํ—ค๋”์— ์‹ค๋ฆฐ ํ† ํฐ์˜ ๋งจ ์•ž์— Bearer๊ฐ€ ํฌํ•จ๋˜์–ด์žˆ๋Š”์ง€๋ฅผ ํ™•์ธํ•˜์—ฌ AccessToken์ž„์„ ํ™•์ธํ•จ
    try {
        String jws;
        if (request.getHeader("Authorization").startsWith("Bearer ")) {
            jws = request.getHeader("Authorization").replace("Bearer ", "");
        } else {
            log.error("[ JwtTokenizer - verifyAccessJws ] Request Header์— AccessToken์ด ์—†์Šต๋‹ˆ๋‹ค.");
            throw new RuntimeException("ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
        }

        // ์„œ๋ฒ„์— ์ €์žฅ๋˜์–ด ์žˆ๋Š” secretKey๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋ช…์— ์‚ฌ์šฉํ•  ๋น„๋ฐ€ํ‚ค ์ƒ์„ฑ
        SecretKey signKey = createSignKey(secretKey);

        // ๊ฒ€์ฆ ์‹œ์— ๋งŒ๋“  secretKey๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Jwt ํŒŒ์‹ฑ์— ์„ฑ๊ณตํ•œ๋‹ค๋ฉด
        // ํ•ด๋‹น JWT๋ฅผ ๋งŒ๋“ค ๋–„ ์‚ฌ์šฉํ•œ signKey์™€ ๊ฐ™์€ ๋น„๋ฐ€ํ‚ค๋ผ๋Š” ์ด์•ผ๊ธฐ์ด๋ฏ€๋กœ
        // ์„œ๋ฒ„์—์„œ ์ƒ์„ฑ๋œ JWT์ž„์ด ํ™•์ธ๋จ.
        Claims claims = Jwts.parser().verifyWith(signKey).build().parseSignedClaims(jws).getPayload();

        return claims;
    } catch (SignatureException e) {
        log.error(e.getMessage());
        throw new RuntimeException("ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
    }
}
  • AccessToken์˜ JWS๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค.

 

verifyRefreshJws ๋ฉ”์„œ๋“œ

    public Claims verifyRefreshJws(HttpServletRequest request) {

        try {
            String jws;
            if (!request.getHeader("Refresh").isBlank()) {
                jws = request.getHeader("Refresh");
            } else {
                log.error("[ JwtTokenizer - verifyAccessJws ] Request Header์— RefreshToken์ด ์—†์Šต๋‹ˆ๋‹ค.");
                throw new RuntimeException("ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
            }
            SecretKey signKey = createSignKey(secretKey);

            return Jwts.parser().verifyWith(signKey).build().parseSignedClaims(jws).getPayload();
        } catch (SignatureException e) {
            log.error(e.getMessage());
            throw new RuntimeException("ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
        }
    } 
}// ํด๋ž˜์Šค ๋
  • RefreshToken์˜ JWS๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค.
  • AccessToken JWS ๊ฒ€์ฆ ๋ฉ”์„œ๋“œ์™€ ๋Œ€์ฒด๋กœ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.
  • ์—ฌ๊ธฐ๊นŒ์ง€๊ฐ€ JwtTokenizer ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.

 

๐Ÿ›‘ redis

Docker์—์„œ Redis ์„ค์น˜ ๋ฐ ํ…Œ์ŠคํŠธ

# redis ์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ
$ docker pull redis

# redis ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰
$ docker run -d --name redis-container -p 6379:6379 redis

# redis CLI๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ ‘์† ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ํ™•์ธ
docker exec -it redis-container redis-cli
  • ์ด ๋ถ€๋ถ„์€ ์ž์œ ๋กญ๊ฒŒ ํ•ด์ฃผ์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๋กœ์ปฌ Redis๋„ ์ƒ๊ด€์—†์Šต๋‹ˆ๋‹ค.

 

RedisConfig

@Configuration // ํ•ด๋‹น ํด๋ž˜์Šค๋ฅผ Spring ์„ค์ • ํด๋ž˜์Šค(Configuration)๋กœ ์ง€์ •
@RequiredArgsConstructor // ํ•„์ˆ˜ ์ƒ์„ฑ์ž๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ด์ฃผ๋Š” Lombok ์–ด๋…ธํ…Œ์ด์…˜
public class RedisConfig {

    /**
     * Redis ์—ฐ๊ฒฐ์„ ์œ„ํ•œ ConnectionFactory๋ฅผ Bean์œผ๋กœ ๋“ฑ๋ก
     * ์—ฌ๊ธฐ์„œ๋Š” LettuceConnectionFactory๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Redis์— ์—ฐ๊ฒฐ
     */
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory();
    }

    /**
     * RedisTemplate์„ ์„ค์ •ํ•˜์—ฌ Redis์™€์˜ ๋ฐ์ดํ„ฐ ์ง๋ ฌํ™” ๋ฐ ์—ฐ๊ฒฐ์„ ๊ด€๋ฆฌ
     * 
     * @return ์„ค์ •๋œ RedisTemplate ๊ฐ์ฒด
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

        // Redis ์—ฐ๊ฒฐ์„ ์œ„ํ•œ ConnectionFactory ์„ค์ •
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        // Key Serializer ์„ค์ • (๋ฌธ์ž์—ด ์ง๋ ฌํ™”)
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());

        // Value Serializer ์„ค์ •
        redisTemplate.setValueSerializer(new StringRedisSerializer()); // ๋ฌธ์ž์—ด ์ง๋ ฌํ™”
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); // JSON ์ง๋ ฌํ™”

        return redisTemplate;
    }
}
  • ์ด ํด๋ž˜์Šค๋Š” Spring Boot ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ Redis๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์„ค์ •์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.
  1. RedisConnectionFactory ์„ค์ •
    • LettuceConnectionFactory๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Redis ์„œ๋ฒ„์™€์˜ ์—ฐ๊ฒฐ์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
  2. RedisTemplate ์„ค์ •
    • Redis์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์‰ฝ๊ฒŒ ์ €์žฅํ•˜๊ณ  ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋„๋ก RedisTemplate<String, Object>๋ฅผ ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
    • ํ‚ค๋Š” StringRedisSerializer๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฌธ์ž์—ด๋กœ ์ง๋ ฌํ™”ํ•ฉ๋‹ˆ๋‹ค.
    • ๊ฐ’์€ StringRedisSerializer์™€ GenericJackson2JsonRedisSerializer๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ง๋ ฌํ™” ๋ฐฉ์‹์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.

 

RedisService

Service
@RequiredArgsConstructor
public class RedisService {

    private final RedisTemplate<String, Object> redisTemplate;

    /*
     ์ผ๋ฐ˜ ์ €์žฅ
     */
    public void saveData(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /*
     ํŠน์ • ์‹œ๊ฐ„ ๊ฒฝ๊ณผ ํ›„ ์ž๋™ ์‚ญ์ œ ์ €์žฅ
     */
    public void saveDataLimitTime(String key, Object value, int limitTime, TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, value, limitTime, timeUnit);
    }

    /*
     Redis์— ์ €์žฅํ•œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
     */
    public String getData(String key) {

        return (String) redisTemplate.opsForValue().get(key.replace("Bearer ", ""));
    }

    /*
     RefreshToken Redis์— ์ €์žฅ
     */
    public void setRefreshToken(String key, String value, int expirationMinutes) {
        if (key.startsWith("Bearer")) throw new RuntimeException("์œ ํšจํ•˜์ง€ ์•Š์€ Refresh Token์ž…๋‹ˆ๋‹ค.");

        redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(expirationMinutes));
    }

    /*
     AccessToken Redis์— ์ €์žฅ
     */
    public void setBlackList(String key, String value, int expirationMinutes) {
        if (!key.startsWith("Bearer")) throw new RuntimeException("์œ ํšจํ•˜์ง€ ์•Š์€ Access Token์ž…๋‹ˆ๋‹ค.");

        redisTemplate.opsForValue().set(key.replace("Bearer ", ""), value, Duration.ofMinutes(expirationMinutes));
    }

    /*
    ๋กœ๊ทธ์•„์›ƒ ํ›„ RefreshToken ์‚ญ์ œ
     */
    public void deleteRefreshToken(String key) {
        redisTemplate.delete(key);
    }
}
  • Redis์™€ ํ†ต์‹ ํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ์„œ๋น„์Šค ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
  • ๊ฐ ๋ฉ”์„œ๋“œ๋Š” Redis์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ฑฐ๋‚˜ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

 

๐Ÿ‘จ๐Ÿป‍๐Ÿ’ป ๋‹ค์Œ ์‹œ๊ฐ„

  • ๋‹ค์Œ ๊ธ€์—์„œ๋Š” ์‹ค์ œ ๊ตฌํ˜„ํ•˜์—ฌ ๋กœ๊ทธ์ธ ์‹œ jwt๋ฅผ ๋ฐœ๊ธ‰ํ•˜๊ณ , ํ† ํฐ์„ redis์— ์ €์žฅํ•˜๋Š” ๋ถ€๋ถ„์„ ๋ณด์—ฌ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

 

 

 

์ฐธ๊ณ 

https://dkswnkk.tistory.com/684#refresh-token-rotationrtr-๋ฐฉ๋ฒ•

 

 

728x90