SpringSecurity + Refresh Token

ys·2024년 3월 27일

SpringSecurity+JWT

목록 보기
3/3

지금까지 만든 토큰을 보면,
1. 로그인 성공 -> 서버측에서 jwt token 발급 -> 클라이언트에게 jwt token header에 담아 발급
2. 권한이 필요한 모든 요청 : 클라이언트 header에 jwt토큰 담아서 서버에 전송을 한다

  • 그런데 우리가, 한 서버에서 여러 작업들을 할 때마다, 클라이언트가 jwt토큰을 서버로 보내야 한다
  • 이렇게 jwt가 계속 요청할 때마다, http 통신을 통해서 서버로 전달된다면... 그만큼 해커에게 token을 탈취당할 가능성이 커지게 된다


🤔Refresh Token이 필수인 이유

  • 그렇기 때문에, refresh token이 필요하다
  • Access 토큰 : 권한이 필요한 모든 요청 헤더에 사용될 JWT로 탈취 위험을 낮추기 위해 약 10분 정도의 짧은 생명주기를 가진다.
  • Refresh 토큰 : Access 토큰이 만료되었을 때 재발급 받기 위한 용도로만 사용되며 약 24시간 이상의 긴 생명주기를 가진다.

권한이 필요한 모든 요청

  • Acess 토큰을 통해 요청을 한다
  • Acess 토큰만 사용하기 때문에, Refresh 토큰은 호출, 전송 빈도가 낮다
  • 권한이 알맞다고 가정
    1. 데이터 응답
    2. token 만료 응답
  • Acess token이 살아 있을때, 요청이 오면 -> 원하는 요청 실행
  • 해커에게 탈취 가능성이 높아, 10분의 짧은 주기를 갖고, 만약 토큰이 만료되었다면 만료응답을 반환
  • 만약 ✅토큰이 만료되었다면, Refresh token을 이용해서 acess 토큰을 새로 발급

Acess token이 만료되었다는 요청을 클라이언트가 받았을 때, 프론트에서는 Refresh token을 가지고 ✅서버의 Refresh 토큰을 받는 특정 경로로 요청을 보내 Acess token을 재발급 받는다

  • 서버측에서는 Refresh token을 검증 후, 새로 Acess 토큰을 발급한다


- Acess, Refresh 토큰의 저장 위치

  • 단일 → 다중 토큰으로 전환하며 자주 사용되는 Access 토큰이 탈취되더라도 생명주기가 짧아 피해 확률이 줄었다.
  • 하지만, Refresh token도 탈취를 당할 수 있다
  • 그렇기에 Refresh token에 대한 보호 방법도 필요하다
  • 클라이언트에서 발급 받은 JWT를 저장하기 위해 로컬 스토리지와 쿠키에 대해 많은 고려를 한다.
  • 각 스토리지에 따른 특징과 취약점은 아래와 같다.
  • 로컬 스토리지 : XSS 공격에 취약함 : Access 토큰 저장
  • httpOnly 쿠키 : CSRF 공격에 취약함 : Refresh 토큰 저장

Access 토큰

  • Access 토큰은 주로 로컬 스토리지에 저장됩니다.
  • 짧은 생명 주기로 탈취에서 사용까지 기간이 매우 짧고, 에디터 및 업로더에서 XSS를 방어하는 로직을 작성하여 최대한 보호 할 수 있지만 CSRF 공격의 경우 클릭 한 번으로 단시간에 요청이 진행되기 때문입니다.
  • 권한이 필요한 모든 경로에 사용되기 때문에 CSRF 공격의 위험보다는 XSS 공격을 받는 게 더 나은 선택일 수 있습니다.
  • -> 그렇기에 Acess 토큰은, 로컬 스토리지에 저장

Refresh 토큰

  • Refresh 토큰은 주로 쿠키에 저장됩니다.
  • 쿠키는 XSS 공격을 받을 수 있지만 httpOnly를 설정하면 완벽히 방어할 수 있습니다.
  • 그럼 가장 중요한 CSRF 공격에 대해 위험하지 않을까라는 의구심이 생깁니다.
  • 하지만 Refresh 토큰의 사용처는 단 하나인 토큰 재발급 경로입니다.
  • CSRF는 Access 토큰이 접근하는 회원 정보 수정, 게시글 CRUD에 취약하지만 토큰 재발급 경로에서는 크게 피해를 입힐 만한 로직이 없기 때문입니다.
  • -> 그렇기에 Refresh 토큰은, httpOnly Cookie에 저장


