[Spring] Access Token 블랙리스트 관리하기

2025. 2. 25. 15:33·Backend

🎉 개요

로그인 기능을 구현하고 테스트 하던 중, Access Token을 새로 발급 받았는데도 발급 전의 Access Token으로 여전히 접근이 가능하다는 것을 알게 되었다. 이는 이전 토큰의 만료 기간이 남았기 때문에 해당 토큰이 유효하다고 인식하여 발생한 일이었다.

 

만약 이전 토큰이 탈취된 경우라면, 새 토큰이 발급되었음에도 탈취한 토큰으로 계속 접근이 가능하므로 보안상 문제가 생길 수 있다. 따라서 이전의 토큰을 무효화하여 접근할 수 없도록 해야 한다.

 

이 문제를 해결하기 위해 이전 토큰들을 블랙리스트에 넣어서 관리하는 방법을 선택했다. 스프링 시큐리티 관련 코드는 이전에 정리했으니, 블랙 리스트에 넣는 과정만 기록하려고 한다.

 

[Spring] Spring Security JWT 로그인 구현하기(with Redis)

📝 개요앞에서 JWT에 대해 대략적으로 알아봤으니, 이제는 JWT를 사용한 Spring Security 로그인을 구현하려고 한다. 이때, Refresh Token을 저장하기 위해 Redis를 사용하려고 한다. [Spring] JWT(JSON Web Token)

guswls28.tistory.com

 

✒️ 토큰 블랙리스트 서비스

우선 토큰을 블랙리스트에 등록하는 서비스를 생성한다. 이때, Refresh Token을 Redis에 저장하는 것과 마찬가지로 블랙리스트에 등록될 토큰도 Redis에 저장한다. (∵ TTL & In-Memory)

 

블랙리스트 키는 blacklist:AccessToken 형식으로 저장한다. 

private final JwtTokenProvider jwtTokenProvider;
private final RedisDao redisDao;
private static final String BLACKLIST_PREFIX = "blacklist:";

 

addBlackList

이전의 토큰도 만료 시간이 지나면 유효한 토큰이 아니다. 따라서 오랫동안 저장할 필요 없이, 남은 만료 시간을 구한 뒤 그 시간만큼만 블랙리스트로 등록하면 된다. 

 

또한, 블랙리스트에 해당 key가 존재하는지 여부만 확인할 것이므로 value는 중요하지 않다. 따라서 value에는 빈 문자열을 저장하면 된다.

public void addBlacklist(String token) {
    String key = BLACKLIST_PREFIX + token;
    // key 조회용 => data 중요하지 않음
    Claims claims = jwtTokenProvider.parseClaims(token);
    Date expiration = claims.getExpiration();
    long now = new Date().getTime();
    long remainingExpiration = expiration.getTime() - now;
    if (remainingExpiration > 0) { // 남은 만료시간만큼 블랙리스트로 등록
        redisDao.setValues(key, "", Duration.ofMillis(remainingExpiration));
    }
}

 

isBlacklisted

토큰이 블랙리스트에 존재하는지 확인하기 위한 메서드이다. 블랙리스트에서 key로 찾은 결과가 null이면 해당 토큰은 블랙리스트에 존재하지 않는 것이다. (true: 블랙리스트에 존재, false: 블랙리스트에 존재 X)

public boolean isBlacklisted(String token) {
    String key = BLACKLIST_PREFIX + token;
    return redisDao.getValues(key) != null;
}

 

전체 코드

더보기
// 토큰 갱신시 이전 토큰 폐기
@Service
@RequiredArgsConstructor
public class TokenBlacklistService {
    private final JwtTokenProvider jwtTokenProvider;
    private final RedisDao redisDao;
    private static final String BLACKLIST_PREFIX = "blacklist:";

    public void addBlacklist(String token) {
        String key = BLACKLIST_PREFIX + token;
        // key 조회용 => data 중요하지 않음
        Claims claims = jwtTokenProvider.parseClaims(token);
        Date expiration = claims.getExpiration();
        long now = new Date().getTime();
        long remainingExpiration = expiration.getTime() - now;
        if (remainingExpiration > 0) { // 남은 만료시간만큼 블랙리스트로 등록
            redisDao.setValues(key, "", Duration.ofMillis(remainingExpiration));
        }
    }

    public boolean isBlacklisted(String token) {
        String key = BLACKLIST_PREFIX + token;
        return redisDao.getValues(key) != null;
    }
}

 

💾 블랙리스트 저장

로그아웃 시, 해당 토큰을 블랙리스트에 추가하여 더 이상 사용할 수 없도록 처리해야 한다.

// 로그아웃 
@Override
public void logout(String accessToken, String email) {
    // 저장된 refresh Token 삭제
    jwtTokenProvider.deleteRefreshToken(email);
    
    // access token 블랙리스트에 저장
    tokenBlacklistService.addBlacklist(accessToken);
}

 

