이전에 Refresh Token을 도입하여 인증을 수행하도록 코드를 개선했다.
그런데 이번엔 중복 로그인 문제가 발생했다.
토큰 발급 이후 AccessToken을 key
, RefreshToken을 value
로만 저장해두었다.
따라서 사용자의 로그인 여부 판단을 위해서는 서버에서 모든 토큰의 subject를 조회해야하는 상황이 된 것이다.
가벼움을 위해 선택한 Redis를 무겁게 쓴다니..?! 말도 안되는 상황이다.
로그인 시도마다 모든 데이터를 조회하고 꺼내오는 것은 서버에 부담이 될 것이라고 판단, 코드를 전면 수정하기로 결정했다.
우선은 Redis의 key-value 형식을 활용하여 토큰 발급 시
loginId - accessToken
, loginId-refreshToken
의 두가지 데이터를 저장하는 방식을 고려해봤다.
loginId를 key로 Token들을 저장해놓는다면 간단한 구현으로 손쉬운 조회가 가능해진다.
하지만 이것은 loginId만으로도 RefreshToken에 접근이 가능해진다는 문제가 있었다.
다음으로 고려한 것은 AccessToken을 key
로, RefreshToken을 value
로 저장하는 방식이었다.
loginId-AccessToken
, AccessToken-RefreshToken
의 데이터를 DB에 저장해놓는다면,
이렇게 한다면 AccessToken이 만료(15분) 될 때 마다 RefreshToken에 접근하는 key값이 갱신되므로 보안상으로 유리할 것이라고 판단했다.
loginId-AccessToken
, AccessToken-RefreshToken
로 변경했다. public String login(LoginRequestDto requestDto) {
String loginId = requestDto.getLoginId();
... // 아이디 패스워드 확인
// 중복 로그인 확인
if (jwtUtil.checkIsLoggedIn(loginId)) {
return jwtUtil.getAccessTokenByLoginId(loginId);
}
// access token 및 refresh token
String accessToken = jwtUtil.createAccessToken(loginId);
jwtUtil.saveAccessTokenByLoginId(loginId, accessToken); // Redis에 저장
String refreshToken = jwtUtil.createRefreshToken(loginId);
jwtUtil.saveRefreshTokenByAccessToken(accessToken, refreshToken); // Redis에 저장
return accessToken;
}
위에서 계획 한 것 처럼 AccessToken으로 RefreshToken을 조회하고
만료시마다 RefreshToken에 접근하는 key값이 변경되도록 코드를 수정했다.
public class JwtAuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(...) {
String accessToken = jwtUtil.getTokenFromRequest(request);
if (Objects.nonNull(accessToken)) {
UserDetailsImpl userDetails;
// accessToken이 만료되었는지 확인
if (jwtUtil.shouldAccessTokenBeRefreshed(accessToken.substring(7))) {
String refreshTokenValue = jwtUtil.getRefreshtokenByAccessToken(accessToken).substring(7);
// refreshtoken이 유효한지 확인
if (jwtUtil.validateToken(refreshTokenValue)) {
// accessToken 재발급
String newAccessToken = jwtUtil.createAccessTokenByRefreshToken(refreshTokenValue);
Cookie cookie = jwtUtil.addJwtToCookie(newAccessToken);
response.addCookie(cookie);
// DB 토큰도 새로고침
jwtUtil.regenerateToken(newAccessToken, accessToken, refreshTokenValue);
// 재발급된 토큰으로 검증 진행하도록 대입
accessToken = newAccessToken;
}
// refreshToken이 유효하지 않다면 재발급 없이 만료된 상태로 진행
}
... // Spring Security 인증 객체 생성
filterChain.doFilter(request, response);
}
}
이제 중복 로그인을 판단하여 Token을 발급할 수 있게 되었다.
JWT의 단점 중 하나인, 발급 이후에는 컨트롤하기 어렵다점은 어느정도 극복 한 것이다.
하지만 이것도 완벽한 방식은 아니다.
AccessToken이 탈취되는 잠깐의 사이에 RefreshToken에 접근하여 사용자의 정보가 노출 될 수 있기 때문이다.
보안을 더 강화를 위해서는 클라이언트에 '인증 정보를 추가로 저장하는 쿠키를 발급'하거나 'HTTPS와 같은 키교환 방식'도 고려해 볼 수 있을 것 같다.