- Refresh Token 구현

  • 이번 코드는 저번 블로그 글인 https://velog.io/@yys/SpringSecurity-JWT-token 에서 이어집니다
  • 저번 코드에서는 로그인이 성공되었을 때, 단일 토큰을 하나 생성했지만
  • 이제는 로그인이 성공했을때, refresh token까지 생성을 합니다

LoginFilter의 successfulAuthentication

  • 로직은 간단하다!
  • acess, refresh 토큰을 생성해주면 된다!
  • 그리고 acess token은 header에 잘 담아서 Authorization이름으로 전달하고
  • refresh token은 쿠키로 잘 만들어서 응답 해주면 된다
  • 우리는 JwtUtil 클래스에서 token을 생성하는 메서드인 createJwt를 만들었었고
  • 필요한 퀀한, 이름, role은 파라미터인 Authentication에서 가져온다!
@Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {

        // 유저 정보
        String username = authentication.getName();

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

        // jwt 생성
        String access = jwtUtil.createJwt("access", username, role, 60 * 10 * 1000L);
        String refresh = jwtUtil.createJwt("refresh", username, role, 60 * 60 * 24 * 1000L);

        //Refresh 토큰 mysql에 저장
        addRefreshEntity(username, refresh, 86400000L);

        response.addHeader("Authorization","Bearer " + 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);
        //cookie.setPath("/");
        cookie.setHttpOnly(true); // 자바스크립트로 쿠키 접근 할 수 없도록 하기

        return cookie;
    }

추가적으로 JwtUtil creatJwt()메서드 변경

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();
}

public String getCategory(String token) {
      
    return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("category", String.class);
}
  • 이제 토큰이 여러개 이므로 토큰을 category를 이용해 어떤 토큰인지 claim에 정보를 저장해주고
  • category 정보를 찾는 토큰을 jwtUtil에 구현해준다

결과

  • 다음과 같이 잘 발행되는 것을 볼 수 있다!


✅Acess token -> Authentication 생성 (유효 acess token)

  • 이제 클라이언트가 발급받은 Acess token을 가지고, 서버로 요청을 받는다

  • 서버는 요청받은 Acess token에 대해서 유효한지 검사를 하고, 권한을 SecurityContextHolderContext에 저장한다

  • 우리는 Jwt를 검증하는 로직을 JwtFilter에서 작성하였다

JwtFilter

@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final static String HEADER_AUTHORIZATION = "Authorization";
    private final static String TOKEN_PREFIX = "Bearer ";

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

        // request에서 Authorization 헤더를 찾기
        String authorization = request.getHeader(HEADER_AUTHORIZATION);

        // Authorization 헤더 검증 -> jwt token인지
        if (authorization == null || !authorization.startsWith("Bearer ")){
            log.info("token null");
            filterChain.doFilter(request,response);
            // 다음 조건이 해당하면 -> 메서드 종료
            return;
        }

        // 가져온 값에서 접두사 제거 -> 토큰 꺼내 오기
        String token = getAccessToken(authorization);

        if (jwtUtil.validToken(token)){

            // 토큰이 access인지 확인 (발급시 페이로드에 명시)
            String category = jwtUtil.getCategory(token);

            if (!category.equals("access")) {

                //response body
                PrintWriter writer = response.getWriter();
                writer.print("invalid access token");

                //response status code
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return;
            }

            // 토큰에서 username, role 획득
            String email = jwtUtil.getUsername(token);
            String role = jwtUtil.getRole(token);

            // userEntity를 생성해서 해당 값을 넣어준다
            UserEntity userEntity = UserEntity.builder()
                    .email(email)
                    .role(role)
                    .password("temppassword")
                    .build();

            //UserDetails에 회원 정보 객체 담기
            CustomUserDetails customUserDetails = new CustomUserDetails(userEntity);

            //스프링 시큐리티 인증 토큰 생성
            Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());

            //세션에 사용자 등록
            SecurityContextHolder.getContext().setAuthentication(authToken);
        }


        filterChain.doFilter(request, response);
    }

    private String getAccessToken(String authorization){
        if (authorization != null && authorization.startsWith(TOKEN_PREFIX)){
            return authorization.substring(TOKEN_PREFIX.length());
        }
        return null;
    }
}
  • 요청에서 "Authorization" 즉 Acess tokenㅇ르 꺼낸다
  • 먼저 유효한지 검사를 한다
  • 유효한지도 JwtUtilvalidToken으로 구현을 해두었다
  • 이제 getCategory 메서드를 이용해, acess가 아니면 401 에러를 내버린다
  • 그 다음 UsernamePasswordAuthenticationToken을 생성하고
  • 요청이 끝날때까지 적용되는 세션에 사용자를 등록해둔다!
  • doFilter을 통해 다음 filter로 보낸다


