달려LIKE추추
내 머리 속의 지우개🦶
달려LIKE추추
전체 방문자
오늘
어제
  • 분류 전체보기
    • Java
    • 끄적임
    • 네트워크
    • Spring
      • JPA
      • Security
      • WebFlux
      • Cloud
    • Web

블로그 메뉴

  • 홈
  • 방명록

공지사항

인기 글

태그

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
달려LIKE추추
Spring/Security

SpringSecurity와 Redis를 사용해 JWT를 구현해보자!

SpringSecurity와 Redis를 사용해 JWT를 구현해보자!
Spring/Security

SpringSecurity와 Redis를 사용해 JWT를 구현해보자!

2022. 10. 2. 17:32

이전 포스팅에서 JWT의 보안 전략과 각각의 장단점에 대해서 알아보았습니다. 이번 포스팅에서는 AccessToken과 RefreshToken을 사용한 JWT를 실제 구현한 코드를 보면서 다뤄보고자 합니다. Token의 생성과 재발급 및 삭제 관련 내용을 위주로 다뤄 보고자 합니다. 전체 코드가 궁금하신 분들을 위해 아래에 Github 주소를 링크해두었습니다. 

 

 

AccessToken과 RefreshToken의 발급

public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

...

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        /*  /login[POST] 요청 시 진입*/

...

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());
        Authentication authentication = authenticationManager.authenticate(authenticationToken);
        PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
        log.info("Authentication: " + principalDetails.getUsername());

        return authentication;
    }

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

        PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();

        /*JWT Token(ACCESS, REFRESH) 생성*/
        String accessToken = TokenUtil.generateToken(principalDetails.getMember().getUsername(), ACCESS);
        String refreshToken = TokenUtil.generateToken(principalDetails.getMember().getUsername(), REFRESH);

...

        redisService.setValues(principalDetails.getUsername(), refreshToken, JwtProperties.REFRESH_TOKEN_EXPIRATION_TIME, TimeUnit.MILLISECONDS);

    }

로그인 로직을 Spring Security에서 사용하는 필터에서 구현해보고자 UsernamePasswordAuthenticationFilter를 상속받았다.

[POST] /login 요청이 오면 JwtAuthenticationFilter의 attemptAuthentication()에서 해당 요청을 받는다. (Spring Security 설정 파일에서 http.formLogin().disable()를 해주어야 한다.)

attemptAuthentication()에서 인증이 완료되면 아래의 successfulAuthentication()에서 AccessToken과 RefreshToken을 생성하여 클라이언트로 전송한다. 서버는 redis에 RefreshToken을 저장하고 클라이언트는 클라이언트 저장공간에 AccessToken과 RefreshToken을 저장한다.

 

 

데이터 요청

public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
    /*인가 처리 관련 로직*/

...

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("JwtAuthorizationFilter 진입");
        /*header 값 체크  */
        String jwtHeader = request.getHeader(JwtProperties.ACCESS_HEADER_STRING);

        if (jwtHeader == null || !jwtHeader.startsWith("Bearer")) {
            chain.doFilter(request, response);
            return;
        }

        try {
            /*토큰 검증*/
            String jwtToken = request.getHeader(JwtProperties.ACCESS_HEADER_STRING).replace(JwtProperties.TOKEN_PREFIX, "");
            String username = TokenUtil.verifyToken(jwtToken);

            if (username != null) {
                Member member = memberRepository.findByUsername(username);
                PrincipalDetails principalDetails = new PrincipalDetails(member);
                Authentication authentication = new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities());

                /*SecurityContext에 직접 접근 후 세션 생성 -> 자동으로 UserDetailsService에 있는 loadByUsername 호출*/
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
            chain.doFilter(request, response);
        } catch (TokenExpiredException e) {
            ResponseDto responseDto = ResponseDto.builder()
            .status(HttpStatus.UNAUTHORIZED)
            .message("Token Has Expired")
            .build();

            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(responseDto));
        }
    }
}

BasicAuthenticationFilter를 상속받은 필터를 추가해, 데이터 요청이 올 때마다 해당 필터를 거치도록 하였다.

여기서 클라이언트가 데이터 요청 시 Header에 담은 AccessToken을 검증하게 된다.

해당 AccessToken이 정상적으로 검증이 되었으면 이후 로직을 처리하게 되고 그렇지 않으면 클라이언트 측에 에러 메시지를 전송한다. AccessToken이 만료가 된 경우 클라이언트에게 토큰이 만료됨을 알린다.

 

 

AccessToken  재발급 요청

public ResponseEntity<ResponseDto> reissue(MemberRequestDto.Reissue reissue) {
    String username = TokenUtil.verifyToken(reissue.getRefreshToken());
    if (username == null) {
        ResponseDto responseDto = ResponseDto.builder()
                .status(HttpStatus.BAD_REQUEST)
                .message("Invalid Refresh Token")
                .build();
        return new ResponseEntity<>(responseDto, HttpStatus.BAD_REQUEST);
    }

    username = TokenUtil.verifyToken(reissue.getAccessToken());

    String redisToken = redisService.getValues(username);

    if (!redisToken.isEmpty()) {
        String newAccessToken = TokenUtil.generateToken(username, TokenType.ACCESS);

        MemberResponseDto.TokenInfo tokenInfo = MemberResponseDto.TokenInfo.builder()
                .accessToken(newAccessToken)
                .build();

        ResponseDto responseDto = ResponseDto.builder()
                .data(tokenInfo)
                .status(HttpStatus.OK)
                .build();

        return new ResponseEntity<>(responseDto, HttpStatus.OK);
    }

    ResponseDto responseDto = ResponseDto.builder()
            .status(HttpStatus.BAD_REQUEST)
            .message("Invalid Refresh Token")
            .build();

    return new ResponseEntity<>(responseDto, HttpStatus.BAD_REQUEST);

}

AccessToken의 만료 요청을 받은 클라이언트는 저장해두었던 AccessToken과 RefreshToken을 Body에 담아 서버로 전송한다.

서버에서는 RefreshToken을 검증 후 AccessToken의 Payload에 저장되어 있는 username을 조회한다.

Redis에 해당 username을 key 값으로 저장되어 있는 RefreshToken을 조회한다.

RefreshToken이 만료되어 존재하지 않다면 에러 메시지를 전송해 재로그인을 할 수 있도록 한다.

RefreshToken이 존재한다면 AccessToken을 재생성해 클라이언트로 전송한다.

 

결론

이전의 포스팅을 통해 JWT의 개념 및 JWT의 사용 전략에 대해 알아보았고 이번에는 직접 구현을 해보았습니다. 

AccessToken과 RefreshToken을 사용하여 JWT를 구현하는 방식에는 차이가 있을 수 있지만 공통적인 진행과정(토큰의 발급 및 재발급)을 거치게 됩니다. 실제 구현한 코드는 아래의 Github 링크를 통해 확인하실 수 있습니다.

 


Github 전체 코드

https://github.com/cms02/Login

 

GitHub - cms02/Login: SpringSecurity, JWT, JPA, Redis 를 이용한 로그인 구현

SpringSecurity, JWT, JPA, Redis 를 이용한 로그인 구현. Contribute to cms02/Login development by creating an account on GitHub.

github.com

  • AccessToken과 RefreshToken의 발급
  • 데이터 요청
  • AccessToken  재발급 요청
  • 결론
달려LIKE추추
달려LIKE추추
풋내기 백엔드 개발자의 기록 창고

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.