본문 바로가기

Backend Development/Spring boot

[Spring boot] refresh token 갱신 시 DB 저장값과 Cookie 값 안맞을 경우

Interceptor에서 preHandle로 request를 잡아서 refresh token을 재 생성하여 response의 cookie를 변경할경우 cookie write 완료 전에 들어온 request들은 새로 변한 값이 아닌 이전 값으로 올때가 있다. 즉 http request가 동시에 여러개가 올 경우에는 reponse 변경값이 적용되기 전에 요청이 올수가 있다.

 

이럴 경우에는 refresh token 변조 확인을 위해 DB의 token 값을 읽을 때 생성된지 얼마 안된 토큰은 skip하고 일정 threshold (예: 2초) 지난후 token부터 진위여부를 체크한다.

 

 

   public ApiResponse refreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // access token 확인
        String accessToken = HeaderUtil.getAccessTokenCookie(request);
        String refreshToken = CookieUtil.getCookie(request, REFRESH_TOKEN)
            .map(Cookie::getValue)
            .orElse((null));

        if (accessToken == null || refreshToken == null) {
            return new ApiResponse(new ApiResponseHeader(ApiResponse.FAILED, ApiResponse.EMPTY_TOKEN), null);
        }

        if (tokenProvider.validateToken(accessToken) == null) {
            return ApiResponse.invalidAccessToken();
        }
        String userId = tokenProvider.getUserNameFromToken(accessToken);

        // refresh token
        if (tokenProvider.validateToken(refreshToken) == null) {
            return ApiResponse.invalidRefreshToken();
        }

        Date now = new Date();
        long tokenExpirationMsec = appProperties.getAuth().getTokenExpirationMsec();
        String newAccessToken = tokenProvider.createToken(userId, tokenExpirationMsec);

        int cookieMaxAge = (int) tokenExpirationMsec / 60;
        CookieUtil.deleteCookie(request, response, ACCESS_TOKEN);
        CookieUtil.addCookie(response, ACCESS_TOKEN, newAccessToken, cookieMaxAge);

        long validTime = tokenProvider.validateToken(refreshToken).getExpiration().getTime() - now.getTime();

        // refresh 토큰 기간이 3일 이하로 남은 경우, refresh 토큰 갱신
        if (validTime <= THREE_DAYS_MSEC) {
            // userId refresh token 으로 DB 확인
            SecurityUserVO oneUserInfo = securityUserMapper.getOneUserInfo(userId, null);
            long tokenCreationAging = Math.abs(now.getTime() - (tokenProvider.validateToken(oneUserInfo.getRefreshToken()).getExpiration().getTime() - appProperties.getAuth().getRefreshTokenExpirationMsec()));
            
            // DB의 userInfo에 저장된 최신 token을 읽어서 2초 (2000msec) 이후 지난 경우에 진위 체크를 진행한다.
            ////////////////////////////////////////////////////////////////////////////////////////
            if ((tokenCreationAging > 2000) && !refreshToken.equals(oneUserInfo.getRefreshToken())) {

                // 클라이언트에서 넘어온 refreshToken이 DB에 저장되어 있지 않다면 위조로 판단하고 로그인으로 Redirect 시킨다.
                return ApiResponse.invalidRefreshToken();
            }
            ////////////////////////////////////////////////////////////////////////////////////////
            
            // refresh 토큰 설정
            long refreshTokenExpirationMsec = appProperties.getAuth().getRefreshTokenExpirationMsec();
            String newRefreshToken = tokenProvider.createToken(userId, refreshTokenExpirationMsec);

            // DB에 refresh 토큰 업데이트
            Map<String, Object> paramMap = new HashMap<>();
            paramMap.put("userId", userId);
            paramMap.put("refreshToken", newRefreshToken);
            securityUserMapper.updateRefreshToken(paramMap);

            cookieMaxAge = (int) refreshTokenExpirationMsec / 60;
            CookieUtil.deleteCookie(request, response, REFRESH_TOKEN);
            CookieUtil.addCookie(response, REFRESH_TOKEN, newRefreshToken, cookieMaxAge);
        }

        return ApiResponse.success("access_token", newAccessToken);
    }