Refresh Token으로 Access Token을 재발급 받을 때도 해당 토큰을 블랙리스트에 저장하도록 한다. 

@Override
public JwtToken checkToken(String accessToken, String refreshToken) {
    // Refresh Token이 유효하지 않은 경우
    if (refreshToken == null || !jwtTokenProvider.validateRefreshToken(refreshToken))
        throw new InvalidTokenException("유효하지 않은 Refresh Token입니다");

    // Access Token이 유효하지 않은 경우 (null이거나 블랙리스트에 존재함)
    if (accessToken == null || tokenBlacklistService.isBlacklisted(accessToken))
        throw new InvalidTokenException("유효하지 않은 Access Token입니다");

    // 블랙리스트에 이전 accessToken 넣기
    tokenBlacklistService.addBlacklist(accessToken);

    // Access Token & Refresh Token 재발급
    String userNameFromToken = jwtTokenProvider.getEmailFromToken(refreshToken);
    return jwtTokenProvider.generateTokenWithRefreshToken(userNameFromToken);
}

 

✅ 토큰 유효성 검사

이제 사용자의 JWT를 검사하는 필터에서 해당 토큰이 블랙리스트에 존재하는지만 확인하면 된다. 만약 토큰이 블랙리스트에 존재한다면 다음 필터로 이동하지 못해 서비스에 접근할 수 없다.

RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilter {
    private final JwtTokenProvider jwtTokenProvider;
    private final TokenBlacklistService tokenBlacklistService;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // Request Header에서 Access Token 추출
        String accessToken = resolveToken((HttpServletRequest) servletRequest);
        if (accessToken != null) {
            // Access Token이 유효하고 Access Token이 블랙리스트에 존재하는지 확인
            if (jwtTokenProvider.validateToken(accessToken) && !tokenBlacklistService.isBlacklisted(accessToken)) {
                // 토큰이 유효하면 Authentication 객체를 가지고 와서 SecurityContext에 저장
                Authentication authentication = jwtTokenProvider.getAuthentication(accessToken);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            } else {
                // 토큰이 유효하지 않으면 401 (더이상 필터 처리 X)
                HttpServletResponse response = (HttpServletResponse) servletResponse;
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return;
            }
        }
        // 다음 필터로 요청 전달
        filterChain.doFilter(servletRequest, servletResponse);
    }
    
    ...
}

 

🎯마치며

로그아웃을 하면 아래와 같이 블랙리스트 키에 등록된 것을 확인할 수 있다. 생각보다 간단한 코드였지만 블랙리스트로 관리하는 방법을 배우게 되어 유익했다!


참고

https://velog.io/@boo105/Redis-%EB%A5%BC-%ED%86%B5%ED%95%9C-JWT-Blacklist-%EA%B5%AC%ED%98%84

 

'Backend' 카테고리의 다른 글

[Web] 웹 소켓(Web Socket)이란?  (1) 2025.01.29
[Spring] Spring Security JWT 로그인 구현하기(with Redis)  (1) 2025.01.24
[Spring] JWT(JSON Web Token)란?  (0) 2025.01.11
[Spring] 객체 지향 설계와 스프링  (0) 2024.10.22
[Error] 셧다운 포트가 설정되지 않았습니다.  (0) 2024.04.16
'Backend' 카테고리의 다른 글
  • [Web] 웹 소켓(Web Socket)이란?
  • [Spring] Spring Security JWT 로그인 구현하기(with Redis)
  • [Spring] JWT(JSON Web Token)란?
  • [Spring] 객체 지향 설계와 스프링
hjin28
hjin28
  • hjin28
    끄적이는 기록장
    hjin28
  • 전체
    오늘
    어제
  • 블로그 메뉴

    • 홈
    • 태그
    • GitHub
    • 이전 블로그
    • 분류 전체보기
      • TIL
      • Frontend
      • Backend
      • Infra
      • Java
        • 이것이 자바다
      • CS
        • 컴퓨터구조
        • 운영체제
        • 네트워크
        • 데이터베이스
      • Algorithm
        • 자료구조
        • 시뮬레이션
        • 완전탐색
        • BFS & DFS
        • DP
        • 그리디
        • 최단경로
        • 유니온파인드
        • 위상정렬
        • 정렬
        • SQL
      • ETC
  • 최근 글

  • 태그

    Spring
    SQL
    위상정렬
    til
    투포인터
    springsecurity
    Programmers
    CS
    덱
    백준
    자바
    구현
    websocket
    SWEA
    BFS
    DP
    유니온파인드
    백트래킹
    vue
    다이나믹프로그래밍
    완전탐색
    JWT
    DFS
    비트마스킹
    최단경로
    그리디
    ec2
    컴퓨터구조
    자료구조
    java
  • hELLO· Designed By정상우.v4.10.1
hjin28
[Spring] Access Token 블랙리스트 관리하기
상단으로

티스토리툴바