ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • refresh token redis로 구현하기
    Spring 2024. 2. 22. 03:48

    로그인할 때 access token을 생성하고, access token을 이용하여 redis에 key값으로 저장하여 refresh token을 구현했습니다.

    jwt token 발급 flow

    1. 클라이언트에서 로그인한다.
    2. 서버는 클라이언트에게 Access Token과 Refresh Token을 발급
    3. Access Token은 사용자 쿠키에 저장, Refresh Token은 key값을 email로 redis에 저장
    4. 요청마다 cookie에 있는 access token을 검증
    5. 이 때, Access Token이 만료가 되면 Redis에 index인 access Token로 검색하여 refresh token이 있는지 확인
    6. 서버는 Refresh Token 유효성 체크를 하게 되고, 새로운 Access Token을 발급하며 refresh Token도 갱신

    Access Token의 유효 기간은 30분, Refresh Token의 유효 기간은 일주일로 설정

    Refresh Token

    Redis repository에 저장하기 위한 entity

    TTL을 이용하여 refresh token이 갱신되지 않는다면 삭제 

     

    @Builder
    @Getter
    @AllArgsConstructor
    @NoArgsConstructor
    @RedisHash(value = "refreshToken", timeToLive = 604800)
    public class RefreshToken {
    
        @Id
        private String email;
        @Indexed
        private String accessToken;
        private String refreshToken;
    }

    redis repository

    Redis에 key를 email, index를 accestoken,  value를 refresh token로 저장

    import java.util.Optional;
    
    import org.springframework.data.repository.CrudRepository;
    
    public interface RefreshTokenRepository extends CrudRepository<RefreshToken, Long> {
        Optional<RefreshToken> findByAccessToken(String accessToken);
    }

    Spring Security Config

    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class SecurityConfig {
    
        private final JwtTokenProvider jwtTokenProvider;
    
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
           httpSecurity
              .httpBasic(HttpBasicConfigurer::disable)
              .csrf(CsrfConfigurer::disable)
              .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
              .authorizeHttpRequests(authorize ->
                 authorize
                    .requestMatchers("/api/v1/signin", "/api/v1/signup")
                    .permitAll()
                    .requestMatchers("/api/v1/admin/**")
                    .hasRole("ADMIN")
                    .anyRequest()
                    .authenticated())
              .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
           return httpSecurity.build();
    
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
           return PasswordEncoderFactories.createDelegatingPasswordEncoder();
        }
    
    }

    JwtAutheticationFilter

    토큰에 대한 검증을 진행

    @RequiredArgsConstructor
    public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
        private final JwtTokenProvider jwtTokenProvider;
        private static final String[] WHITELIST = {
            "/api/v1/signin", // 로그인
            "/api/v1/signup"  // 회원가입
        };
        private final AntPathMatcher antPathMatcher = new AntPathMatcher();
    
        @Override
        public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws
            IOException,
            ServletException {
    
            String path = request.getRequestURI();
            if (Arrays.stream(WHITELIST).anyMatch(pattern -> antPathMatcher.match(pattern, path))) {
                chain.doFilter(request, response);
                return;
            }
    
            String token = resolveToken(request);
            try {
                jwtTokenProvider.validateToken(token);
                Authentication authentication = jwtTokenProvider.getAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(authentication);
    
            } catch (ExpiredJwtException e) {    // 토큰 만료시 재발급 후 검증
                String newAccessToken = jwtTokenProvider.reissuanceAccessToken(token,
                    jwtTokenProvider.validateRefreshToken(token));
                jwtTokenProvider.createCookieAccessToken(newAccessToken, response);
                Authentication authentication = jwtTokenProvider.getAuthentication(newAccessToken);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
    
            chain.doFilter(request, response);
        }
    
        private String resolveToken(HttpServletRequest request) {
    
            Cookie[] cookies = request.getCookies();
            if (cookies == null) {
                return null;
            }
    
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals("Authorization")) {
                    try {
                        String token = URLDecoder.decode(cookie.getValue(), "UTF-8");
                        if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
                            return token.substring(7);
                        }
                        return URLDecoder.decode(cookie.getValue(), "UTF-8");
                    } catch (UnsupportedEncodingException e) {
                        return null;
                    }
                }
            }
            return null;
        }
    }
    

    JwtTokenProvider

    jwt token을 발급하고 관리하기 위한 클래스

    public class JwtTokenProvider {
    
        private final Key key;
        private final RefreshTokenRepository refreshTokenRepository;
    
        // 생성자, secretKey를 기반으로 JWT 서명에 사용할 Key를 생성
        public JwtTokenProvider(@Value("${jwt.secret.key}") String secretKey,
            RefreshTokenRepository refreshTokenRepository) {
            this.refreshTokenRepository = refreshTokenRepository;
            byte[] keyBytes = Decoders.BASE64.decode(secretKey);
            this.key = Keys.hmacShaKeyFor(keyBytes);
        }
    
        // 첫 로그인시 access token, refresh token 발급 후, access token이 포함된 jwtToken 반환
        public JwtToken generateToken(Authentication authentication) {
    
            String authorities = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));
    
            String accessToken = createAccessToken(authentication.getName(), authorities);
            String refreshToken = createRefreshToken(authentication.getName());
    
            refreshTokenRepository.save(RefreshToken.builder()
                .email(authentication.getName())
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .build());
    
            return JwtToken.builder()
                .grantType("Bearer")
                .accessToken(accessToken)
                .build();
        }
    
        // access token 생성
        public String createAccessToken(String email, String auth) {
            return Jwts.builder()
                .setSubject(email)
                .claim("auth", auth)
                .setExpiration(new Date((new Date()).getTime() + 43200000))
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
        }
    
        public String createRefreshToken(String email) {
            return Jwts.builder()
                .setSubject(email)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
        }
    
        // Access Token으로 사용자의 인증 정보를 가져온 후, 인증 정보를 기반으로 Spring Security의 Authentication 객체를 생성하여 반환
        public Authentication getAuthentication(String accessToken) {
    
            Claims claims = parseClaims(accessToken);
    
            if (claims.get("auth") == null) {
                throw new RuntimeException("권한 정보가 없는 토큰입니다.");
            }
    
            Collection<? extends GrantedAuthority> authorities = Arrays.stream(claims.get("auth").toString().split(","))
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    
            UserDetails principal = new User(claims.getSubject(), "", authorities);
            return new UsernamePasswordAuthenticationToken(principal, "", authorities);
        }
    
        // access token 검사
        public void validateToken(String token) {
            try {
                Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token);
            } catch (SecurityException | MalformedJwtException e) {
                log.info("Invalid JWT Token", e);
            } catch (UnsupportedJwtException e) {
                log.info("Unsupported JWT Token", e);
            } catch (IllegalArgumentException e) {
                log.info("JWT claims string is empty.", e);
            }
        }
    
        // refresh token 검사
        public String validateRefreshToken(String accessToken) {
            String refreshToken = refreshTokenRepository.findByAccessToken(accessToken)
                .orElseThrow(() -> new ExpiredJwtException(null, null, "데이터가 없음"))
                .getRefreshToken();
            try {
                Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(refreshToken);
                return refreshToken;
            } catch (SecurityException | MalformedJwtException e) {
                log.info("Invalid JWT Token", e);
            } catch (UnsupportedJwtException e) {
                log.info("Unsupported JWT Token", e);
            } catch (IllegalArgumentException e) {
                log.info("JWT claims string is empty.", e);
            }
            return null;
        }
    
        // refresh token 재발급
        public String reissuanceAccessToken(String accessToken, String refreshToken) {
            Claims claims = parseClaims(accessToken);
            String newAccessToken = createAccessToken((String)claims.get("sub"), (String)claims.get("auth"));
    
            refreshTokenRepository.save(RefreshToken.builder()
                .email((String)claims.get("sub"))
                .accessToken(newAccessToken)
                .refreshToken(refreshToken)
                .build());
    
            return newAccessToken;
        }
    
        // access token에서 claim 추출
        private Claims parseClaims(String accessToken) {
            try {
                return Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(accessToken)
                    .getBody();
            } catch (ExpiredJwtException e) {
                return e.getClaims();
            }
        }
    
        // access token이 저장되는 cookie 생성
        public void createCookieAccessToken(String accessToken, HttpServletResponse httpServletResponse) throws
            UnsupportedEncodingException {
            Cookie cookie = new Cookie("Authorization",
                URLEncoder.encode("Bearer " + accessToken, "utf-8").replaceAll("\\+", "%20"));
            cookie.setPath("/");
            cookie.setMaxAge(604800);  // 쿠키 유효 시간 : 일주일
            httpServletResponse.addCookie(cookie);
        }
    }

     

    UserService

    로그인 요청이 들어왔을 때 호출되는 method, 유저의 정보를 검증 후 JWT Token 발급

    @Transactional(readOnly = true)
    public JwtToken signIn(UserRequestDto userRequestDto) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
           userRequestDto.getEmail(),
           userRequestDto.getPassword());
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
        return jwtTokenProvider.generateToken(authentication);
    }
    

     

     

     

    잘못된 부분에 대해 댓글 남겨주시면 감사합니다.

     

Designed by Tistory.