[JWT+] Refresh토큰 생성 및 컨트롤하기

원모어깨찰빵·2024년 3월 27일
0

스프링

목록 보기
15/17

JWTUtil 수정

기존에는 AccessToken만 생성하였기 때문에 토큰의 카테고리가 필요하지 않았지만, 이제 Access토큰과 Refresh토큰으로 분류하여 생성하기 때문에 카테고리 문항을 추가하여야 한다.
JWT생성시 카테고리 추가

public String createJwt(String category, String username, String role, Long expiredMs) { //토큰생성
        return Jwts.builder()
                .claim("category", category) //추가
                .claim("username", username)
                .claim("role", role)
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + expiredMs))
                .signWith(secretKey)
                .compact();
    }

JWT에서 카테고리값 뽑는 코드 추가

public String getCategory(String token){  //토큰의 카테고리 꺼내는 로직 추가
        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("category", String.class);
    }

CustomSuccessHandler 수정

기존의 단일쿠키 생성에서 다중코드 생성으로 수정해야 한다.
기존 코드

        //토큰 생성
        String token = jwtUtil.createJwt(username, role, 60*60*60L);
        response.addCookie(createCookie("Authorization", token));
        response.sendRedirect("http://localhost:3000/");  //프론트의 url에 redirect
}

수정 후

//토큰 생성
        String accessToken = jwtUtil.createJwt("access", username, role, 600000L);  //10분
        String refreshToken = jwtUtil.createJwt("refresh", username, role, 86400000L); //24시간
        //Access토큰은 헤더에, Refresh 토큰은 쿠키에 담아 보내기
        response.setHeader("access", accessToken);
        response.addCookie(createCookie("refresh", refreshToken));
        response.setStatus(HttpStatus.OK.value());  //200으로 프론트에 반환쳐주기
        response.sendRedirect("http://localhost:3000/");  //프론트의 url에 redirect
    }

JWTFilter 수정

JWTFilter의 doFilterInternal 메소드의 코드 수정이 필요하다.
기존에는 Access토큰만 넘어왔지만, 이제 토큰이 분할되어있기 때문에 Access토큰을 가져와서 검정하는 로직으로 수정한다.

@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 헤더에서 access키에 담긴 토큰을 꺼냄
        String accessToken = request.getHeader("access");
// 토큰이 없다면 다음 필터로 넘김 (권한이 필요 없는 요청들이 있을 수 있기 때문)
        if (accessToken == null) {
            filterChain.doFilter(request, response);
            return;
        }
// 토큰 만료 여부 확인, 만료시 다음 필터로 넘기지 않음
        try {
            jwtUtil.isExpired(accessToken);
        } catch (ExpiredJwtException e) {
            //response body
            PrintWriter writer = response.getWriter();
            writer.print("access token expired");
            //response status code(상태반환: 다음 필터로 넘기지 않고 바로 응답 반환)
           response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); //401
            return;
        }
// 토큰이 access인지 확인 (발급시 페이로드에 명시)
        String category = jwtUtil.getCategory(accessToken);
        if (!category.equals("access")) {
            //response body
            PrintWriter writer = response.getWriter();
            writer.print("invalid access token");
            //response status code(상태반환: 다음 필터로 넘기지 않고 바로 응답 반환)            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);//401
            return;
        }
        //토큰에서 username과 role 획득
        String username = jwtUtil.getUsername(accessToken);
        String role = jwtUtil.getRole(accessToken);
        //userDTO를 생성하여 값 set
        UserDTO userDTO = new UserDTO();
        userDTO.setUsername(username);
        userDTO.setRole(role);
        //UserDetails에 회원 정보 객체 담기
        CustomOAuth2User customOAuth2User = new CustomOAuth2User(userDTO);
        //스프링 시큐리티 인증 토큰 생성(로그인 진행)
        Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2User, null, customOAuth2User.getAuthorities());
        //일시적인 세션에 사용자 등록(사용자 요청에 대해서 로그인 상태로 변환됨)        SecurityContextHolder.getContext().setAuthentication(authToken);
        filterChain.doFilter(request, response); //다음 필터로 넘기기
    }

Refresh토큰 검정 후 새로운 Access토큰 발급하는 컨트롤러 로직 작성

@Controller
@ResponseBody
public class ReissueController {
    private final JWTUtil jwtUtil;
    public ReissueController(JWTUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }
    @PostMapping("/reissue")
    public ResponseEntity<?> reissue(HttpServletRequest request, HttpServletResponse response) {
        //get refresh token
        String refresh = null;
        Cookie[] cookies = request.getCookies();
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals("refresh")) {
                refresh = cookie.getValue();
            }
        }
        if (refresh == null) {
            //response status code
            return new ResponseEntity<>("refresh token null", HttpStatus.BAD_REQUEST);
        }
        //expired check
        try {
            jwtUtil.isExpired(refresh);
        } catch (ExpiredJwtException e) {
            //response status code
            return new ResponseEntity<>("refresh token expired", HttpStatus.BAD_REQUEST);
        }
        // 토큰이 refresh인지 확인 (발급시 페이로드에 명시)
        String category = jwtUtil.getCategory(refresh);
        if (!category.equals("refresh")) {
            //response status code
            return new ResponseEntity<>("invalid refresh token", HttpStatus.BAD_REQUEST);
        }
        String username = jwtUtil.getUsername(refresh);
        String role = jwtUtil.getRole(refresh);
        //make new JWT
        String newAccess = jwtUtil.createJwt("access", username, role, 600000L);
        //response
        response.setHeader("access", newAccess);
        return new ResponseEntity<>(HttpStatus.OK);
    }
}

물론 이는 컨트롤러가 아닌 서비스단에서 나누어 구현하여야 한다.

SecurityConfig에서 경로 열어두기

.requestMatchers("/reissue").permitAll()

Reference

개발자 유미 노션
개발자 유미 유튜브

profile
https://fuzzy-hose-356.notion.site/1ee34212ee2d42bdbb3c4a258a672612

0개의 댓글