✅Acess token 만료 -> refresh token으로 재발급

  • 이제 Acess token의 10분이 지나서 refresh token을 이용해 재발급 하는 로직이다
  • 서버에서 Acess token이 만료됬다는 응답을 보내면
  • 프론트 즉 클라이언트에서는, refresh token을 이용해서 -> ✅Acess token을 만드는 특정 경로로 다시 요청을 보내준다
  • 이번 프로젝트에서는 간단하게 controller를 구현해 보겠다

ReissueController

@RestController
@RequiredArgsConstructor
public class ReissueController {

    private final JwtUtil jwtUtil;
    @PostMapping("/reissue")
    public Api<?> reissue(HttpServletRequest request, HttpServletResponse response){

        String refresh = refreshTokenService.findRefreshCookie(request);

        if (refresh == null) {

            return Api.Error(TokenErrorCode.NULL_REFRESH_TOKEN);

        }
        //expired check
        try {
            jwtUtil.isExpired(refresh);
        } catch (TokenException e) {

            //response status code
            return Api.Error(TokenErrorCode.EXPIRED_REFRESH_TOKEN);
        }

        // 토큰이 refresh인지 확인 (발급시 페이로드에 명시)
        String category = jwtUtil.getCategory(refresh);

        if (!category.equals("refresh")) {

            //response status code
            return Api.Error(TokenErrorCode.REFRESH_TOKEN_EXCEPTION);
        }

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

        //make new JWT
        String newAccess = jwtUtil.createJwt("access", username, role, 600000L);

        //response
        response.addHeader("Authorization","Bearer " + newAccess);

        return Api.OK(null);
    }
}
  • 먼저 쿠키에서 refresh token을 가져온다
  • 다음 refresh token에서 getCategory를 통해
  • refresh로 저장되어있지 않으면! 예외를 내준다
  • 다음 refresh token에서, 이름, 역할을 가져온 후에 다시 acess token을 만들어준다
  • 그다음 헤더에 담아서 응답을 해준다

SpringSecurity “/reissue” 경로 permitAll

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

- Refresh Rotate

  • 처음에 말했듯이, refresh token도 탈취를 당할 수 있다
  • 그래서 refresh token에서 acess token을 재발급하면
  • 다시 refresh token도 재발급을 해서 ✅지속시간도 늘리고, 보안성도 강화할 수 있다

저번에 발급했던 🤔refresh token은 삭제되는게 아님!!!

  • 전에 발급된 refresh token에 대한 blacklist 로직을 추가해야 한다

ReissueController

@RestController
@RequiredArgsConstructor
public class ReissueController {

    private final JwtUtil jwtUtil;
    @PostMapping("/reissue")
    public Api<?> reissue(HttpServletRequest request, HttpServletResponse response){

        String refresh = refreshTokenService.findRefreshCookie(request);

        if (refresh == null) {

            return Api.Error(TokenErrorCode.NULL_REFRESH_TOKEN);

        }
        //expired check
        try {
            jwtUtil.isExpired(refresh);
        } catch (TokenException e) {

            //response status code
            return Api.Error(TokenErrorCode.EXPIRED_REFRESH_TOKEN);
        }

        // 토큰이 refresh인지 확인 (발급시 페이로드에 명시)
        String category = jwtUtil.getCategory(refresh);

        if (!category.equals("refresh")) {

            //response status code
            return Api.Error(TokenErrorCode.REFRESH_TOKEN_EXCEPTION);
        }

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

        //make new JWT
        String newAccess = jwtUtil.createJwt("access", username, role, 600000L);
        String newRefresh = jwtUtil.createJwt("refresh", username, role, 86400000L);

        //response
        response.addHeader("Authorization","Bearer " + newAccess);
        response.addCookie(createCookie("refresh", newRefresh));

        return Api.OK(null);
    }
    
