package com.example.loans_domain.auth.jwt;
import com.example.loans_domain.auth.entity.CustomUserDetails;
import com.example.loans_domain.auth.entity.UserEntity;
import com.example.loans_domain.auth.entity.constant.Role;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@RequiredArgsConstructor // JWTUtil을 의존성 주입 받기 위해 사용
public class JWTFilter extends OncePerRequestFilter {
private final JWTUtil jwtUtil; // JWT 유틸리티 클래스 의존성 주입
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorization = request.getHeader("Authorization"); // 요청 헤더에서 Authorization 헤더 값을 가져옴
// Authorization 헤더가 없거나 Bearer로 시작하지 않으면 필터 체인을 계속 진행
if (authorization == null || !authorization.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
// Bearer 토큰에서 실제 토큰 값을 추출
String token = authorization.split(" ")[1];
// 토큰이 만료되었으면 필터 체인을 계속 진행
if (jwtUtil.isExpired(token)) {
filterChain.doFilter(request, response);
return;
}
// 토큰에서 사용자 이메일과 역할을 추출
String username = jwtUtil.getUsername(token);
String role = jwtUtil.getRole(token);
// 임시 UserEntity 객체를 생성 (비밀번호는 임시 값으로 설정)
UserEntity user = UserEntity.builder()
.userEmail(username)
.userPassword("tempPassword")
.userRole(Role.valueOf(role))
.build();
// UserEntity 객체로 CustomUserDetails 객체를 생성
CustomUserDetails customUserDetails = new CustomUserDetails(user);
// 사용자 인증 정보 생성
Authentication authentication = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
// SecurityContext에 사용자 인증 정보를 설정
SecurityContextHolder.getContext().setAuthentication(authentication);
// 필터 체인 계속 진행
filterChain.doFilter(request, response);
}
}
클라이언트 요청시
토큰 만료시
JWT 기반 인증의 작동 방식
JWT 기반 인증 시스템은 클라이언트가 각 요청마다 JWT 토큰을 서버에 전달하고, 서버는 그 토큰을 검증하여 사용자를 인증합니다. 이를 통해 서버는 stateless로 동작할 수 있습니다.
전체 흐름
- 로그인 요청: 사용자가 로그인하면 서버는 사용자의 자격 증명을 확인합니다.
- JWT 생성: 사용자가 성공적으로 인증되면 서버는 JWT 토큰을 생성하여 클라이언트에 반환합니다. 이 토큰에는 사용자의 정보(예: 이메일, 역할 등)가 포함됩니다.
- 클라이언트 요청: 클라이언트는 이후의 모든 요청에 이 JWT 토큰을 포함시켜 서버에 보냅니다.
- 토큰 검증: 서버는 각 요청에서 JWT 토큰을 검증하고, 유효한 토큰인 경우 사용자를 인증합니다.
Stateless의 의미
JWT 기반 인증 시스템은 stateless하므로 서버는 세션 상태를 유지하지 않습니다. 클라이언트는 매 요청마다 JWT 토큰을 서버에 전달하고, 서버는 그 토큰을 검증하여 사용자를 인증합니다.
로그인 과정과 매 요청마다의 인증 과정
로그인 시 사용자가 성공적으로 인증되면 서버는 JWT 토큰을 생성하여 클라이언트에 반환합니다. 이 토큰은 사용자의 식별 정보와 만료 시간 등을 포함합니다.
SecurityContextHolder와 매 요청의 관계
SecurityContextHolder는 현재 요청 스레드의 보안 컨텍스트를 보관하는 데 사용됩니다. 그러나 JWT 기반 인증에서는 서버가 상태를 유지하지 않으므로, 각 요청마다 SecurityContextHolder를 새로 설정해야 합니다.
각 요청에서 토큰을 검증하고 SecurityContextHolder를 설정하는 이유
각 요청마다 JWT 토큰을 검증하고 SecurityContextHolder를 설정하는 이유는 다음과 같습니다:
- Stateless: JWT 기반 인증 시스템은 상태를 유지하지 않기 때문에, 이전 요청에서 설정된 SecurityContextHolder의 상태를 유지하지 않습니다.
- 보안: 각 요청마다 JWT 토큰을 검증하여 토큰이 유효한지, 만료되지 않았는지 확인합니다. 이를 통해 보안을 강화합니다.
- 독립성: 각 요청이 독립적으로 처리되기 때문에, 각 요청마다 사용자 정보를 확인하고 인증을 설정해야 합니다.
1. 로그인 시
- 사용자가 로그인 요청을 보냅니다.
- 서버는 사용자의 자격 증명을 확인하고, 유효한 경우 JWT 토큰을 생성합니다. 이 토큰에는 사용자 이름, 역할, 만료 시간이 포함됩니다.
- 서버는 생성된 JWT 토큰을 클라이언트에 반환합니다.
- 클라이언트는 이 JWT 토큰을 저장하고, 이후의 요청에서 이 토큰을 사용합니다.
2. 각 요청 시
- 클라이언트는 요청을 보낼 때 JWT 토큰을 Authorization 헤더에 포함시킵니다.
- 서버는 요청을 받을 때마다 JWTFilter를 통해 JWT 토큰을 검증합니다.
- JWTFilter는 다음 작업을 수행합니다:
- Authorization 헤더에서 JWT 토큰을 추출합니다.
- 토큰의 유효성을 검증합니다 (서명 검증 및 만료 시간 확인).
- 토큰이 유효하면 사용자 이름과 역할 정보를 추출합니다.
- 추출한 정보를 기반으로 SecurityContextHolder에 인증 정보를 설정합니다.
- 서버는 SecurityContextHolder에 설정된 인증 정보를 사용하여 요청을 처리하고, 권한을 확인합니다.
3. 토큰 만료 시
- JWT 토큰은 만료 시간이 포함되어 있으며, 만료 시간이 지나면 더 이상 유효하지 않습니다.
- 클라이언트가 만료된 토큰으로 요청을 보낼 경우, 서버는 토큰이 만료되었음을 인지하고 요청을 거부합니다.
- 클라이언트는 다시 로그인하여 새로운 JWT 토큰을 발급받아야 합니다.