백엔드(Back End)/Spring

[TIL]20230713 - RefreshToken

tjsdn9803 2023. 7. 14. 00:46

오늘은 기존 진행하던 프로젝트에 리프레쉬 토큰을 적용해보았다.

JWT는 Stateless한 방식이기 때문에 클라이언트의 토큰이 탈취당하면 서버에서는 알 방법이 없어 공격자가 탈취한 토큰을 가지고 접근할 수 있다는 단점이 있었다. 
이 단점을 보안하기 위한 방법으로는 토큰의 만료시간을 적게 주는 방법이 있다. 하지만 만료시간이 적으면 그만큼 자주 토큰을 발급 받아야하기 때문에 사용자의 불편함을 초래할 수 있다. 이에 제안된것이 리프레쉬 토큰이다.

사용자가 로그인 성공시 Access 토큰과 Refresh토큰을 발급해주고 사용자는 인가가 필요한 요청시 Access토큰과 Refresh토큰을 Header에 담아 요청한다. 

우선 Access토큰을 검사하여 검증이 된다면 그대로 인가를 받고 만약 Access토큰이 만료되었다면 Refresh토큰을 검사하여 검증이 된다면 Access토큰을 재발급해준다.

만약 Access토큰이 탈취 당했다면 공격자는 우선 접근이 가능하지만 Access토큰의 매우 짧은 만료시간이 지나면 사용할 수 없다.

또한 Refresh 토큰이 탈취 당해 공격자가 Refresh 토큰을 가지고 접근한다면 서버의 데이터 베이스에 각 사용자에 1대1로 매핑되는 Access 토큰, Refresh토큰 쌍을 저장하기 때문에 공격자가 Refresh 토큰으로 새로운 Access 토큰을 발급받아 접근하면 데이터베이스의 토큰 쌍과 다른것을 확인하여 알아 차릴 수 있다.

토큰 검증 로직

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {

        String accessToken = jwtUtil.getJwtFromHeader(req, "Access");
        String refreshToken = jwtUtil.getJwtFromHeader(req, "Refresh");

        if(accessToken!= null){
            if(jwtUtil.validateToken(accessToken)){
                setAuthentication(jwtUtil.getUsernameFromToken(accessToken));
            }else if(refreshToken != null){
                boolean isRefreshToken = jwtUtil.refreshTokenValidation(refreshToken);
                if(isRefreshToken){
                    String loginId = jwtUtil.getUsernameFromToken(refreshToken);
                    User user =  userRepository.findByUsername(loginId).orElseThrow(() -> new CustomException(ErrorCode.WRONG_NAME));
                    String newAccessToken = jwtUtil.createToken(loginId, user.getRole() , "Access");
                    res.setHeader("Access_Token", newAccessToken);
                    Claims info = jwtUtil.getUserInfoFromToken(newAccessToken.substring(7));
                    setAuthentication(info.getSubject());
                } else{
                    throw new CustomException(ErrorCode.WRONG_TOKEN);
                }
            }
        }

        filterChain.doFilter(req, res);
    }

우선 Access토큰을 검사하고 검증되었다면 Access토큰의 담긴 유저의 정보를 통해 인증한다. 만약 Access토큰의 검증이 실패한다면 Refresh토큰을 검사하여 검증되었다면 새로운 Access토큰인 newAccessToken을 발급하고 Refresh토큰이 검증되지 않는다면 예외를 발생시키며 인증을 해주지 않는다.

jwtUtil.refreshTokenValidation메소드는 유저와 리프레쉬 토큰 정보를 담고있는 DB를 통해 리프레쉬 토큰을 검증하는 메소드이다.

하지만 리프레쉬 토큰을 사용한 방법도 만약 Access토큰과 Refresh토큰이 모두 탈취당한다면 형 상황에서는 대응할 방법이 없다. 때문에 추가적인 로직으로 보안을 강화하거나 Https를 사용하는 등의 추가적인 조치가 필요할 것이다.