    private Cookie createCookie(String key, String value) {

    Cookie cookie = new Cookie(key, value);
    cookie.setMaxAge(24*60*60);
    //cookie.setSecure(true);
    //cookie.setPath("/");
    cookie.setHttpOnly(true);

    return cookie;
}
  • 위에서 만든 ReissueController에, refresh token을 이용해서 acess token을 만들 때, refresh token도 새로 만들어준다
  • 그 다음 refresh token도 쿠키에 담아서 다시 응답해준다
  • 이 때, 전에 만들어진 refresh token에 대해서는 blacklist 처리를 해줘야한다

🤔서버측에 refresh token 저장

✅token에 대한 서버측 주도권

  • 단순하게 JWT를 발급하여 클라이언트측으로 전송하면 인증/인가에 대한 주도권 자체가 클라이언트측에 맡겨진다
  • 만약 Jwt token이 탈취 당했다고 가정을 해보자...!!!
  • JWT가 만료되기 까지 서버측에서는 그것을 막을 수 없으며,
  • 프론트측에서 토큰을 삭제하는 로그아웃을 구현해도 이미 복제가 되었다면 피해를 입을 수 있다.

그렇기 때문에!!! ✅서버측에서 token에 대한 주도권을 가져야 한다

일반적으로 서버측에 주도권을 잡는 방법에는
1. redis를 이용해 캐시에 저장
2. rdbrefresh token을 저장
이 있다

  • 나는 아직 redis에 익숙하지 않으므로,,, rdb인 mysql에 저장을 하겠다
  • 다음에는 redis를 이용해 프로젝트를 구현해 보자!!!
  • ✅redis를 이용하면 TTL설정을 통해 생명주기가 끝난 토큰을 삭제할 수 있다는 장점이 있다고 한다!

✅구현 방법

  • 발급시
    • Refresh 토큰을 서버측 저장소에 저장
  • 갱신시 (Refresh Rotate)
    • 기존 Refresh 토큰을 삭제하고 새로 발급한 Refresh 토큰을 저장

RefreshEntity

@Entity
@Getter
@Setter
public class RefreshEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;
    private String refresh;
    private String expiration;
}

RefreshRepository

public interface RefreshRepository extends JpaRepository<RefreshEntity, Long> {

    Boolean existsByRefresh(String refresh);

    @Transactional
    void deleteByRefresh(String refresh);
}

LoginFilter/successfulAuthentication

@Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {

        // 유저 정보
        String username = authentication.getName();

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

        // jwt 생성
        String access = jwtUtil.createJwt("access", username, role, 60 * 10 * 1000L);
        String refresh = jwtUtil.createJwt("refresh", username, role, 60 * 60 * 24 * 1000L);

        //Refresh 토큰 mysql에 저장
        addRefreshEntity(username, refresh, 86400000L);

        response.addHeader("Authorization","Bearer " + access);
        response.addCookie(createCookie("refresh", refresh));
        response.setStatus(HttpStatus.OK.value());
    }
    
    private void addRefreshEntity(String email, String refresh, Long expiredMs) {

        Date date = new Date(System.currentTimeMillis() + expiredMs);

        RefreshEntity refreshEntity = RefreshEntity.builder()
                .email(email)
                .refresh(refresh)
                .expiration(date.toString())
                .build();
        refreshRepository.save(refreshEntity);
    }
  • 로그인이 성공해서 refresh token을 만들 때
  • db에 refresh token을 저장해준다!
  • RefreshRepository를 의존성 주입해준다

ReissueController

@RestController
@RequiredArgsConstructor
public class ReissueController {

