Notice
Recent Posts
Recent Comments
Link
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
Archives
Today
Total
관리 메뉴

밤빵's 개발일지

[TIL]20241209 JWT 기반 로그아웃 처리와 블랙리스트 방식 본문

개발Article

[TIL]20241209 JWT 기반 로그아웃 처리와 블랙리스트 방식

최밤빵 2024. 12. 9. 04:32

현재 프로젝트에서 JWT 기반 인증을 구현하면서 로그아웃 처리 방식에 대한 고민이 있었다. JWT는 기본적으로 stateless한 인증 방식이다. 서버가 클라이언트의 상태를 기억하지 않는 것이 원칙이지만, 클라이언트에서만 로그아웃을 처리하는 방식이 사용자 경험 측면에서 불완전할 수 있다는 의견이 있었다. 그래서 백엔드 서버에도 로그아웃 기능을 구현하기로 결정했다.

 

1. JWT 로그아웃 처리

JWT는 클라이언트가 자체적으로 보관하는 Self-contained Token으로, 로그아웃을 처리할 때 몇 가지 고민이 있었다.

  1. 토큰 삭제 불가능: JWT는 발급 후 서버에 저장되지 않기 때문에, 특정 토큰을 삭제하거나 무효화 할 수 없다.
  2. 발급된 토큰의 유효기간 문제: 로그아웃 시에도 기존 토큰은 만료 시간이 남아 있는 한 계속 유효하다.
  3. stateless 원칙과의 충돌: 서버가 상태를 저장하지 않으면서도 로그아웃 기능을 구현하려면 추가적인 고민이 필요하다.

2. 로그아웃 처리 방식의 선택

문제를 해결하기 위해 두 가지 방식을 고민했다.

1) 클라이언트 중심의 로그아웃

클라이언트가 JWT를 삭제하는 방식으로, 클라이언트에서 브라우저의 로컬 스토리지나 세션 스토리지에 저장된 JWT를 삭제하면 된다. 이 방식의 장점은 stateless 원칙을 완전히 지킬 수 있다는 점이지만 문제는 클라이언트가 JWT를 삭제하지 않거나, 악의적으로 유지하면 여전히 유효한 토큰으로 서버에 요청할 수 있다는 점이다.

 

2) 서버 중심의 로그아웃 (블랙리스트 방식)

로그아웃된 토큰을 서버에서 관리하는 방식.

블랙리스트: 로그아웃된 JWT를 별도의 저장소에 저장하고, 각 요청에서 블랙리스트를 조회하여 유효성을 검사한다. 블랙리스트 방식은 서버가 클라이언트의 상태를 일부 기억하게 되므로 stateless 원칙을 완전히 지키지는 못한다. 하지만 보안 강화 측면에서 실질적인 이점이 있다.

3. 블랙리스트 방식으로 구현

최종적으로는 블랙리스트 방식을 선택했다. 

- 안정성 강화: 클라이언트가 로그아웃한 토큰을 재사용하지 못하도록 확실히 차단.
- TTL 설정: 블랙리스트 항목은 토큰의 유효기간이 지나면 자동으로 삭제되도록 TTL을 설정하여 관리 비용을 줄인다.
- 관리 용이성: 서버 측에서 토큰 상태를 명확히 관리할 수 있어 보안적인 측면에서 유리하다.

1) 로그아웃 API 구현

@Service
public class AuthService {

    private final JwtUtil jwtUtil;
    private final Map<String, Long> blacklist = new ConcurrentHashMap<>();

    public AuthService(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    public void logout(String token) {
        String cleanToken = token.replace("Bearer ", "");
        if (!jwtUtil.validateToken(cleanToken)) {
            throw new IllegalArgumentException("유효하지 않은 토큰입니다.");
        }

        long expiration = jwtUtil.getExpiration(cleanToken);
        blacklist.put(cleanToken, expiration);
    }

    public boolean isBlacklisted(String token) {
        return blacklist.containsKey(token);
    }
}

2) JWT 유효성 검사에 블랙리스트 반영

@Component
public class JwtFilter extends OncePerRequestFilter {

    private final AuthService authService;
    private final JwtUtil jwtUtil;

    public JwtFilter(AuthService authService, JwtUtil jwtUtil) {
        this.authService = authService;
        this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String authorizationHeader = request.getHeader("Authorization");

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            String token = authorizationHeader.replace("Bearer ", "");
            if (authService.isBlacklisted(token)) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return;
            }

            if (jwtUtil.validateToken(token)) {
                String username = jwtUtil.getUsername(token);
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(username, null, jwtUtil.getRoles(token));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }

        filterChain.doFilter(request, response);
    }
}

 


4. 고민과 해결

고민했던 부분

- 메모리 사용량: 블랙리스트에 저장된 토큰이 많아질 경우 메모리 소모가 우려되었지만, 블랙리스트 항목에 TTL을 설정하고, 유효기간이 지난 토큰은 삭제하도록 관리했다.
- stateless 원칙과의 충돌: 블랙리스트 방식은 완전히 stateless하지 않지만 보안 요구 사항을 우선적으로 고려하여 수용하기로 했다.
- 클라이언트와의 협업: 클라이언트가 만료된 토큰으로 API 요청을 반복할 경우 서버 부하가 발생할 수 있다. 클라이언트와의 협업을 통해 토큰 만료 시 빠르게 갱신하도록 해야한다. 

 

배운 점

- JWT의 stateless 특성과 보안 요구 사항 간의 균형을 맞추는 방법을 고민할 수 있었다.
- 실무에서는 이론적인 원칙만 고집하기보다는, 현실적인 요구 사항을 반영한 설계가 필요함을 깨달았다.
- 간단한 메모리 기반 블랙리스트 구현으로도 효율적인 보안 강화를 이룰 수 있었다.

블랙리스트 방식의 로그아웃 구현은 stateless 원칙과 현실적 요구 사항 간의 균형을 찾는 과정이었다. 단순히 클라이언트에서 토큰을 삭제하는 방식만으로는 보안적인 허점이 존재하지만, 블랙리스트 방식은 이러한 허점을 보완하면서도 효율적인 로그아웃 처리를 가능하게 한다. 이번 고민과 구현 과정을 통해 보안 강화와 설계 원칙 사이에서 적절한 타협점을 찾는 법을 배웠다. 이 경험은 앞으로 더 복잡한 시스템 설계에서도 균형 있는 결정을 내릴 수 있는 기반이 되지 않을까..?