[캡스톤디자인] Spring Boot, Spring Security, JWT, Redis를 사용한 사용자 인증 구현 (2)

Dev_Sanizzang·2023년 5월 3일
0

캡스톤디자인

목록 보기
3/15

👨‍💻 구현 코드(apigateway-service, user-service)
https://github.com/Sanizzang/come-capstone23-wcd/tree/main/003%20Code/backend

이번에는 이해한 Spring Security를 바탕으로 로그인을 구현한 과정을 기록해보겠다.

이번 Spring Security를 통한 로그인 구현은 아래 블로그를 참고하였다!

https://hou27.tistory.com/entry/Spring-Security-JWT

📌 Security Config

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig {

    private final MyUserDetailsService myUserDetailsService;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    private final Environment env;
    private final RedisTemplate<String, String> redisTemplate;
    private final JwtTokenProvider jwtTokenProvider;

    // 권한과 관련한 메서드
    // HttpSecurity: HTTP 요청에 대한 보안 구성 지정
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {


        // 사용자 인증 매니저, 인증 매니저는 사용자의 인증 정보를 확인
        AuthenticationManager authenticationManager = getAuthenticationManager(http);

        http
            .csrf().disable() // CSRF 공격 방지 기능 해제
            .headers().frameOptions().disable()
                .and()
                    .authorizeHttpRequests() // HTTP 요청에 대한 인가 설정
                    .requestMatchers("/**").permitAll() // 모든 요청에 대해서 인증 없이 허용
                .and()
                    .authenticationManager(authenticationManager) // AuthenticationManager 설정
                    .addFilter(getAuthenticationFilter(authenticationManager)); // 사용자 인증을 처리하는 필터 추가

        return http.build();
    }

    private AuthenticationManager getAuthenticationManager(HttpSecurity http) throws Exception {
        // 인증 매니저를 구성하는 빌더 클래스
        AuthenticationManagerBuilder authenticationManagerBuilder =
                http.getSharedObject(AuthenticationManagerBuilder.class);

        // userDetailService: 사용자 인증 정보를 검색할 때 사용하는 서비스 (userService)
        // passwordEncoder: 패스워드 인코딩을 위해 사용
        authenticationManagerBuilder.userDetailsService(myUserDetailsService).passwordEncoder(bCryptPasswordEncoder);
        return authenticationManagerBuilder.build();
    }

    private AuthenticationFilter getAuthenticationFilter(AuthenticationManager authenticationManager) {
        return new AuthenticationFilter(authenticationManager, myUserDetailsService, env, redisTemplate, jwtTokenProvider);
    }
}

위 코드는 Spring Security 구성을 담당하는 SecurityCofig 클래스이다.
@EnableWebSecurity 어노테이션을 사용하여 Spring Security를 사용하도록 설정하며, SecurityFilterChain을 반환하는 filterChain 빈을 생성한다. filterChain 빈은 HTTP 요청에 대한 보안 구성을 지정하고, 사용자 인증 매니저와 인증 필터를 설정한다.

getAuthenticationManager() 메서드는 사용자 인증 매니저를 생성하고 반환한다. 인증 매니저는 사용자 인증 정보를 확인하는 데 사용된다. AuthenticationManagerBuilder 클래스를 사용하여 사용자 인증 정보를 검색할 때 사용하는 MyUserDetailsService를 설정하고, 패스워드 인코딩을 위해 BCryptPasswordEncoder를 사용한다.

getAuthenticationFilter() 메서드는 사용자 인증을 처리하는 필터를 생성하고 반환한다. 이 필터는 사용자가 보낸 인증 정보를 확인하고, 검증된 사용자 정보를 기반으로 JWT 토큰을 생성하여 반환한다.

🔑 Spring Security를 통한 Jwt 적용

Jwt를 사용하기 위해 구현해야 할 것은 크게 기본적으로

  • Jwt 토큰 제공을 위한 JwtTokenProvider
  • HTTP Request에서 토큰을 읽어 들여 정상 토큰이면 Security Context에 저장하는 JwtTokenFilter 이렇게 2개이다.

