백/spring boot

spring security2 - access, refresh 토큰 응답 및 access 검증

남승현 2024. 8. 12. 17:08

이전까지 들었던 강의는 하나의 Authroization토큰으로만 인증을 진행했는데 access, refresh 토큰 모두 이용해 인증을 진행해 보고 싶어서 처음부터 다시 시작!!

 

 

로그인 성공시, 생명주기와 활용도가 다른 2개의 토큰 발급 (successfulAuthentication())

1) Access 토큰

: 권한이 필요한 모든 요청 헤더에 사용됨. JWT로 탈취 위험을 낮추기 위해 약 10분정도로 짧은 생명주기 가진다

: 헤더에 발급 후 프론트에서 로컬스토리지에 저장

2) Refresh 토큰

: Access 토큰이 만료되었을 때 재발급 받기 위한 용도로만 사용되게 약 24시간 이상의 긴 생명주기 가진다

: 쿠키에 발급 (http Only 쿠키)

 

권한이 알맞다는 가정하에 -> 데이터 응답, 토큰 만료 응답

refresh 토큰으로 access 토큰 재발급

 

클라이언트 측에서는 jwt 토큰을 decode해서 claim에 실린 정보를 얻을 수 있다

-> user_id는 많이 쓰일 것 같기에 claim에 추가하여 토큰을 만들었다

-> user_id는 Authentication에서 접근 불가능하므로 authentication.getPrincipal 즉, UserDetails로부터 얻어와야 한다

<Authentication 인터페이스로 부터 사용할 수 있는 메소드>

1) getPrincipal() : 인증 대상에 관한 정보로 사용자의 아이디 혹은 User객체가 저장

2) getAuthorities() : 인증된 권한 정보 목록

3) getName() : 인증된 사용자의 이름(username) 반환

3) getCredentials() : 인증 확인을 위한 정보로 주로 비밀번호(보안을 위해 인증 후 삭제)

4) getDetails() : 그 밖에 필요한 정보로 IP, 세션정보, 기타 인증요청에서 사용했던 정보들

 

 

<JwtUtil>

//jwt 검증, 발급
@Component
public class JWTUtil {

    private SecretKey secretKey;

    //application.properties에 저장한 암호화 키를 불러와 이를 바탕으로 객체 키 생성해준다
    public JWTUtil(@Value("${spring.jwt.secret}")String secret){
        this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
    }

    //토큰 검증
    public String getUsername(String token) {

        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class);
    }

    public String getRole(String token) {

        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class);
    }

    public String getCategory(String token){
        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("category", String.class);
    }

    public Boolean isExpired(String token) { //토큰이 만료되었는지 검사

        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
    }

    //토큰 생성(로그인 성공시)
    public String createJwt(String category, String username, String role, Long userId,Long expiredMs){
        return Jwts.builder()
                .claim("category",category)
                .claim("username",username)
                .claim("role",role)
                .claim("user_id",userId)
                .issuedAt(new Date(System.currentTimeMillis())) //토큰 생성시간
                .expiration(new Date(System.currentTimeMillis()+expiredMs)) //토큰만료시간
                .signWith(secretKey) //암호화
                .compact();
    }
}

 

<LoginFilter>

//로그인 성공시, jwt 토큰 만들어 반환
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {

    CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();

    Long userId=userDetails.getId();
   String username=authentication.getName();

   Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
   Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
   GrantedAuthority auth=iterator.next();
   String role=auth.getAuthority();



   //토큰 생성
    String access=jwtUtil.createJwt("access", username, role, userId,600000L);
    String refresh=jwtUtil.createJwt("refresh", username, role, userId,86400000L);

    //응답 설정
    response.setHeader("access", access);
    response.addCookie(createCookie("refresh", refresh));
    response.setStatus(HttpStatus.OK.value());

}


    private Cookie createCookie(String key, String value){
        Cookie cookie=new Cookie(key,value);
        cookie.setMaxAge(24*60*60); //쿠키 유효시간
        //cookie.setSecure(true); //https 통신을 하는 경우 사용
        //cookie.setPath("/");  //쿠키가 적용될 범위
        cookie.setHttpOnly(true); //클라이언트에서 자바스크립트단으로 해당 쿠키 접귾하지 못하도록 막기

        return cookie;

    }

 

 

<로그인 성공 후 응답>

 


Access token 검증

 

1) 헤더의 access키에서 토큰 꺼냄

2) 토큰이 비었다면 토큰이 필요없는 요청(ex)login)일 수 있으니 그냥 다음 filter로 넘겨버림

3-1) 토큰이 존재한다면 토큰 만료여부 확인 후 만료되었다면 다음 필터로 넘기지 않고 "token expired" response 보냄

3-2) 토큰이 만료되지 않았다면 access토큰으로부터 username과 role을 얻어 authentication 토큰을 만들고 얘를 SecurityContextHolder 내의 Authentication에 저장 후 다음 필터로 넘김

 

<JWT Filter>

public class JWTFilter extends OncePerRequestFilter {
    //JWTUtil에서 검증할 메소드 가져와야 하므로 의존성 주입
    private final JWTUtil jwtUtil;
    public JWTFilter(JWTUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String requestURI=request.getRequestURI();
        if("/login".equals(requestURI)){
            filterChain.doFilter(request, response);
            return;
        }

        //헤더에서 access 키에 담긴 토큰을 꺼냄
        String accessToken=request.getHeader("access");

        //토큰이 없다면 더이상 밑에 작업 처리 하지 않고 다음 필터로 넘김
        if(accessToken==null){
            filterChain.doFilter(request, response);
            return;
        }

        //토큰이 있다면 토큰 만료여부 확인, 만료시 다음 필터로 넘기지 않고 만료되었음을 response
        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);
            return;
        }

        String username=jwtUtil.getUsername(accessToken);
        String role=jwtUtil.getRole(accessToken);

        User userEntity=new User();
        userEntity.setUsername(username);
        userEntity.setRole(role);
        CustomUserDetails userDetails = new CustomUserDetails(userEntity);

        Authentication authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authToken);

        filterChain.doFilter(request, response);
        
    }
}