์ด์ ๋ก๊ทธ์ธ๊น์ง ๊ตฌํ์ด ๋๋ฌ์ผ๋ ๋ก๊ทธ์ธ์ด ์ฑ๊ณตํ๋ฉด JWT๋ฅผ ๋ฐ๊ธํ์ฌ์ AccessToken๊ณผ RefreshToken์ ๊ฐ์ง๊ณ ๋ก๊ทธ์ธ ์์ด ์ธ์ฆ์ ์ฒ๋ฆฌํ๋ ์ฝ๋๋ฅผ ์์ฑํ๊ฒ ์ต๋๋ค.
- ์ํ๋ฆฌํฐ๋ฅผ ์ด์ฉํ JSON ๋ฐ์ดํฐ๋ก ๋ก๊ทธ์ธ (์๋ฃ)
- JWT๋ฅผ ์ด์ฉํ ์ธ์ฆ (์งํ ์ค)
- ๋๋ฉ์ธ, ํ ์ด๋ธ ์ค๊ณ, ์ํฐํฐ ์์ฑ
- ๋๊ธ ์ญ์ ๋ก์ง ๊ตฌํ
- ํ์๊ฐ์ + ์ ๋ณด์์ ๋ฑ ํ์ ์๋น์ค ๊ตฌํ
- ๊ฒ์ํ ์๋น์ค ๊ตฌํ
- ๋๊ธ ์๋น์ค ๊ตฌํ (1๋๊ธ -> *(๋ฌดํ) ๋๋๊ธ ๊ตฌ์กฐ)
- ์์ธ ์ฒ๋ฆฌ
- ์์ธ ๋ฉ์ธ์ง ๊ตญ์ ํ
- ์นดํ ๊ณ ๋ฆฌ๋ณ ๊ฒ์ํ ๋ถ๋ฅ
- ๊ฒ์๊ธ ํ์ด์ง
- ๋์ ์ธ ๊ฒ์ ์กฐ๊ฑด์ ์ฌ์ฉํ ๊ฒ์
- ์ฌ์ฉ์ ๊ฐ ์ชฝ์ง ๊ธฐ๋ฅ
- ๋ฌดํ ์ชฝ์ง ์คํฌ๋กค
- ๊ฒ์๋ฌผ & ๋๊ธ์ ๋ํ ์๋
- ์ชฝ์ง์ ๋ํ ์๋
- ์ ์ํ ์ฌ์ฉ์ ๊ฐ ์ค์๊ฐ ์ฑํ
- ํ์๊ฐ์ ์ ๊ฒ์ฆ(์: XX๋ํ๊ต XX๊ณผ๊ฐ ์๋๋ฉด ๊ฐ์ ํ ์ ์๊ฒ)
- Swagger๋ฅผ ์ฌ์ฉํ API ๋ฌธ์ ๋ง๋ค๊ธฐ
- ์ ๊ณ & ๋ธ๋๋ฆฌ์คํธ ๊ธฐ๋ฅ
- AOP๋ฅผ ํตํ ๋ก๊ทธ
- ์ด๋๋ฏผ ํ์ด์ง
- ์บ์
- ๋ฐฐํฌ (+ ๋ฌด์ค๋จ ๋ฐฐํฌ)
- ๋ฐฐํฌ ์๋ํ
- ํฌํธ์ ์ด๋ํฐ ์ค๊ณ๋ฅผ ๋ฐ๋ฅด๋ ํจํค์ง ๊ตฌ์กฐ ์ค๊ณํ๊ธฐ
- ...
JWT์ ๋ํด์
์ฐ์ JWT์ ๋ํด์ ๊ณต๋ถํ๊ธฐ ์ ์, JWT๋ฅผ ์ ์ฐ๋์ง์ ๋ํด์ ๊ถ๊ธํ์๋ค๋ฉด ์๋ ์ฌ์ดํธ๋ฅผ ์ฐธ๊ณ ํ์๋ฉด ์ข์ ๊ฑฐ ๊ฐ์ต๋๋ค.
(์น ๋ณด์ ์ดํด ๋ถ๋ถ์ ๋ค์ผ์๋ฉด ์ข์ ๊ฑฐ ๊ฐ์์!)
JWT(JSON Web Token)๋ ๋น์ฌ์ ๊ฐ์ ์ ๋ณด๋ฅผ JSON ๊ฐ์ฒด๋ก ์์ ํ๊ฒ ์ ์กํ๊ธฐ ์ํ ๊ฐ๊ฒฐํ๊ณ ์์ฒด ํฌํจ๋ ๋ฐฉ๋ฒ์ ์ ์ ํ๋ ๊ฐ๋ฐฉํ ํ์ค( RFC 7519 )์ ๋๋ค. ์ด ์ ๋ณด๋ ๋์งํธ ์๋ช ๋์ด ์์ผ๋ฏ๋ก ํ์ธํ๊ณ ์ ๋ขฐํ ์ ์์ต๋๋ค. JWT๋ ๋น๋ฐ( HMAC ์๊ณ ๋ฆฌ์ฆ ์ฌ์ฉ)์ ์ฌ์ฉ ํ๊ฑฐ๋ RSA ๋๋ ECDSA๋ฅผ ์ฌ์ฉํ๋ ๊ณต๊ฐ/๊ฐ์ธ ํค ์์ ์ฌ์ฉํ์ฌ ์๋ช ํ ์ ์์ต๋๋ค .
JWT๋ฅผ ์ํธํํ์ฌ ๋น์ฌ์ ๊ฐ์ ๋น๋ฐ๋ ์ ๊ณตํ ์ ์์ง๋ง ์๋ช ๋ ํ ํฐ์ ์ค์ ์ ๋ ๊ฒ ์ ๋๋ค. ์๋ช ๋ ํ ํฐ์ ๊ทธ ์์ ํฌํจ๋ ํด๋ ์ ์ ๋ฌด๊ฒฐ์ฑ ์ ํ์ธํ ์ ์๋ ๋ฐ๋ฉด ์ํธํ๋ ํ ํฐ์ ์ด๋ฌํ ํด๋ ์์ ๋ค๋ฅธ ๋น์ฌ์๋ก๋ถํฐ ์จ๊ธธ ์ ์์ต๋๋ค. ๊ณต๊ฐ/๊ฐ์ธ ํค ์์ ์ฌ์ฉํ์ฌ ํ ํฐ์ ์๋ช ํ ๋ ์๋ช ์ ๊ฐ์ธ ํค๋ฅผ ๋ณด์ ํ๊ณ ์๋ ๋น์ฌ์๋ง ์๋ช ํ์์ ์ฆ๋ช ํฉ๋๋ค.
HMAC ์๊ณ ๋ฆฌ์ฆ์ด๋ RSA๋ฑ์ ์๊ณ ๋ฆฌ์ฆ์ ์ฌ์ฉํ ์ ์๋๋ฐ ๋ฐฉ์์ด ๊ฐ๊ฐ ๋ค๋ฅด๋ฉฐ
๊ณต๊ฐ/๊ฐ์ธ ํค ์์ ์ด์ฉํ์ฌ ์๋ช ์ ํ ์ ์์ต๋๋ค.
(์๋ช ์ ์ฝ๊ฒ ๋งํ๋ฉด, ์ด ํ ํฐ์ ์์ฑํ ์ฌ๋์ด ๋๊ตฌ์ธ์ง๋ฅผ ๋ฐํ๋ ๊ฒ์ ๋๋ค.)
JWT๋ ๋ํ์ ์ผ๋ก ๋ค์ ๋ ๊ฐ์ง ๊ฒฝ์ฐ์ ์ฌ์ฉํ๋ฉด ์ ์ฉํฉ๋๋ค.
- ๊ถํ ๋ถ์ฌ : JWT๋ฅผ ์ฌ์ฉํ๋ ๊ฐ์ฅ ์ผ๋ฐ์ ์ธ ๋ฐฉ์์ ๋๋ค. ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธํ๋ฉด ๊ฐ ํ์ ์์ฒญ์ JWT๊ฐ ํฌํจ๋์ด ์ฌ์ฉ์๊ฐ ํด๋น ํ ํฐ์ผ๋ก ์๋น์ค ๋ฐ ๋ฆฌ์์ค์ ์ ๊ทผํ ์ ์์ต๋๋ค
- ์ ๋ณด ๊ตํ : JWT๋ ์ ๋ณด๋ฅผ ์์ ํ๊ฒ ์ ์กํ๋ ์ข์ ๋ฐฉ๋ฒ์ ๋๋ค. ์๋ฅผ ๋ค์ด ๊ณต๊ฐ/๊ฐ์ธ ํค ์์ ์ฌ์ฉํ์ฌ JWT์ ์๋ช ํ ์ ์๊ธฐ ๋๋ฌธ์ ๋ฐ์ ์๊ฐ ๋๊ตฌ์ธ์ง ํ์ธํ ์ ์์ต๋๋ค. ๋ํ ํค๋์ ํ์ด๋ก๋๋ฅผ ์ฌ์ฉํ์ฌ ์๋ช ์ ๊ณ์ฐํ๋ฏ๋ก ์ฝํ ์ธ ๊ฐ ๋ณ์กฐ๋์ง ์์๋์ง ํ์ธํ ์๋ ์์ต๋๋ค.
JWT์ ๊ตฌ์กฐ
JWT์ ๊ตฌ์กฐ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค
header.payload.signature . ์ผ๋ก ๊ตฌ๋ถํฉ๋๋ค.
HEADER
ํ ํฐ์ ํ์ (type)๊ณผ ํด์ฑ ์๊ณ ๋ฆฌ์ฆ(alg)์ ์ง์ ํฉ๋๋ค.
์๊ณ ๋ฆฌ์ฆ์ ๋ฌด์์ ์ฌ์ฉํ๊ณ , ํ์ ์ ๋ฌด์จ ํ์ ์ด๋ค๋ฅผ ๋ํ๋ ๋๋ค.
๋ณดํต HMAC SHA256 ํน์ RSA๊ฐ ์ฌ์ฉ๋๋ฉฐ ํ ํฐ์ ๊ฒ์ฆํ ๋ ์ฌ์ฉํฉ๋๋ค.
์ ํฌ๋ HMAC512๋ฅผ ์ฌ์ฉํ์ฌ ์ํธํ ๋ฐ ๋ณตํธํ ํ๊ฒ ์ต๋๋ค.
์์ ์ฌ์ง์ฒ๋ผ, JWT์ HS512 ์๊ณ ๋ฆฌ์ฆ์ ์ ์ฉํ๋ฏ๋ก, ์์ ๊ฐ์ด ์ค์ ์ด ๋ ๊ฒ์ ๋๋ค.
(์ฐธ๊ณ ๋ก ์ํธํ ๋ฐฉ์์์ SHA๋ ํด์ฌ๋ฅผ ์ฌ์ฉํ ์ํธํ ๋ฐฉ์์ผ๋ก, ๋ณตํธํ๊ฐ ๋ถ๊ฐ๋ฅํฉ๋๋ค. ๊ทธ๋ฌ๋ HMAC์ ์ํฌ๋ฆฟ ํค๋ฅผ ํฌํจํ์ฌ ์ํธํํ๋ ๋ฐฉ์์ ๋๋ค. ๊ทธ๋ฆฌ๊ณ HS๋, HMAC + SHA๋ก ์ํฌ๋ฆฟ ํค๋ฅผ ๊ฐ์ง๊ณ )
์ฝ๊ฒ ๋งํด Header์๋ ํ ํฐ์ ์ข ๋ฅ์ ์ด ํ ํฐ์ ์ํธํ ๋ฐฉ์์ด ์ ํ์์ต๋๋ค.
PAYLOAD
ํ ํฐ์ ๋ด์ ์ ๋ณด์ ๋๋ค.
ํ ํฐ์ ๋ด์ ํ๋์ ์ ๋ณด, ํ ์กฐ๊ฐ์ ์ ๋ณด๋ฅผ ํด๋ ์์ด๋ผ๊ณ ํฉ๋๋ค
ํด๋ ์์ name/value์ ํ ์์ผ๋ก ์ด๋ฃจ์ด์ ธ ์์ต๋๋ค.
ํด๋ ์์ ์ข ๋ฅ๋ ๋ค์๊ณผ ๊ฐ์ด 3๊ฐ์ง์ ์ข ๋ฅ๊ฐ ์์ต๋๋ค.
๋ฑ๋ก๋ ํด๋ ์ (registered claim)
- ์๋น์ค์ ํ์ํ ์ ๋ณด๊ฐ ์๋๋ผ ํ ํฐ์ ๋ํ ์ ๋ณด๋ค์ ๋ด๊ธฐ์ํด ์ด๋ฆ์ด ์ด๋ฏธ ์ ํด์ง ํด๋ ์์ ๋๋ค
- ํ์๋ ์๋์ง๋ง ๊ถ์ฅ๋๋ ํด๋ ์์ ๋๋ค.
- iss(๋ฐ๊ธ์), sub(์ ๋ชฉ), aud(๋์์), exp(๋ง๋ฃ์๊ฐ), nbf(ํ ํฐ์ ํ์ฑ๋ ์ง), iat(๋ฐ๊ธ๋์๊ฐ), jti(JWT ๊ณ ์ ์๋ณ์, ์ผํ์ฉ ํ ํฐ์ ์ฌ์ฉ)
๊ณต๊ฐ ํด๋ ์ (public claim)
- ๊ณต๊ฐ ํด๋ ์์ ์ฌ์ฉ์ ์ ์ ํด๋ ์์ผ๋ก. ๊ณต๊ฐ์ฉ ์ ๋ณด ์ ๋ฌ์ ์ํด ์ฌ์ฉ๋ฉ๋๋ค. ์ถฉ๋ ๋ฐฉ์ง๋ฅผ ์ํด URI ํฌ๋งท์ ์ด์ฉํด์ผ ํ๋ค๋๋ฐ ์ธ์ ์ฌ์ฉํ๋์ง๋ ์ ๋ชจ๋ฅด๊ฒ ์ต๋๋ค.
๋น๊ณต๊ฐ ํด๋ ์ (private claim)
- ๋น๊ณต๊ฐ ํด๋ ์์ ๋ฑ๋ก๋ ํด๋ ์๋ ์๋๊ณ , ๊ณต๊ฐ ํด๋ ์๋ ์๋ ๋น์ฌ์๊ฐ์ ์ ๋ณด๋ฅผ ๊ณต์ ํ๊ธฐ ์ํด ๋ง๋ค์ด์ง ์ฌ์ฉ์์ง์ ํด๋ ์์ด๋ค. ์ฆ ์ด๊ณณ์ ์ธ์ฆ ์ ๋ณด ๋ฑ์ ์๋ฒ์ ํด๋ผ์ด์ธํธ๊ฐ ํ์ํ ์ ๋ณด๋ฅผ ๋ฃ์ด๋๋ ํ์์ด๊ฒ ์ฃ ?
Payload์๋ ์ ๋ณด๊ฐ ๋ค์ด ์์ต๋๋ค.
๊ฐ๋ฐ์๊ฐ ์ ๋ณด๋ฅผ ๋ ์ถ๊ฐํ ์๋ ์์ต๋๋ค.
SIGNATURE
ํ ํฐ์ ์ธ์ฝ๋ฉํ๊ฑฐ๋ ์ ํจ์ฑ ๊ฒ์ฆ์ ํ ๋ ์ฌ์ฉํ๋ ๊ณ ์ ํ ์ํธํ ์ฝ๋์ ๋๋ค
์๋ช ์ ์์์ ๋ง๋ ํค๋์ ํ์ด๋ก๋์ ๊ฐ์ ๊ฐ๊ฐ BASE64๋ก ์ธ์ฝ๋ฉํ๊ณ ,
์ธ์ฝ๋ฉ ํ ๊ฐ์ ๋น๋ฐ ํค๋ฅผ ์ด์ฉํ์ฌ ํค๋์์ ์ ํ ์๊ณ ๋ฆฌ์ฆ์ผ๋ก ํด์ฑ์ ํ ํ ๋ค์ BASE64๋ก ์ธ์ฝ๋ฉํ์ฌ ์์ฑํฉ๋๋ค.
HMACSHA256(
base64UrlEncode(header) + "." +base64UrlEncode(payload),
secret)
์๋ช ์ HS256 ๋ฐฉ์์ ๊ฒฝ์ฐ ์ฐ๋ฆฌ๊ฐ ๋ง๋ค์๋ header์ payload, ๊ทธ๋ฆฌ๊ณ ๋๋ง ์๊ณ ์๋ ๊ฐ์ธ ํค๋ฅผ ๋ฃ์ด์ HS256 ์ํธํ ์๊ณ ๋ฆฌ์ฆ์ ์ฌ์ฉํ์ฌ ์ํธํ๋ฅผ ํ์ฌ์ ์ฌ์ฉํฉ๋๋ค.
์ด๋ ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก๋ถํฐ JWT๋ฅผ ๋ฐ์์ ๋, JWT์ header์ payload๋ฅผ ์๋ฒ์์ ๋๊ฐ์ด HS256์ผ๋ก ์ํธํํ์ฌ, ํด๋ผ์ด์ธํธ๊ฐ ๋ณด๋ธ JWT์ Signature์ ๊ฐ๋ค๋ฉด ์์ฒญ๋ ๊ฒ์ผ๋ก ์๊ณ ๊ฒ์ฆํฉ๋๋ค.
๋ง์ RSA๋ผ๋ฉด, ์ํฌ๋ฆฟ ํค๋ฅผ ๋ฃ์ง ์๊ณ , ์๋ฒ์ ๊ฐ์ธ ํค๋ก ์ ๊ตฐ ํ, ํ ํฐ์ ์ ์กํฉ๋๋ค.
ํด๋ผ์ด์ธํธ๋ ํด๋น ํ ํฐ์ ๋ฐ์์, ๋ค์ ์๋ฒ์ ์ ์กํ ๋, ๊ทธ๋ฅ ์๋ฒ์ ๊ณต๊ฐ ํค๋ก ์ด์ด๋ณด๊ธฐ๋ง ํ๋ฉด ๋ฉ๋๋ค.
JWT์ ์ฃผ์์
์ธ์ฆ์์ ์ฌ์ฉ์๊ฐ ์๊ฒฉ ์ฆ๋ช ์ ์ฌ์ฉํ์ฌ ์ฑ๊ณต์ ์ผ๋ก ๋ก๊ทธ์ธํ๋ฉด JSON ์น ํ ํฐ์ด ๋ฐํ๋ฉ๋๋ค.
ํ ํฐ์ ์๊ฒฉ ์ฆ๋ช ์ ์ฃผ ๋ชฉ์ ์ผ๋ก ์ฌ์ฉํ๋ฏ๋ก ๋ณด์ ๋ฌธ์ ๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ์ธ์ฌํ ์ฃผ์๋ฅผ ๊ธฐ์ธ์ฌ์ผ ํฉ๋๋ค.
์ผ๋ฐ์ ์ผ๋ก ํ ํฐ์ ํ์ ์ด์์ผ๋ก ์ค๋ ๋ณด๊ดํด์๋ ์ ๋ฉ๋๋ค.
๋ํ ๋ณด์์ด ์ทจ์ฝํ๊ธฐ ๋๋ฌธ์ ๋ฏผ๊ฐํ ์ธ์ ๋ฐ์ดํฐ๋ฅผ ๋ธ๋ผ์ฐ์ ์ ์ฅ์์ ์ ์ฅํด์๋ ์๋ฉ๋๋ค.
ํด๋ผ์ด์ธํธ๊ฐ ๋ณดํธ๋ ๊ฒฝ๋ก ๋๋ ๋ฆฌ์์ค์ ์ ๊ทผํ๋ ค๊ณ ํ ๋๋ง๋ค, ํด๋ผ์ด์ธํธ๋ ์ผ๋ฐ์ ์ผ๋ก Bearer ์คํค๋ง๋ฅผ ์ฌ์ฉํ์ฌ Authorization ํค๋ ์์ JWT๋ฅผ ๋ณด๋ด์ผ ํฉ๋๋ค .
ํค๋์ ๋ด์ฉ์ ๋ค์๊ณผ ๊ฐ์์ผ ํฉ๋๋ค.
Authorization: Bearer <token>
์ด๋ ํน์ ๊ฒฝ์ฐ์ ์ํ ๋น์ ์ฅ ๊ถํ ๋ถ์ฌ ๋ฉ์ปค๋์ฆ์ด ๋ ์ ์์ต๋๋ค.
์๋ฒ์ ๋ณดํธ๋ ๊ฒฝ๋ก๋ Authorizationํค๋์ ์ ํจํ JWT๊ฐ ์๋์ง ํ์ธํ๊ณ JWT๊ฐ ์๋ ๊ฒฝ์ฐ ์ฌ์ฉ์๋ ๋ณดํธ๋ ๋ฆฌ์์ค์ ์ก์ธ์คํ ์ ์์ต๋๋ค.
JWT์ ํ์ํ ๋ฐ์ดํฐ๊ฐ ํฌํจ๋์ด ์์ผ๋ฉด ํน์ ์์ ์ ๋ํด ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฟผ๋ฆฌํด์ผ ํ ํ์์ฑ์ด ์ค์ด๋ค ์ ์์ง๋ง ํญ์ ๊ทธ๋ฐ ๊ฒ์ ์๋๋๋ค.
ํ ํฐ์ด Authorizationํค๋ ๋ก ์ ์ก๋๋ฉด CORS(Cross-Origin Resource Sharing)๋ ์ฟ ํค๋ฅผ ์ฌ์ฉํ์ง ์์ผ๋ฏ๋ก ๋ฌธ์ ๊ฐ ๋์ง ์์ต๋๋ค.
(์ง๊ธ๊น์ง ์์ด๋ฅผ ๊ทธ๋๋ก ๋ฒ์ญํ ๊ฒ์ด ๋ง์ ๋ถ์์ฐ์ค๋ฌ์ด ๋ด์ฉ์ด ์์ ์ ์์ต๋๋ค. ์ ๊ฐ ๋๋ฆ ๊ณ ์น๊ธด ํ๋๋ฐ, ์์ง ์ ๋ชฐ๋ผ์ ํจ๋ถ๋ก ์ ๋๋๋๋ก ๊ณ ์น๋๊ฑด ์๋ ๊ฒ ๊ฐ์์์..)
JWT๋ฅผ ์ ์ธ๊น์?
JWT๊ฐ Session์ ๋นํด ๊ฐ์ง๋ ์ฅ์ ์ด ๋ญ๊น์?
AccessToken๋ง ๊ฐ์ง๋ ๊ฒฝ์ฐ ํ์คํ๊ฒ Statelessํ๊ฒ ์ธ์ฆ ๋ฑ์ ์ ๋ณด๋ฅผ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
์ด๊ฒ ํ์คํ ์ธ์ ์ ๋นํด ์ฅ์ ์ ๊ฐ์ง๋ค๊ณ ์๊ฐํฉ๋๋ค.
๊ทธ๋ฐ๋ฐ RefreshToken์ ์ฌ์ฉํ๋ ์๊ฐ StateLess์ ๋ํด์๋ ์๋ฌธ์ด ๋ญ๋๋ค.
๋ ๊ฒฐ๊ตญ RefreshToken์ DB์ ์ ์ฅํ๊ฒ ๋ ํ ๋ฐ, ๊ทธ๋ผ Session์ ์ฌ์ฉํ๋ ๊ฒ๊ณผ ๋ค๋ฅผ ๋ฐ ์์ง ์๋ ํ๋ ์๊ฐ์ด ๋ญ๋๋ค.
์ฌ๋ฌ ๊ธ์ ์ฐพ์๋ณด์์ง๋ง ๋ช ํํ ๋ต์ ์๋ ๊ฒ ๊ฐ์์ต๋๋ค.
๊ทธ๋๋ง ๊ฐ์ง๋ ์ฅ์ ์ด๋ผ๋ฉด 'Session์ ๋นํด RefreshToken์ด ๋ง๋ฃ๋ ๊ฒฝ์ฐ์๋ง DB์ ์ ๊ทผํ๋ฏ๋ก I/O๊ฐ ์ค์ด ์ฑ๋ฅ ํฅ์์ด ๋๋ค' ์ ๋์์ต๋๋ค.
๊ด๋ จ ์ฐธ์กฐ๊ธ
JWT ์ฌ์ฉ
๋ค์ ์คํ์์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๊ฒ ์ต๋๋ค.
https://github.com/auth0/java-jwt
build.gradle์ ๋ค์๊ณผ ๊ฐ์ ์์กด์ฑ์ ์ถ๊ฐํด์ค๋๋ค.
implementation 'com.auth0:java-jwt:3.18.2'
์ ์ฒด gradle์ ์๋์ ๊ฐ์ต๋๋ค.
plugins {
id 'org.springframework.boot' version '2.6.2'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'boardexample'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
implementation 'com.auth0:java-jwt:3.18.2'
}
test {
useJUnitPlatform()
}
Member ์ํฐํฐ์ RefreshToken์ ์ถ๊ฐํด์ค๋๋ค.
@Column(length = 1000)
private String refreshToken;//RefreshToken
public void updateRefreshToken(String refreshToken){
this.refreshToken = refreshToken;
}
public void destroyRefreshToken(){
this.refreshToken = null;
}
์๋๋ ์ ์ฒด ์ฝ๋์ ๋๋ค.
package boardexample.myboard.domain.member;
import boardexample.myboard.domain.BaseTimeEntity;
import lombok.*;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.persistence.*;
@Table(name = "MEMBER")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@AllArgsConstructor
@Builder
public class Member extends BaseTimeEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id; //primary Key
@Column(nullable = false, length = 30, unique = true)
private String username;//์์ด๋
private String password;//๋น๋ฐ๋ฒํธ
@Column(nullable = false, length = 30)
private String name;//์ด๋ฆ(์ค๋ช
)
@Column(nullable = false, length = 30)
private String nickName;//๋ณ๋ช
@Column(nullable = false, length = 30)
private Integer age;//๋์ด
@Enumerated(EnumType.STRING)
private Role role;//๊ถํ -> USER, ADMIN
@Column(length = 1000)
private String refreshToken;//RefreshToken
//== ์ ๋ณด ์์ ==//
public void updatePassword(PasswordEncoder passwordEncoder, String password){
this.password = passwordEncoder.encode(password);
}
public void updateName(String name){
this.name = name;
}
public void updateNickName(String nickName){
this.nickName = nickName;
}
public void updateAge(int age){
this.age = age;
}
public void updateRefreshToken(String refreshToken){
this.refreshToken = refreshToken;
}
public void destroyRefreshToken(){
this.refreshToken = null;
}
//== ํจ์ค์๋ ์ํธํ ==//
public void encodePassword(PasswordEncoder passwordEncoder){
this.password = passwordEncoder.encode(password);
}
}
MemberRepository์ RefreshToken์ ํตํด ํ์ ์กฐํํ๋ ๋ฉ์๋ ์ถ๊ฐ
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByUsername(String username);
boolean existsByUsername(String username);
Optional<Member> findByRefreshToken(String refreshToken);
}
JwtService ์์ฑ
JWT๊ด๋ จ๋ ์๋น์ค(AccessToken ์์ฑ, RefreshToken์์ฑ, RefreshToken ์ฌ๋ฐ๊ธ, RefreshToken ์ญ์ , AccessToken ์ญ์ ๋ฑ)๋ฅผ ์ ๊ณตํ๋ ํด๋์ค๋ฅผ ๋ง๋ค๊ฒ ์ต๋๋ค.
์ฐ์ application.yml์ ๋ค์์ ์ถ๊ฐํด์ค๋๋ค.
spring:
profiles:
include: jwt
๋ค์์ฒ๋ผ ์ค์ ํ๋ฉด application-jwt ์ ํด๋นํ๋ properties ํน์ yml ํ์ผ์ ์ฝ์ด์ฌ ์ ์์ต๋๋ค.
์ด๋ ๊ฒ ํด์ jwt ๊ด๋ จ๋ ์ค์ ์ application-jwt์์ ์ค์ ํด์ฃผ๋๋ก ํ๊ฒ ์ต๋๋ค.
์ ์ฒด ์ค์ ์ ์๋์ ๊ฐ์ต๋๋ค.
spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:tcp://localhost/~/jpa
username: sa
password: 1
jpa:
properties:
hibernate:
format_sql: true
user_sql_cooments: true
hibernate:
ddl-auto: create
profiles:
include: jwt
logging:
level:
org:
apache:
coyote:
http11: debug
hiberante:
SQL: debug
boardexample:
myboard: info
๊ทธ๋ฆฌ๊ณ ์๋ก์ด yml ํ์ผ์ธ application-jwt.yml์ ์์ฑํด์ค๋๋ค
ํด๋น ํ์ผ์์๋ JWT ๊ด๋ จ ์ํฌ๋ฆฟ ํค๋ผ๋๊ฐ, ํ ํฐ์ ๋ง๋ฃ ์๊ฐ๋ฑ์ ์ค์ ํด ์ฃผ๋๋ก ํ๊ฒ ์ต๋๋ค.
jwt:
secret: base64๋ก ์ธ์ฝ๋ฉ๋ ์ํธ ํค, HS512๋ฅผ ์ฌ์ฉํ ๊ฒ์ด๊ธฐ ๋๋ฌธ์, 512๋นํธ(64๋ฐ์ดํธ) ์ด์์ด ๋์ด์ผ ํฉ๋๋ค. ๊ธธ๊ฒ ์จ์ฃผ์ธ์
access:
expiration: 80
header: Authorization
refresh:
expiration: 90
header: Authorization-refresh
์ ๋ accessToken์ 80์ด, refreshToken์ 90์ด๋ก ์ค์ ์ ํด์ฃผ์๋๋ฐ ๋์ค์ ๋ฐ๊ฟ์ค ๊ฒ์ ๋๋ค.
(๋ฆฌํ๋ ์ํ ํฐ์ ์ผ๋ถ๋ฌ ์งง๊ฒ ์ฃผ์์ต๋๋ค.)
์ธํฐํ์ด์ค๋ถํฐ ์์ฑํ๋๋ก ํ๊ฒ ์ต๋๋ค.
public interface JwtService {
String createAccessToken(String username);
String createRefreshToken();
void updateRefreshToken(String username, String refreshToken);
void destroyRefreshToken(String username);
void sendToken(HttpServletResponse response, String accessToken, String refreshToken) throws IOException;
String extractAccessToken(HttpServletRequest request) throws IOException, ServletException;
String extractRefreshToken(HttpServletRequest request) throws IOException, ServletException;
String extractUsername(String accessToken);
void setAccessTokenHeader(HttpServletResponse response, String accessToken);
void setRefreshTokenHeader(HttpServletResponse response, String refreshToken);
}
๊ฐ์ ํจํค์ง์ ๊ตฌํ์ฒด๋ ์์ฑํ๋๋ก ํ๊ฒ ์ต๋๋ค.
package boardexample.myboard.global.jwt.service;
import boardexample.myboard.domain.member.repository.MemberRepository;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@Transactional
@Service
@RequiredArgsConstructor
@Setter(value = AccessLevel.PRIVATE)
public class JwtServiceImpl implements JwtService{
//== 1 ==//
@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;
//== 2 ==//
private static final String ACCESS_TOKEN_SUBJECT = "AccessToken";
private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken";
private static final String USERNAME_CLAIM = "username";
private static final String BEARER = "Bearer ";
private final MemberRepository memberRepository;
private final ObjectMapper objectMapper;
//== 3 ==//
@Override
public String createAccessToken(String username) {
return JWT.create()
.withSubject(ACCESS_TOKEN_SUBJECT)
.withExpiresAt(new Date(System.currentTimeMillis() + accessTokenValidityInSeconds * 1000))
.withClaim(USERNAME_CLAIM, username)
.sign(Algorithm.HMAC512(secret));
}
@Override
public String createRefreshToken() {
return JWT.create()
.withSubject(REFRESH_TOKEN_SUBJECT)
.withExpiresAt(new Date(System.currentTimeMillis() + refreshTokenValidityInSeconds * 1000))
.sign(Algorithm.HMAC512(secret));
}
@Override
public void updateRefreshToken(String username, String refreshToken) {
memberRepository.findByUsername(username)
.ifPresentOrElse(
member -> member.updateRefreshToken(refreshToken),
() -> new Exception("ํ์์ด ์์ต๋๋ค")
);
}
@Override
public void destroyRefreshToken(String username) {
memberRepository.findByUsername(username)
.ifPresentOrElse(
member -> member.destroyRefreshToken(),
() -> new Exception("ํ์์ด ์์ต๋๋ค")
);
}
//== 5 ==//
@Override
public void sendToken(HttpServletResponse response, String accessToken, String refreshToken) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_OK);
setAccessTokenHeader(response, accessToken);
setRefreshTokenHeader(response, refreshToken);
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put(ACCESS_TOKEN_SUBJECT, accessToken);
tokenMap.put(REFRESH_TOKEN_SUBJECT, refreshToken);
String token = objectMapper.writeValueAsString(tokenMap);
response.getWriter().write(token);
}
@Override
public String extractAccessToken(HttpServletRequest request) throws IOException, ServletException {
return Optional.ofNullable(request.getHeader(accessHeader)).map(accessToken -> accessToken.replace(BEARER, "")).orElse(null);
}
@Override
public String extractRefreshToken(HttpServletRequest request) throws IOException, ServletException {
return Optional.ofNullable(request.getHeader(refreshHeader)).map(refreshToken -> refreshToken.replace(BEARER, "")).orElse(null);
}
//== 4 ==//
@Override
public String extractUsername(String accessToken) {
return JWT.require(Algorithm.HMAC512(secret)).build().verify(accessToken).getClaim(USERNAME_CLAIM).asString();
}
@Override
public void setAccessTokenHeader(HttpServletResponse response, String accessToken) {
response.setHeader(accessHeader, accessToken);
}
@Override
public void setRefreshTokenHeader(HttpServletResponse response, String refreshToken) {
response.setHeader(refreshHeader, refreshToken);
}
}
1. @Vaule๋ฅผ ์ฌ์ฉํ์ฌ yml ํ์ผ์ ์์ฑํด๋ ์ค์ ๊ฐ๋ค์ ๊ฐ์ ธ์์ ์ฌ์ฉํ์ต๋๋ค.
์ ํฌ๋ jwt.secret, jwt.access.expiration, ๋ฑ ๊ฐ๋ค์ ๋ฐ๋ก ์์์ ์ธํ ํด ์ฃผ์๊ธฐ ๋๋ฌธ์, @Value๋ฅผ ํตํด ์ฌ์ฉํ ์ ์์ผ๋ฉฐ, ํ๋๋ฅผ static์ผ๋ก ์ ์ธ ์ ๊ฐ์ด ๋ค์ด์ค์ง ์์ผ๋ ์ฃผ์ํ์ธ์.
2. JWT์ ๋ฃ์ด์ค Subject์, Claim์ผ๋ก username์ ์ฌ์ฉํ ๊ฒ์ด๊ธฐ์ ํด๋ ์์ name์ "username"์ผ๋ก ์ง์ ํด ์ฃผ์์ต๋๋ค. ๊ทธ๋ฆฌ๊ณ ํญ์ JWT์ ํค๋์ ๋ค์ด์ค๋ ๊ฐ์ผ๋ก๋ 'Authorization = Bearer [ํ ํฐ]' ์ ํ์์ ์ทจํ๊ฒ ํ ๊ฒ์ด๊ธฐ์ BEARER์ ๋ฏธ๋ฆฌ ์ง์ ํด ์ฃผ์์ต๋๋ค.
3. ๋ค๋ฅธ ์ฝ๋๋ค๋ ์ด๊ฑฐ ํ๋๋ง ๋ณด๋ฉด, ์ด๋ฆ๊ณผ ์ฝ๋๋ฅผ ํตํด ์ถฉ๋ถํ ๋ฌด์ผ ํ๋ ์ฝ๋์ธ์ง ์์ค๊ฑฐ๋ผ ์์ํ๊ณ ๋์ด๊ฐ๊ฒ ์ต๋๋ค.
AccessToken์ ๋ง๋๋ ์ฝ๋๋ฅผ ๋ณด๊ฒ ์ต๋๋ค.
JWT.create() //JWT ํ ํฐ์ ์์ฑํ๋ ๋น๋๋ฅผ ๋ฐํํฉ๋๋ค.
.withSubject(ACCESS_TOKEN_SUBJECT)
//๋น๋๋ฅผ ํตํด JWT์ Subject๋ฅผ ์ ํฉ๋๋ค. AccessToken์ด๋ฏ๋ก ์ ๋ ์์์ ์ค์ ํ๋
//AccessToken์ subject๋ฅผ ๊ฐ์ ธ์ ์ฌ์ฉํ๊ฒ ์ต๋๋ค
.withExpiresAt(new Date(System.currentTimeMillis() + accessTokenValidityInSeconds * 1000))
//๋ง๋ฃ์๊ฐ์ ์ค์ ํ๋ ๊ฒ์
๋๋ค. ํ์ฌ ์๊ฐ + ์ ํฌ๊ฐ ์ค์ ํ ์๊ฐ(๋ฐ๋ฆฌ์ด) * 1000์ ํ๋ฉด
//์๋ฅผ ๋ค์ด ์ ์ ๊ฒฝ์ฐ๋ accessTokenValidityInSeconds์ด 80์ด๊ธฐ ๋๋ฌธ์
//ํ์ฌ์๊ฐ์ 80 * 1000 ๋ฐ๋ฆฌ์ด๋ฅผ ๋ํ 'ํ์ฌ์๊ฐ + 80์ด'๊ฐ ์ค์ ์ด ๋๊ณ
//๋ฐ๋ผ์ 80์ด ์ดํ์ ์ด ํ ํฐ์ ๋ง๋ฃ๋ฉ๋๋ค.
.withClaim(USERNAME_CLAIM, username)
//ํด๋ ์์ผ๋ก๋ ์ ํฌ๋ username ํ๋๋ง ์ฌ์ฉํฉ๋๋ค.
//์ถ๊ฐ์ ์ผ๋ก ์๋ณ์๋, ์ด๋ฆ ๋ฑ์ ์ ๋ณด๋ฅผ ๋ ์ถ๊ฐํ์
๋ ๋ฉ๋๋ค.
//์ถ๊ฐํ์ค ๊ฒฝ์ฐ .withClaim(ํด๋์ ์ด๋ฆ, ํด๋์ ๊ฐ) ์ผ๋ก ์ค์ ํด์ฃผ์๋ฉด ๋ฉ๋๋ค
.sign(Algorithm.HMAC512(secret));
//์ ํฌ๋ HMAC512 ์๊ณ ๋ฆฌ์ฆ์ ์ฌ์ฉํ์ฌ, ์ ํฌ๊ฐ ์ง์ ํ secret ํค๋ก ์ํธํ ํ ๊ฒ์
๋๋ค.
์ถ๊ฐ๋ก refreshToken์๋ username์ ๋ฃ์ง ์์ต๋๋ค. ์ด๋ AccessToken์ ์ ๋ฐ๊ธ ํ ์ฉ๋๋ก ์ฌ์ฉํ๊ธฐ์, ๊ฐ๊ธ์ ์ ๋๋ ๋ฃ์ง ์๊ณ , DB์ ์ ์ฅํ์ฌ ๊ด๋ฆฌํ๊ฒ ์ต๋๋ค.
4. Token์์ username์ ์ถ์ถํ๋ ์ฝ๋์ ๋๋ค.
JWT.require(Algorithm.HMAC512(secret))
//ํ ํฐ์ ์๋ช
์ ์ ํจ์ฑ์ ๊ฒ์ฌํ๋๋ฐ ์ฌ์ฉํ ์๊ณ ๋ฆฌ์ฆ์ด ์๋
//JWT verifier builder๋ฅผ ๋ฐํํฉ๋๋ค
.build()//๋ฐํ๋ ๋น๋๋ก JWT verifier๋ฅผ ์์ฑํฉ๋๋ค
.verify(accessToken)//accessToken์ ๊ฒ์ฆํ๊ณ ์ ํจํ์ง ์๋ค๋ฉด ์์ธ๋ฅผ ๋ฐ์์ํต๋๋ค.
.getClaim(USERNAME_CLAIM)//claim์ ๊ฐ์ ธ์ต๋๋ค
.asString();
5. accessToken๊ณผ refreshToken์ ํค๋์ ๋ฐ๋์ ๋ ๋ค ์ธํ ํด ์ฃผ์๋๋ฐ, ์ด๋ ์์์ ๊ตฌํํ์๋ฉด ๋ฉ๋๋ค.
ํค๋์๋ง ํ์ค๊ฑฐ๋ฉด ๋ฐ๋์ ์ธํ ํด์ฃผ๋ ๋ถ๋ถ์ ๋นผ์ ๋ ์๊ด์์ต๋๋ค.
ํ ์คํธ์ฝ๋ ์์ฑ
์ด์ ํด๋น ์๋น์ค๊ฐ ์ ์๋ํ๋์ง ํ ์คํธ์ฝ๋๋ฅผ ์์ฑํ๋๋ก ํ๊ฒ ์ต๋๋ค. ์ดํ Filter์ ์ ์ฉํ์ฌ ์ธ์ฆ์ ์งํํ๋๋ก ํ๊ฒ ์ต๋๋ค.
๊ธฐ๋ณธ ๊ตฌ์ฑ
@SpringBootTest
@Transactional
class JwtServiceTest {
@Autowired JwtService jwtService;
@Autowired MemberRepository memberRepository;
@Autowired EntityManager em;
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access.header}")
private String accessHeader;
@Value("${jwt.refresh.header}")
private String refreshHeader;
private static final String ACCESS_TOKEN_SUBJECT = "AccessToken";
private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken";
private static final String USERNAME_CLAIM = "username";
private static final String BEARER = "Bearer ";
private String username = "username";
@BeforeEach
public void init(){
Member member = Member.builder().username(username).password("1234567890").name("Member1").nickName("NickName1").role(Role.USER).age(22).build();
memberRepository.save(member);
clear();
}
private void clear(){
em.flush();
em.clear();
}
private DecodedJWT getVerify(String token) {
return JWT.require(HMAC512(secret)).build().verify(token);
}
AccessToken ๋ฐ๊ธ ํ ์คํธ
@Test
public void createAccessToken_AccessToken_๋ฐ๊ธ() throws Exception {
//given, when
String accessToken = jwtService.createAccessToken(username);
DecodedJWT verify = getVerify(accessToken);
String subject = verify.getSubject();
String findUsername = verify.getClaim(USERNAME_CLAIM).asString();
//then
assertThat(findUsername).isEqualTo(username);
assertThat(subject).isEqualTo(ACCESS_TOKEN_SUBJECT);
}
RefreshToken ๋ฐ๊ธ ํ ์คํธ
@Test
public void createRefreshToken_RefreshToken_๋ฐ๊ธ() throws Exception {
//given, when
String refreshToken = jwtService.createRefreshToken();
DecodedJWT verify = getVerify(refreshToken);
String subject = verify.getSubject();
String username = verify.getClaim(USERNAME_CLAIM).asString();
//then
assertThat(subject).isEqualTo(REFRESH_TOKEN_SUBJECT);
assertThat(username).isNull();
}
refreshToken์ username์ด ์์ด์ผ ํฉ๋๋ค.
RefreshToken ์ ๋ฐ์ดํธ
@Test
public void updateRefreshToken_refreshToken_์
๋ฐ์ดํธ() throws Exception {
//given
String refreshToken = jwtService.createRefreshToken();
jwtService.updateRefreshToken(username, refreshToken);
clear();
Thread.sleep(3000);
//when
String reIssuedRefreshToken = jwtService.createRefreshToken();
jwtService.updateRefreshToken(username, reIssuedRefreshToken);
clear();
//then
assertThrows(Exception.class, () -> memberRepository.findByRefreshToken(refreshToken).get());//
assertThat(memberRepository.findByRefreshToken(reIssuedRefreshToken).get().getUsername()).isEqualTo(username);
}
3์ด๋ฅผ sleep ํ ์ด์ ๋, ์ด๋ฅผ ํด์ฃผ์ง ์์ผ๋ฉด refreshToken์ด ๋๊ฐ์ด ๋ฐ๊ธ๋ ์๊ฐ ์์ต๋๋ค.
RefreshToken ์ ๊ฑฐ
@Test
public void destroyRefreshToken_refreshToken_์ ๊ฑฐ() throws Exception {
//given
String refreshToken = jwtService.createRefreshToken();
jwtService.updateRefreshToken(username, refreshToken);
clear();
//when
jwtService.destroyRefreshToken(username);
clear();
//then
assertThrows(Exception.class, () -> memberRepository.findByRefreshToken(refreshToken).get());
Member member = memberRepository.findByUsername(username).get();
assertThat(member.getRefreshToken()).isNull();
}
AccessToken, RefreshToken ํค๋ ์ค์ ํ ์คํธ
@Test
public void setAccessTokenHeader_AccessToken_ํค๋_์ค์ () throws Exception {
MockHttpServletResponse mockHttpServletResponse = new MockHttpServletResponse();
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
jwtService.setAccessTokenHeader(mockHttpServletResponse, accessToken);
//when
jwtService.sendToken(mockHttpServletResponse,accessToken,refreshToken);
//then
String headerAccessToken = mockHttpServletResponse.getHeader(accessHeader);
assertThat(headerAccessToken).isEqualTo(accessToken);
}
@Test
public void setRefreshTokenHeader_RefreshToken_ํค๋_์ค์ () throws Exception {
MockHttpServletResponse mockHttpServletResponse = new MockHttpServletResponse();
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
jwtService.setRefreshTokenHeader(mockHttpServletResponse, refreshToken);
//when
jwtService.sendAccessAndRefreshToken(mockHttpServletResponse,accessToken,refreshToken);
//then
String headerRefreshToken = mockHttpServletResponse.getHeader(refreshHeader);
assertThat(headerRefreshToken).isEqualTo(refreshToken);
}
ํ ํฐ ์ ์ก ํ ์คํธ
@Test
public void sendToken_ํ ํฐ_์ ์ก() throws Exception {
//given
MockHttpServletResponse mockHttpServletResponse = new MockHttpServletResponse();
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
//when
jwtService.sendToken(mockHttpServletResponse,accessToken,refreshToken);
//then
String headerAccessToken = mockHttpServletResponse.getHeader(accessHeader);
String headerRefreshToken = mockHttpServletResponse.getHeader(refreshHeader);
assertThat(headerAccessToken).isEqualTo(accessToken);
assertThat(headerRefreshToken).isEqualTo(refreshToken);
}
AccessToken ์ถ์ถ ํ ์คํธ
ํ ํฐ ์ ์ก ํ ์คํธ๋ฅผ ๋ง์ณค์ผ๋, ๋ฐ๋ณต๋๋ ํ ํฐ ์ ์ก ์ฝ๋๋ฅผ ์ค์ด๊ธฐ ์ํด ๋ฉ์๋๋ฅผ ํ๋ ์์ฑํ๊ฒ ์ต๋๋ค.
private HttpServletRequest setRequest(String accessToken, String refreshToken) throws IOException {
MockHttpServletResponse mockHttpServletResponse = new MockHttpServletResponse();
jwtService.sendToken(mockHttpServletResponse,accessToken,refreshToken);
String headerAccessToken = mockHttpServletResponse.getHeader(accessHeader);
String headerRefreshToken = mockHttpServletResponse.getHeader(refreshHeader);
MockHttpServletRequest httpServletRequest = new MockHttpServletRequest();
httpServletRequest.addHeader(accessHeader, BEARER+headerAccessToken);
httpServletRequest.addHeader(refreshHeader, BEARER+headerRefreshToken);
return httpServletRequest;
}
ํ ํฐ์ header์ ๋ฃ์ด์ ์ ์กํด์ฃผ๋ ๋ฉ์๋์ ๋๋ค.
์ด์ ํ ์คํธ์ฝ๋๋ฅผ ์์ฑํ๊ฒ ์ต๋๋ค.
@Test
public void extractAccessToken_AccessToken_์ถ์ถ() throws Exception {
//given
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
HttpServletRequest httpServletRequest = setRequest(accessToken, refreshToken);
//when
String extractAccessToken = jwtService.extractAccessToken(httpServletRequest);
//then
assertThat(extractAccessToken).isEqualTo(accessToken);
assertThat(getVerify(extractAccessToken).getClaim(USERNAME_CLAIM).asString()).isEqualTo(username);
}
RefreshToken ์ถ์ถ ํ ์คํธ
@Test
public void extractRefreshToken_RefreshToken_์ถ์ถ() throws Exception {
//given
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
HttpServletRequest httpServletRequest = setRequest(accessToken, refreshToken);
//when
String extractRefreshToken = jwtService.extractRefreshToken(httpServletRequest);
//then
assertThat(extractRefreshToken).isEqualTo(refreshToken);
assertThat(getVerify(extractRefreshToken).getSubject()).isEqualTo(REFRESH_TOKEN_SUBJECT);
}
Username ์ถ์ถ ํ ์คํธ
@Test
public void extractUsername_Username_์ถ์ถ() throws Exception {
//given
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
HttpServletRequest httpServletRequest = setRequest(accessToken, refreshToken);
String requestAccessToken = jwtService.extractAccessToken(httpServletRequest);
//when
String extractUsername = jwtService.extractUsername(requestAccessToken);
//then
assertThat(extractUsername).isEqualTo(username);
}
์ ์ฒด ํ ์คํธ์ฝ๋
@SpringBootTest
@Transactional
class JwtServiceImplTest {
@Autowired
JwtService jwtService;
@Autowired
MemberRepository memberRepository;
@Autowired
EntityManager em;
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access.header}")
private String accessHeader;
@Value("${jwt.refresh.header}")
private String refreshHeader;
private static final String ACCESS_TOKEN_SUBJECT = "AccessToken";
private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken";
private static final String USERNAME_CLAIM = "username";
private static final String BEARER = "Bearer ";
private String username = "username";
@BeforeEach
public void init() {
Member member = Member.builder()
.username(username)
.password("1234567890")
.name("Member1")
.nickName("NickName1")
.role(Role.USER)
.age(22)
.build();
memberRepository.save(member);
clear();
}
private void clear() {
em.flush();
em.clear();
}
private DecodedJWT getVerify(String token) {
return JWT.require(HMAC512(secret)).build().verify(token);
}
@Test
public void createAccessToken_AccessToken_๋ฐ๊ธ() throws Exception {
//given, when
String accessToken = jwtService.createAccessToken(username);
DecodedJWT verify = getVerify(accessToken);
String subject = verify.getSubject();
String findUsername = verify.getClaim(USERNAME_CLAIM).asString();
//then
assertThat(findUsername).isEqualTo(username);
assertThat(subject).isEqualTo(ACCESS_TOKEN_SUBJECT);
}
@Test
public void createRefreshToken_RefreshToken_๋ฐ๊ธ() throws Exception {
//given, when
String refreshToken = jwtService.createRefreshToken();
DecodedJWT verify = getVerify(refreshToken);
String subject = verify.getSubject();
String username = verify.getClaim(USERNAME_CLAIM).asString();
//then
assertThat(subject).isEqualTo(REFRESH_TOKEN_SUBJECT);
assertThat(username).isNull();
}
@Test
public void updateRefreshToken_refreshToken_์
๋ฐ์ดํธ() throws Exception {
//given
String refreshToken = jwtService.createRefreshToken();
jwtService.updateRefreshToken(username, refreshToken);
clear();
Thread.sleep(3000);
//when
String reIssuedRefreshToken = jwtService.createRefreshToken();
jwtService.updateRefreshToken(username, reIssuedRefreshToken);
clear();
//then
assertThrows(Exception.class, () -> memberRepository.findByRefreshToken(refreshToken).get());//
assertThat(memberRepository.findByRefreshToken(reIssuedRefreshToken).get().getUsername()).isEqualTo(username);
}
@Test
public void destroyRefreshToken_refreshToken_์ ๊ฑฐ() throws Exception {
//given
String refreshToken = jwtService.createRefreshToken();
jwtService.updateRefreshToken(username, refreshToken);
clear();
//when
jwtService.destroyRefreshToken(username);
clear();
//then
assertThrows(Exception.class, () -> memberRepository.findByRefreshToken(refreshToken).get());
Member member = memberRepository.findByUsername(username).get();
assertThat(member.getRefreshToken()).isNull();
}
@Test
public void setAccessTokenHeader_AccessToken_ํค๋_์ค์ () throws Exception {
MockHttpServletResponse mockHttpServletResponse = new MockHttpServletResponse();
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
jwtService.setAccessTokenHeader(mockHttpServletResponse, accessToken);
//when
jwtService.sendToken(mockHttpServletResponse,accessToken,refreshToken);
//then
String headerAccessToken = mockHttpServletResponse.getHeader(accessHeader);
assertThat(headerAccessToken).isEqualTo(accessToken);
}
@Test
public void setRefreshTokenHeader_RefreshToken_ํค๋_์ค์ () throws Exception {
MockHttpServletResponse mockHttpServletResponse = new MockHttpServletResponse();
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
jwtService.setRefreshTokenHeader(mockHttpServletResponse, refreshToken);
//when
jwtService.sendToken(mockHttpServletResponse,accessToken,refreshToken);
//then
String headerRefreshToken = mockHttpServletResponse.getHeader(refreshHeader);
assertThat(headerRefreshToken).isEqualTo(refreshToken);
}
@Test
public void sendToken_ํ ํฐ_์ ์ก() throws Exception {
//given
MockHttpServletResponse mockHttpServletResponse = new MockHttpServletResponse();
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
//when
jwtService.sendToken(mockHttpServletResponse,accessToken,refreshToken);
//then
String headerAccessToken = mockHttpServletResponse.getHeader(accessHeader);
String headerRefreshToken = mockHttpServletResponse.getHeader(refreshHeader);
assertThat(headerAccessToken).isEqualTo(accessToken);
assertThat(headerRefreshToken).isEqualTo(refreshToken);
}
private HttpServletRequest setRequest(String accessToken, String refreshToken) throws IOException {
MockHttpServletResponse mockHttpServletResponse = new MockHttpServletResponse();
jwtService.sendToken(mockHttpServletResponse,accessToken,refreshToken);
String headerAccessToken = mockHttpServletResponse.getHeader(accessHeader);
String headerRefreshToken = mockHttpServletResponse.getHeader(refreshHeader);
MockHttpServletRequest httpServletRequest = new MockHttpServletRequest();
httpServletRequest.addHeader(accessHeader, BEARER+headerAccessToken);
httpServletRequest.addHeader(refreshHeader, BEARER+headerRefreshToken);
return httpServletRequest;
}
@Test
public void extractAccessToken_AccessToken_์ถ์ถ() throws Exception {
//given
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
HttpServletRequest httpServletRequest = setRequest(accessToken, refreshToken);
//when
String extractAccessToken = jwtService.extractAccessToken(httpServletRequest);
//then
assertThat(extractAccessToken).isEqualTo(accessToken);
assertThat(getVerify(extractAccessToken).getClaim(USERNAME_CLAIM).asString()).isEqualTo(username);
}
@Test
public void extractRefreshToken_RefreshToken_์ถ์ถ() throws Exception {
//given
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
HttpServletRequest httpServletRequest = setRequest(accessToken, refreshToken);
//when
String extractRefreshToken = jwtService.extractRefreshToken(httpServletRequest);
//then
assertThat(extractRefreshToken).isEqualTo(refreshToken);
assertThat(getVerify(extractRefreshToken).getSubject()).isEqualTo(REFRESH_TOKEN_SUBJECT);
}
@Test
public void extractUsername_Username_์ถ์ถ() throws Exception {
//given
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
HttpServletRequest httpServletRequest = setRequest(accessToken, refreshToken);
String requestAccessToken = jwtService.extractAccessToken(httpServletRequest);
//when
String extractUsername = jwtService.extractUsername(requestAccessToken);
//then
assertThat(extractUsername).isEqualTo(username);
}
}
๋ค์๋ฒ์๋ ํด๋น JwtService๋ฅผ ๊ฐ์ง๊ณ , ์ํ๋ฆฌํฐ ํํฐ์ ์ถ๊ฐํ์ฌ ์ธ์ฆ์ ์งํํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
๊ฐ์ฌํฉ๋๋ค
์ ์ฒด ์ฝ๋๋ ๊นํ๋ธ์์ ํ์ธํ์ค ์ ์์ต๋๋ค.
https://github.com/ShinDongHun1/SpringBoot-Board-API
๐ Reference
์คํ๋ง์์ JWT ์ฌ์ฉ ์ฐธ๊ณ
๊ฒ์ํ ์ฐธ๊ณ
JWT ์ฐธ๊ณ