    private final JwtUtil jwtUtil;
    private final RefreshTokenService refreshTokenService;
    private final RefreshRepository refreshRepository;
    @PostMapping("/reissue")
    public Api<?> reissue(HttpServletRequest request, HttpServletResponse response){

        String refresh = refreshTokenService.findRefreshCookie(request);

        if (refresh == null) {

            return Api.Error(TokenErrorCode.NULL_REFRESH_TOKEN);

        }
        //expired check
        try {
            jwtUtil.isExpired(refresh);
        } catch (TokenException e) {

            //response status code
            return Api.Error(TokenErrorCode.EXPIRED_REFRESH_TOKEN);
        }

        // 토큰이 refresh인지 확인 (발급시 페이로드에 명시)
        String category = jwtUtil.getCategory(refresh);

        if (!category.equals("refresh")) {

            //response status code
            return Api.Error(TokenErrorCode.REFRESH_TOKEN_EXCEPTION);
        }

        // DB에 저장되어 있는지 확인
        Boolean isExist = refreshRepository.existsByRefresh(refresh);
        if (!isExist) {

            //response body
            return Api.Error(TokenErrorCode.INVALID_REFRESH_TOKEN);
        }

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

        //make new JWT
        String newAccess = jwtUtil.createJwt("access", username, role, 600000L);
        String newRefresh = jwtUtil.createJwt("refresh", username, role, 86400000L);

        //Refresh 토큰 저장 DB에 기존의 Refresh 토큰 삭제 후 새 Refresh 토큰 저장
        refreshRepository.deleteByRefresh(refresh);
        addRefreshEntity(username, newRefresh, 86400000L);


        //response
        response.addHeader("Authorization","Bearer " + newAccess);
        response.addCookie(createCookie("refresh", newRefresh));

        return Api.OK(null);
    }
    private void addRefreshEntity(String email, String refresh, Long expiredMs) {

        Date date = new Date(System.currentTimeMillis() + expiredMs);

        RefreshEntity refreshEntity = RefreshEntity.builder()
                .email(email)
                .refresh(refresh)
                .expiration(date.toString())
                .build();
        refreshRepository.save(refreshEntity);
    }
    private Cookie createCookie(String key, String value) {

        Cookie cookie = new Cookie(key, value);
        cookie.setMaxAge(24*60*60);
        //cookie.setSecure(true);
        //cookie.setPath("/");
        cookie.setHttpOnly(true);

        return cookie;
    }
}
  • 아까 만들었던 reissue controller에서 새로운 refresh token을 발급받으면
  • 기존의 refresh token을 삭제한다

이번 프로젝트에서는 간단한 경우이기 때문에 repository를 의존성을 주입받았다
프로젝트가 복잡해지고, 요구 사항이 많아지면 -> 🤔 Service계층을 구현해서 @Transactional을 통해 원자성을 보장해주자!



✅로그아웃 기능

  • 마지막으로 로그아웃 기능을 추가해주자
  • 로그아웃 기능을 사용하면, JWT 탈취 시간을 줄일 수 있다
  • 로그아웃 버튼 클릭시
    • 프론트엔드측 : 로컬 스토리지에 존재하는 Access 토큰 삭제서버측 로그아웃 경로로 Refresh 토큰 전송
    • 백엔드측 : 로그아웃 로직을 추가하여 Refresh 토큰을 받아 쿠키 초기화Refresh DB에서 해당 Refresh 토큰 삭제 (모든 계정에서 로그아웃 구현시 username 기반으로 모든 Refresh 토큰 삭제)

✅백엔드에서 로그아웃 수행 작업
1. DB에 저장하고 있는 Refresh 토큰 삭제
2. Refresh 토큰 쿠키 null로 변경

LogoutFilter

@RequiredArgsConstructor
public class CustomLogoutFilter extends GenericFilterBean {