그리고 Spring Security에 적용하기 위해 구현해야 할 것은 기본적으로

  • Jwt Filter를 Spring Security Filter Chain에 추가하기 위한 JwtSecurityConfig
  • 기본적으로 Spring Security 설정을 위한 SecurityConfig이다.

Authentication Token을 Authentication Manager가 넘겨받아
Authentication 객체를 생성하고, 이를 Provider에게 전달하여 Token을 생성하게 된다.

💡 여기서 나는 MSA를 통해 애플리케이션을 구성하기 때문에 user-service에만 Jwt Filter를 구성하게 되면 사용자 인증 기능을 구현한게 아무런 의미가 없어진다.
그래서 나는 user-service에 Jwt Filter를 구성하지 않고 apigateway-service에 Authorization Header를 검증하는 로직을 추가하였다.

📌 JwtTokenProvider

@Component
@RequiredArgsConstructor
public class JwtTokenProvider {

    private final Environment env;
    private final MyUserDetailsService myUserDetailsService;

    public String generateAccessToken(Authentication authentication) {
        // HMAC SHA-512 알고리즘으로 생성된 Secret Key 생성
        Key secretKey = Keys.hmacShaKeyFor(env.getProperty("access_token.secret").getBytes(StandardCharsets.UTF_8));

        String access_token = Jwts.builder()
                // JWT 토큰의 subject를 설정
                .setSubject(authentication.getName())
                // JWT 토큰의 만료 시간 설정(현재 시간 + token.expiration_time 값)
                .setExpiration(new Date(System.currentTimeMillis()
                        + Long.parseLong(env.getProperty("access_token.expiration_time"))))
                // JWT 토큰에 서명 추가
                .signWith(secretKey, SignatureAlgorithm.HS512)
                // JWT 토큰을 문자열로 변환
                .compact();

        return access_token;
    }

    public String generateRefreshToken(Authentication authentication) {
        Key secretKey = Keys.hmacShaKeyFor(env.getProperty("refresh_token.secret").getBytes(StandardCharsets.UTF_8));

        String refresh_token = Jwts.builder()
                // JWT 토큰의 subject를 설정
                .setSubject(authentication.getName())
                // JWT 토큰의 만료 시간 설정(현재 시간 + token.expiration_time 값)
                .setExpiration(new Date(System.currentTimeMillis()
                        + Long.parseLong(env.getProperty("refresh_token.expiration_time"))))
                // JWT 토큰에 서명 추가
                .signWith(secretKey, SignatureAlgorithm.HS512)
                // JWT 토큰을 문자열로 변환
                .compact();

        return refresh_token;
    }

