
JWT๋ JSON Web Token์ ์ฝ์๋ก, ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ์ ์ง์ํ๋ ๊ฒ
โ ํ ํฐ์ ์ธ์ฆ์ ์ฌ์ฉ (์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ฅผ ๊ตฌ๋ถํ๊ธฐ ์ํ ์ ์ผํ ๊ฐ)
์๋ฒ์์ ํ ํฐ์ ์์ฑํ์ฌ ํด๋ผ์ด์ธํธ์๊ฒ ์ ๋ฌ โ ํด๋ผ์ด์ธํธ๋ ํ ํฐ์ ๊ฐ์ง๊ณ ์๋ค๊ฐ ์ฌ๋ฌ ์์ฒญ๊ณผ ํ ํฐ์ ํจ๊ป ๋ณด๋ โ ์๋ฒ์์ ์ ํจ์ฑ ๊ฒ์ฌ ํ ํ ํฐ์ด ์ ํจํ๋ค๋ฉด ํด๋ผ์ด์ธํธ์ ์์ฒญ์ ์ฑ๊ณต์ ์ผ๋ก ์ฒ๋ฆฌ
header / payload / signature
์ธ๊ฐ์ง๋ก ์ด๋ฃจ์ด์ ธ ์์ผ๋ฉฐ, ๊ฐ๊ฐ์ โ.โ์ผ๋ก ๊ตฌ๋ถํ๋ค. (ํค๋, ๋ด์ฉ, ์๋ช )
JWT๋ฅผ ๋๋ฒ๊น ํ ์ ์๋ ์ฌ์ดํธ
https://jwt.io/#debugger-io