    private final JwtUtil jwtUtil;
    private final RefreshRepository refreshRepository;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
    }

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {

        //path and method verify
        String requestUri = request.getRequestURI();
        if (!requestUri.matches("^\\/logout$")) {

            filterChain.doFilter(request, response);
            return;
        }
        String requestMethod = request.getMethod();
        if (!requestMethod.equals("POST")) {

            filterChain.doFilter(request, response);
            return;
        }

        //get refresh token
        String refresh = null;
        Cookie[] cookies = request.getCookies();
        for (Cookie cookie : cookies) {

            if (cookie.getName().equals("refresh")) {

                refresh = cookie.getValue();
            }
        }

        //refresh null check
        if (refresh == null) {

            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }

        //expired check
        try {
            jwtUtil.isExpired(refresh);
        } catch (ExpiredJwtException e) {

            //response status code
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }

        // 토큰이 refresh인지 확인 (발급시 페이로드에 명시)
        String category = jwtUtil.getCategory(refresh);
        if (!category.equals("refresh")) {

            //response status code
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }

        //DB에 저장되어 있는지 확인
        Boolean isExist = refreshRepository.existsByRefresh(refresh);
        if (!isExist) {

            //response status code
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }

        //로그아웃 진행
        //Refresh 토큰 DB에서 제거
        refreshRepository.deleteByRefresh(refresh);

        //Refresh 토큰 Cookie 값 0
        Cookie cookie = new Cookie("refresh", null);
        cookie.setMaxAge(0);
        cookie.setPath("/");

        response.addCookie(cookie);
        response.setStatus(HttpServletResponse.SC_OK);
    }
  • 로그아웃 요청과, Post방식으로 요청이 오면!
  • request에서 refresh token을 가져온다
  • 그 후 여러 인증 절차를 갖고(refreh token인증과 같다)
  1. db에서 ✅deleteByRefresh 메서드를 이용해 refresh token db에서 삭제
  2. ✅cookie의 maxAge를 0으로 설정해, refresh token을 만료시킨다

- SecurityConfig

  • 해당 custom한 logout filter을 securityConfig에 등록한다
 http
.addFilterBefore(new CustomLogoutFilter(jwtUtil, refreshRepository), LogoutFilter.class);
  • 메서드 체이닝과 지금까지 filter등록을 모두 합친 SecurityConfig
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final AuthenticationConfiguration authenticationConfiguration;
    private final JwtUtil jwtUtil;
    private final RefreshRepository refreshRepository;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{


        return http
                // csrf disable
                .csrf((auth) -> auth.disable())
                // Form 로그인 방식 disable -> jwt 인증 방식을 사용할 것이기 때문에
                .formLogin((auth) -> auth.disable())
                // http basic 인증 방식 disable
                .httpBasic((auth) -> auth.disable())
                // 경로별 인가 작업
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/login", "/", "/join").permitAll()
                        .requestMatchers("/admin").hasRole("ADMIN")
                        .requestMatchers("/reissue").permitAll()
                        .anyRequest().authenticated())
                .addFilterBefore(new JwtFilter(jwtUtil), LoginFilter.class)
                .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil,refreshRepository), UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new JwtExceptionHandler(), JwtFilter.class)
                .addFilterBefore(new CustomLogoutFilter(jwtUtil, refreshRepository), LogoutFilter.class)
                // jwt는 세션을 stateless하게 관리한다
                .sessionManagement((session) -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .build();

    }
    // authenticationManager을 Bean으로 등록!
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception{
        return configuration.getAuthenticationManager();
    }
}

👌결과

  • 먼저 만료된 토큰을 이용해 admin controller에 접근한다
  • reissue 경로로 front에서 보내주었다고 가정
    • 내가 postman을 통해 해당 경로를 실행!
  • 성공적으로 진행되었고, issue token과 refresh token 모두 생겼다.
  • db에도 id가 8인 refresh token이 잘 저장되었다
  • 로그아웃 로직을 실행해보겠다
  • 200 Ok가 나오고
  • 8번 refresh token이 삭제됨을 볼 수 있다

🤔이후 보충사항 + 계획!

물론 이번 구현에서는... refresh token이 하루가 지나고 삭제되는 스케줄러 구현은 하지 못하였다

  • db에 refresh token이 쌓이는 문제가 생긴다...
  • 다음에는 refresh token이 만료되면 -> 스케쥴러를 통해서 db에서 삭제하는 로직을 구현해 보겠다
  • redis를 이용하면 TTL설정을 통해, 자동으로 토큰을 삭제할 수 있다고 한다
  • 다음 프로젝트에서는 많은 사람들이 사용하는 redis를 이용하는 refresh token을 구현해 보겠다
profile
개발 공부,정리

0개의 댓글