๐ ์ด๋ฒ ๊ธ์์๋ ์ง๋ ์๊ฐ์ ์ด์ด 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๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํ ์ค์ ์ ๋ด๋นํฉ๋๋ค.
- RedisConnectionFactory ์ค์
- LettuceConnectionFactory๋ฅผ ์ฌ์ฉํ์ฌ Redis ์๋ฒ์์ ์ฐ๊ฒฐ์ ๊ด๋ฆฌํฉ๋๋ค.
- 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-๋ฐฉ๋ฒ