ํ ํฐ์ ํ์ (type)๊ณผ ํด์ฑ ์๊ณ ๋ฆฌ์ฆ(alg)๋ฅผ ์ง์
ํ ํฐ์ ๋ด์ ์ ๋ณด
ํ ํฐ์ ๋ด์ ํ๋์ ์ ๋ณด, ํ ์กฐ๊ฐ์ ์ ๋ณด๋ฅผ ํด๋ ์(Claim)์ด๋ผ๊ณ ํ๋ค.
๋ฑ๋ก๋ ํด๋ ์ (registered claim)
๊ณต๊ฐ ํด๋ ์ (public claim)
๋น๊ณต๊ฐ ํด๋ ์ (private claim)
{
"sub": "accessToken", // ๋ฑ๋ก๋ ํด๋ ์ (์ ๋ชฉ)
"name": "tester", // ๋น๊ณต๊ฐ ํด๋ ์
"email" : "test@test.com" // ๋น๊ณต๊ฐ ํด๋ ์
"iat": 2020233022 // ๋ฑ๋ก๋ ํด๋ ์ (๋ฐ๊ธ๋ ์๊ฐ)
}
ํ ํฐ์ ์ธ์ฝ๋ฉํ๊ฑฐ๋ ์ ํจ์ฑ ๊ฒ์ฆ์ ํ ๋ ์ฌ์ฉํ๋ ๊ณ ์ ํ ์ํธํ ์ฝ๋
์๋ช ์ ์์์ ๋ง๋ ย ํค๋์ ํ์ด๋ก๋์ ๊ฐ์ ๊ฐ๊ฐ BASE64๋ก ์ธ์ฝ๋ฉํ๊ณ , ์ธ์ฝ๋ฉ ํ ๊ฐ์ ๋น๋ฐ ํค๋ฅผ ์ด์ฉํ์ฌ ํค๋์์ ์ ํ ์๊ณ ๋ฆฌ์ฆ์ผ๋ก ํด์ฑ์ ํ ํ ๋ค์ BASE64๋ก ์ธ์ฝ๋ฉ ํ์ฌ ์์ฑ
Stateless ์๋ฒ๋ ํ ๊ฐ์ง ๋ฌธ์ ์ ์ด ์กด์ฌ โ ์ธ์ ๊ธฐ๋ฐ์ ์ธ์ฆ๊ณผ ๊ฐ์ด ์๋ฒ ์ชฝ์์ ๊ด๋ฆฌ๊ฐ ๋๊ณ ์์ง ์๊ธฐ ๋๋ฌธ์ ์ฌ๋ฐ๋ฅธ ์ฌ์ฉ์์ธ์ง, ์๋๋ฉด ๋๊ตฐ๊ฐ ํ ํฐ์ ํ์ทจํด ์ ์์ ์ธ ์๋๋ก ์ ๊ทผํด ์จ ์ฌ์ฉ์์ธ์ง ์์ง ๋ชปํ๋ค๋ ๊ฒ
๋๋ฌธ์ ์์ธ์ค ํ ํฐ์ ์ ํจ ๊ธฐ๊ฐ์ ์์ ์งง๊ฒ ์ค์ ํ๋ ๊ฒ์ผ๋ก ๋๋ฆ ๋ณด์์ ๊ฐํํ ์ ์์ง๋ง, ๊ทธ๋ฌ๋ฉด ์ฌ์ฉํ๋ ์ ์ฅ์์ ๋ถํธํจ์ ๋๋ ์ ๋ฐ์ ์๋ค.
โ ์ด๋ฅผ ํด๊ฒฐํ ์ ์๋ ๊ฒ์ด Refresh Token
๋ฆฌํ๋ ์ฌ ํ ํฐ์ ์์ธ์ค ํ ํฐ์ด ๋ง๋ฃ๋์์ ๋, ์์ธ์ค ํ ํฐ์ ์๋ก ๋ฐ๊ธํ๊ธฐ ์ํ ํ ํฐ
์์ธ์ค ํ ํฐ๊ณผ ๋ค๋ฅธ ์ ์ ๋ฆฌํ๋ ์ ํ ํฐ์ ์์ธ์ค ํ ํฐ๋ณด๋ค ์ ํจ๊ธฐ๊ฐ์ด ๊ธธ๊ณ , ๋ฐ์ดํฐ ๋ฒ ์ด์ค์ ์ ์ฅ
ํด๋ผ์ด์ธํธ ์ธก์์ ์๋ฒ์ ์ธ์ฆ ์์ฒญ
์๋ฒ์ธก์์ ์์ธ์ค ํ ํฐ๊ณผ ๋ฆฌํ๋ ์ ํ ํฐ ๋ ๊ฐ๋ฅผ ํด๋ผ์ด์ธํธ ์ธก์ ์ ์ก
ํด๋ผ์ด์ธํธ๋ ์ ์ก๋ฐ์ ํ ํฐ์ ์ ์ฅ
์๋ฒ์์๋ ๋ฆฌํ๋ ์ ํ ํฐ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ
ํด๋ผ์ด์ธํธ ์ธก์์ ์ธ์ฆ์ด ํ์ํ API ์์ฒญ์ ์๋ฒ์ธก์ ๋ณด๋ผ ๋๋ง๋ค ์์ธ์ค ํ ํฐ์ ๊ฐ์ด ์ ์ก
์๋ฒ ์ธก์์๋ ์ ๋ฌ๋ฐ์ ์์ธ์ค ํ ํฐ์ ๋ํ ์ ํจ์ฑ ๊ฒ์ฌ ํ ์ ํจํ๋ค๋ฉด ํด๋ผ์ด์ธํธ ์์ฒญ์ ์ฒ๋ฆฌ. ๋ง์ฝ ๋ง๋ฃ๋์๋ค๋ฉด, ํ ํฐ์ด ๋ง๋ฃ๋์๋ค๋ ์๋ฌ๋ฅผ ํด๋ผ์ด์ธํธ์๊ฒ ์ ๋ฌ
์๋ฌ๋ฅผ ์ ๋ฌ๋ฐ์ ํด๋ผ์ด์ธํธ๋ ๋ฆฌํ๋ ์ ํ ํฐ๊ณผ ํจ๊ป ์ ์์ธ์ค ํ ํฐ์ ์๋ฒ ์ธก์ ์์ฒญ
์๋ฒ์์๋ ๋ฆฌํ๋ ์ ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ์ํด ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๋ฆฌํ๋ ์ ํ ํฐ์ ์กฐํ ํ ๋น๊ต
์ ํจํ๋ค๋ฉด ์ ์์ธ์ค ํ ํฐ์ ๋ฐ๊ธํ๊ณ ํด๋ผ์ด์ธํธ์๊ฒ ์ ๋ฌ. ํด๋ผ์ด์ธํธ๋ ์ ์์ธ์ค ํ ํฐ์ ๊ฐ์ง๊ณ ์๋ ์์ฒญํ๋ ค๋ API๋ฅผ ๋ค์ ์์ฒญ
์ธ์ฆ์์ ์ฌ์ฉ์๊ฐ ์๊ฒฉ์ฆ๋ช ์ ์ฌ์ฉํ์ฌ ์ฑ๊ณต์ ์ผ๋ก ๋ก๊ทธ์ธํ๋ฉด JSON ์น ํ ํฐ์ด ๋ฐํ๋๋ค. ํ ํฐ์ ์๊ฒฉ์ฆ๋ช ์ ์ฃผ ๋ชฉ์ ์ผ๋ก ์ฌ์ฉํ๋ฏ๋ก ๋ณด์ ๋ฌธ์ ์ ์ฃผ์๊ฐ ํ์ํ๋ค.
@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;
}
}
}
ํํฐ ์๊ตฌ์ฌํญ
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()))
);
}
}
์ฐธ๊ณ