Refresh Token 적용 (2)

김재현·2024년 1월 10일
0

TIL

목록 보기
73/88
post-thumbnail

문제 상황 : 중복 로그인

이전에 Refresh Token을 도입하여 인증을 수행하도록 코드를 개선했다.

그런데 이번엔 중복 로그인 문제가 발생했다.

토큰 발급 이후 AccessToken을 key, RefreshToken을 value로만 저장해두었다.
따라서 사용자의 로그인 여부 판단을 위해서는 서버에서 모든 토큰의 subject를 조회해야하는 상황이 된 것이다.

가벼움을 위해 선택한 Redis를 무겁게 쓴다니..?! 말도 안되는 상황이다.
로그인 시도마다 모든 데이터를 조회하고 꺼내오는 것은 서버에 부담이 될 것이라고 판단, 코드를 전면 수정하기로 결정했다.

해결 방안

1. loginId-AccessToken / loginId-RefreshToken (x)

우선은 Redis의 key-value 형식을 활용하여 토큰 발급 시
loginId - accessToken, loginId-refreshToken의 두가지 데이터를 저장하는 방식을 고려해봤다.

loginId를 key로 Token들을 저장해놓는다면 간단한 구현으로 손쉬운 조회가 가능해진다.

하지만 이것은 loginId만으로도 RefreshToken에 접근이 가능해진다는 문제가 있었다.

2. loginId-AccessToken / AccessToken-RefreshToken (o)

다음으로 고려한 것은 AccessToken을 key로, RefreshToken을 value로 저장하는 방식이었다.

loginId-AccessToken, AccessToken-RefreshToken의 데이터를 DB에 저장해놓는다면,

  1. loginId로 접근 시 현재 AccessToken이 발급되어 있는지 확인하여 로그인 여부를 판단 가능
  2. 발급되어 있는 Token이 있다면 그것을 그대로 반환, 만료되었다면 새로 발급하여 반환

이렇게 한다면 AccessToken이 만료(15분) 될 때 마다 RefreshToken에 접근하는 key값이 갱신되므로 보안상으로 유리할 것이라고 판단했다.

구현

UserService

  1. 로그인 메서드에서 loginId로 중복 로그인 여부를 판단하며
  2. 토큰 저장 방식을 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;
  }

AuthFilter

위에서 계획 한 것 처럼 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와 같은 키교환 방식'도 고려해 볼 수 있을 것 같다.


관련 포스팅

Previous Post

profile
I live in Seoul, Korea, Handsome

0개의 댓글