    /**
     * Access 토큰으로부터 클레임을 만들고(복호화), 이를 통해 User 객체를 생성하여 Authentication 객체를 반환 (토큰에 있는 정보를 꺼냄)
     * @param access_token
     * @return
     */
    public Authentication getAuthenticationByAccessToken(String access_token) {
        Key secretKey = Keys.hmacShaKeyFor(env.getProperty("access_token.secret").getBytes(StandardCharsets.UTF_8));

        JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(secretKey);

        Claims claims = jwtParserBuilder
                .build()
                // 파싱 대상 JWT 토큰을 Jws(JWT Signature를 포함하는 객체) 객체로 파싱
                .parseClaimsJws(access_token)
                // 파싱된 JWT 내용을 가져옴
                .getBody();

        UserDetails userDetails = new User(claims.getSubject(), "", new ArrayList<>());

        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    /**
     * Refresh 토큰으로부터 클레임을 만들고, 이를 통해 User 객체를 생성하여 Authentication 객체를 반환
     * @param refresh_token
     * @return
     */
    public Authentication getAuthenticationByRefreshToken(String refresh_token) {
        Key secretKey = Keys.hmacShaKeyFor(env.getProperty("refresh_token.secret").getBytes(StandardCharsets.UTF_8));

        JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(secretKey);


        Claims claims = jwtParserBuilder
                .build()
                // 파싱 대상 JWT 토큰을 Jws(JWT Signature를 포함하는 객체) 객체로 파싱
                .parseClaimsJws(refresh_token)
                // 파싱된 JWT 내용을 가져옴
                .getBody();

        UserDetails userDetails = new User(claims.getSubject(), "", new ArrayList<>());

        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    /**
     * Access 토큰을 검증
     * @param token
     * @return
     */
    public boolean validateAccessToken(String access_token) {
        try {
            Key secretKey = Keys.hmacShaKeyFor(env.getProperty("access_token.secret").getBytes(StandardCharsets.UTF_8));

            // JWT 토큰을 파싱하기 위한 빌더 객체 생성 및 토큰에 사용될 서명 키 설정
            JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(secretKey);
            jwtParserBuilder
                    .build()
                    // 파싱 대상 JWT 토큰을 Jws(JWT Signature를 포함하는 객체) 객체로 파싱
                    .parseClaimsJws(access_token);

            return true;
        } catch (JwtException e) {
            // MalformedJwtException | ExpiredJwtException | IllegalArgumentException
            throw new CustomException("Error on Access Token", HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * Refresh 토큰을 검증
     * @param token
     * @return
     */
    public boolean validateRefreshToken(String refresh_token) {
        try {
            Key secretKey = Keys.hmacShaKeyFor(env.getProperty("refresh_token.secret").getBytes(StandardCharsets.UTF_8));

            // JWT 토큰을 파싱하기 위한 빌더 객체 생성 및 토큰에 사용될 서명 키 설정
            JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(secretKey);
            jwtParserBuilder
                    .build()
                    // 파싱 대상 JWT 토큰을 Jws(JWT Signature를 포함하는 객체) 객체로 파싱
                    .parseClaimsJws(refresh_token);
            return true;
        } catch (JwtException e) {
            // MalformedJwtException | ExpiredJwtException | IllegalArgumentException
            throw new CustomException("Error on Refresh Token", HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    public Long getAccessTokenExpiration(String access_token) {
        try {
            Key secretKey = Keys.hmacShaKeyFor(env.getProperty("access_token.secret").getBytes(StandardCharsets.UTF_8));

            // JWT 토큰을 파싱하기 위한 빌더 객체 생성 및 토큰에 사용될 서명 키 설정
            JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(secretKey);
            Long expirationTime = jwtParserBuilder
                    .build()
                    // 파싱 대상 JWT 토큰을 Jws(JWT Signature를 포함하는 객체) 객체로 파싱
                    .parseClaimsJws(access_token)
                    .getBody()
                    .getExpiration()
                    .getTime();
            Long currentTime = System.currentTimeMillis();
            Long remainingTime = expirationTime - currentTime;
            return remainingTime;
        } catch (JwtException e) {
            // MalformedJwtException | ExpiredJwtException | IllegalArgumentException
            throw new CustomException("Error on Refresh Token", HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

이 코드는 JWT 토큰을 생성하고 검증하는 JwtTokenProvicer 클래스이다.

  1. access_token과 refresh_token을 생성하는 generateAccessToken()와 generateRefreshToken() 메서드를 제공한다.

    💡 HMAC SHA-512 알고리즘을 사용하여 Secret Key를 생성하고, 해당 Secret Key를 사용하여 JWT 토큰을 생성한다.

  2. access_token, refresh_token으로부터 Authentication 객체를 생성하는 getAuthenticationByAccessToken(), getAuthenticationByRefreshToken() 메서드를 제공한다.

    💡 각각의 token을 파싱하여 클레임을 생성하고, 이를 통해 User 객체를 생성한다,
    User 객체와 권한 정보를 사용하여 Authentication 객체를 생성한다.

  3. access_token과 refresh_token을 검증하는 validateAccessToken()와 validateRefreshToken() 메서드를 제공한다.

    💡 JWT 토큰의 서명을 검증하여 토큰의 유효성을 검증한다.

  4. access_token의 만료 시간을 반환하는 getAccessTokenExpiration() 메서드를 제공한다.

📌 JwtTokenFilter -> apigateway-service AuthorizationHeaderFilter

위에서 말했다 싶이 나는 MSA를 통한 애플리케이션을 구현하고 있다. 모든 마이크로서비스들이 JWT 토큰에 대한 검증 로직이 필요하다.
그래서 user-serivce에 JwtTokenFilter를 구현하는 것이 아닌 apigateway-service에 JwtToken 검증 필터를 추가하였다.

@Component
@Slf4j
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {

    Environment env;
    RedisTemplate<String, String> redisTemplate;

    public AuthorizationHeaderFilter(Environment env, RedisTemplate<String, String> redisTemplate) {
        super(Config.class);
        this.env = env;
        this.redisTemplate = redisTemplate;
    }

    public static class Config { }

    @Override
    public GatewayFilter apply(Config config) {
        return (((exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();

            // HTTP 요청의 Authorization 헤더가 있는지 확인
            if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
                return onError(exchange, "No authorization header", HttpStatus.UNAUTHORIZED);
            }
            // HTTP 요청 헤더에서 Authorization 필드에 해당하는 값을 가져옴
            String authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
            // authorizationHeader 문자열에 Bearer 문자열을 제거
            String jwt = authorizationHeader.replace("Bearer", "");

            // JWT 토큰의 서명을 검증하고, 만료 시간을 확인하여 JWT 토큰이 유효한지 검증
            if (jwt != null && isJwtValid(jwt)) {
                // Redis에 해당 accessToken logout 여부를 확인
                String isLogout = (String) redisTemplate.opsForValue().get(jwt);

                // 로그아웃이 없는(되어 있지 않은) 경우 해당 토큰은 정상적으로 작동하기
                if (!ObjectUtils.isEmpty(isLogout)) {
                    return onError(exchange, "Please Login", HttpStatus.UNAUTHORIZED);
                }

                Authentication authentication = getAuthenticationByAccessToken(jwt);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
            else {
                return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED);
            }

            return chain.filter(exchange);

        }));
    }

    // jwt 문자열을 파싱하여 JWT 토큰이 유효한지 검증
    private boolean isJwtValid(String jwt) {
        boolean returnValue = true;
        String subject = null;

        try {
            /*
                JWT 토큰 서명에 사용될 비밀 키 생성. 이때, 비밀 키는 애플리케이션에서 미리 설정한 token.secret 값으로부터 생성되며,
                hmacShaKeyFor 메서드는 HMAC-SHA 알고리즘을 사용하여 비밀 키를 생성
            */
            Key secretKey = Keys.hmacShaKeyFor(env.getProperty("access_token.secret").getBytes(StandardCharsets.UTF_8));

            // JWT 토큰을 파싱하기 위한 빌더 객체 생성 및 토큰에 사용될 서명 키 설정
            JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(secretKey);
            subject = jwtParserBuilder
                    .build()
                    // 파싱 대상 JWT 토큰을 Jws(JWT Signature를 포함하는 객체) 객체로 파싱
                    .parseClaimsJws(jwt)
                    // 파싱된 JWT 내용을 가져옴
                    .getBody()
                    // JWT의 subject 값을 가져옴
                    .getSubject();
        } catch (Exception ex) {
            log.error("jwtParser = {}", ex.getMessage());
            returnValue = false;
        }

        return returnValue;
    }

    public Authentication getAuthenticationByAccessToken(String access_token) {
        Key secretKey = Keys.hmacShaKeyFor(env.getProperty("access_token.secret").getBytes(StandardCharsets.UTF_8));

        JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(secretKey);

        Claims claims = jwtParserBuilder
                .build()
                // 파싱 대상 JWT 토큰을 Jws(JWT Signature를 포함하는 객체) 객체로 파싱
                .parseClaimsJws(access_token)
                // 파싱된 JWT 내용을 가져옴
                .getBody();

        UserDetails userDetails = new User(claims.getSubject(), "", new ArrayList<>());

        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    /*
        - ServerWebExchange: Spring WebFlux에서 사용되는 HTTP 요청 및 응답 객체
        - Mono -> WebFlux라고 해서 Spring 5 부터 추가된 기능
        - 기존에 동기화 방식의 서버가 아니라 비동기 방식의 서버를 지원할 때 단일값을 전달할 때 사용
    */
    private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(httpStatus);

        log.error(err);

        return response.setComplete();
    }
}

이 필터는 API Gateway에서 들어오는 HTTP 요청의 Authorization 헤더에 JWT(access token)가 있는지 확인하고, 검증을 수행한다.

  1. HTTP 요청의 Authorization 헤더가 있는지 확인한다.
  2. Authorization 헤더에서 Bearer 문자열을 제거하고, JWT(access token) 문자열을 가져온다.
  3. 가져온 JWT(access token) 문자열의 서명을 검증하고, 만료 시간을 확인하여 JWT 토큰이 유효한지 검증한다.
  4. Redis에 해당 accessToken logout 여부를 확인한다.
  5. JWT 토큰이 유효하고 Redis에 로그아웃이 없는(되어 있지 않은) 경우, 해당 토큰으로 인증(Authentication) 객체를 생성하고, SecurityContextHolder에 저장한다.
  6. 인증(Authentication) 객체를 생성한 후, 이후 필터 체인을 실행한다.

application.yml에 필터 추가하기

API Gateway에 필터를 구현하고 해당 필터를 적용시키기 위해 해당 마이크로서비스에 filter를 추가해줘야 한다!

🔒 로그인 구현

나는 따로 로그인 API를 구현하지 않고 SpringSecurity에서 제공하는 /login API를 통해 로그인을 구현해봤다.

AuthenticationFilter

@Slf4j
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private MyUserDetailsService myUserDetailsService;
    private Environment env;

    private RedisTemplate<String, String> redisTemplate;
    private JwtTokenProvider jwtTokenProvider;

    public AuthenticationFilter(AuthenticationManager authenticationManager) {
        super.setAuthenticationManager(authenticationManager);
    }

    public AuthenticationFilter(AuthenticationManager authenticationManager, MyUserDetailsService myUserDetailsService, Environment env, RedisTemplate<String, String> redisTemplate, JwtTokenProvider jwtTokenProvider) {
        super.setAuthenticationManager(authenticationManager);
        this.myUserDetailsService = myUserDetailsService;
        this.env = env;
        this.redisTemplate = redisTemplate;
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        try{
            /*
                클라이언트에서 전송한 로그인 정보를 읽어옴
                - ObjectMapper: JSON 데이터를 자바 객체로 변환하는 Jackson 라이브러리 클래스
                - request.getInputStream(): HTTP 요청 바디(body)에 포함된 데이터를 읽어오기 위한 메서드
             */
            RequestLogin requestLogin = new ObjectMapper().readValue(request.getInputStream(), RequestLogin.class);

            // getAuthenticationManager: 인증 처리 메서드
            return getAuthenticationManager().authenticate(
                    /*
                        사용자가 입력했던 login_id와 password 값을 spring security에서 사용할 수 있는 형태의 값으로 변환하기 위해서
                        UsernamePasswordAuthenticationToken 형태로 변환
                     */
                    new UsernamePasswordAuthenticationToken(
                            requestLogin.getLoginId(),
                            requestLogin.getPassword(),
                            // 권한과 관련된 값
                            new ArrayList<>()
                    )
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        String refresh_token = jwtTokenProvider.generateRefreshToken(authResult);

        TokenDto tokenDto = new TokenDto(
                jwtTokenProvider.generateAccessToken(authResult),
                refresh_token
        );

        redisTemplate.opsForValue().set(
                // Redis 데이터베이스에 저장할 키(key)
                authResult.getName(),
                // value
                refresh_token,
                // Redis에 저장될 기간
                Long.parseLong(env.getProperty("refresh_token.expiration_time")),
                // 기간의 단위를 마이크로 초로 지정
                TimeUnit.MICROSECONDS
        );

        // 응답헤더에 token과 userId 추가
        response.addHeader("access_token", tokenDto.getAccess_token());
        response.addHeader("refresh_token", tokenDto.getRefresh_token());
        // login_id를 반환시켜주는 이유는 우리가 가지고 있는 token과 login_id가 동일한지 확인하기 위함
        response.addHeader("login_id", authResult.getName());
    }
}

이 클래스는 클라이언트가 보낸 로그인 정보를 읽어와 인증을 처리한다.

attemptAuthentication() 메서드에서는 클라이언트에서 전송한 로그인 정보를 ObjectMapper를 사용하여 RequestLogin 객체로 변환한다. 그리고 getAuthenticationManager()를 사용하여 인증 처리 메서드를 호출한다. 이때, UsernamePasswordAuthenticationToken을 생성하여 사용자가 입력한 login_id와 password 값을 전달한다.

successfulAuthentication() 메서드에서는 인증이 성공하면 AccessToken과 RefreshToken을 생성하고 Redis 데이터베이스에 RefreshToken과 login_id를 저장한다. 마지막으로, 응답 헤더에 AccessToken, RefreshToken, login_id를 추가하여 클라이언트에게 전송한다.

즉, AuthenticationFilter 클래스는 로그인 인증을 수행하고, AccessToken과 RefreshToken을 생성하여 Redis 데이터베이스에 저장하는 역할을 담당한다.

🔓 로그아웃 구현

이번에는 로그아웃을 어떻게 구현했는지 적어보겠다.
로그아웃 구현은 아래 블로그를 참고하였다.

https://velog.io/@joonghyun/SpringBoot-Jwt%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83

나는 로그아웃을 구현하기 위해 블랙리스트를 생성하는 것을 채택했다.

UserServiceImpl

    @Override
    public void logout(TokenDto tokenDto) {
        // 로그아웃 하고 싶은 토큰이 유효한 지 검증
        if(!jwtTokenProvider.validateAccessToken(tokenDto.getAccess_token())) {
            throw new IllegalArgumentException("로그아웃 : 유효하지 않은 토큰입니다.");
        }

        // Access Token에서 userId를 가져옴
        Authentication authentication = jwtTokenProvider.getAuthenticationByAccessToken(tokenDto.getAccess_token());

        // Redis에서 해당 userId로 저장된 Refresh Token이 있는지 여부 확인
        if ( redisTemplate.opsForValue().get(authentication.getName()) != null) {
            // Refresh Token 삭제
            redisTemplate.delete(authentication.getName());
        }

        // 해당 Access Token 유효시간을 가져와 BlackList에 저장장
        Long expiration = jwtTokenProvider.getAccessTokenExpiration(tokenDto.getAccess_token());
        redisTemplate.opsForValue().set(tokenDto.getAccess_token(), "logout", expiration);
    }

TokenDto 객체에 저장된 AccessToken을 검증하고, Access Token을 파싱하여 해당 토큰에 연결된 사용자 ID를 가져온다. 그리고 해당 사용자의 Refresh Token이 있는지 여부를 확인하고, Refresh Token이 존재하는 경우에는 해당 Refresh Token을 삭제한다.

그리고 Access Token의 유효시간을 가져와서 해당 기간 동안에는 로그아웃한 사용자가 다시 로그인하여 Access Token을 사용하지 못하도록 Redis의 Blacklist에 해당 Access Token을 추가한다. 이렇게 함으로써, 로그아웃한 사용자의 Access Token이 만료될 때까지 더 이상 유효하지 않도록 처리한다.

profile
기록을 통해 성장합니다.

0개의 댓글