이 프로젝트에서는 JWT
(JSON Web Token)를 사용한 인증 및 인가 시스템을 구축하였습니다. 이를 통해 사용자는 로그인 시 발급된 Access Token
과 Refresh Token
을 사용하여 보호된 리소스에 접근할 수 있으며, 로그아웃 시 토큰이 블랙리스트에 등록되어 이후 재사용을 방지할 수 있습니다.
LocalJwtAuthenticationFilter
)Gateway 레벨에서 JWT를 검증하는 GlobalFilter
를 구현하여, API 요청 시 JWT 토큰을 검증(auth 서비스에 요청)하는 과정을 처리합니다. 이를 통해 /api/auth
, 회원가입(POST /api/member) 경로를 제외한 모든 요청에 대해 JWT가 검증됩니다.
@Component
public class LocalJwtAuthenticationFilter implements GlobalFilter {
private final AuthService authService;
public LocalJwtAuthenticationFilter(@Lazy AuthService authService) {
this.authService = authService;
}
@Override
public Mono<Void> filter(final ServerWebExchange exchange, final GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
String method = exchange.getRequest().getMethod().name();
// /api/auth 경로의 모든 요청은 검증하지 않습니다.
if (path.startsWith("/api/auth")) {
return chain.filter(exchange);
}
// /api/member 경로의 POST 요청은 검증하지 않습니다.
if (path.startsWith("/api/member") && method.equalsIgnoreCase("POST")) {
return chain.filter(exchange);
}
// JWT 토큰 추출 및 검증
String token = extractToken(exchange);
if (token == null) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
Map<String, Object> claimsMap = validateToken(token);
if (claimsMap == null) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 헤더에 유저 정보를 추가한 후 체인을 통해 다음 필터로 이동
exchange.getRequest().mutate()
.header("user_id", claimsMap.get("user_id").toString())
.header("username", claimsMap.get("username").toString())
.header("role", claimsMap.get("role").toString())
.header("email", claimsMap.get("email").toString())
.build();
return chain.filter(exchange);
}
private String extractToken(ServerWebExchange exchange) {
String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}
return null;
}
private Map<String, Object> validateToken(String token) {
try {
return authService.verifyJwt(token);
} catch (Exception e) {
return null;
}
}
}
@FeignClient(name = "auth")
public interface AuthClient extends AuthService {
@GetMapping("/api/auth/verify") // Jwt 검증 API
Map<String, Object> verifyJwt(@RequestParam(value = "token") String token);
}
이 필터는 Gateway에서 JWT를 검증하는 역할을 하며, AuthService
를 통해 JWT를 검증하여 사용자의 정보를 확인하고 요청 헤더에 추가합니다. 이를 통해 모든 요청에 대해 필터링 및 검증이 이루어집니다.
AuthService
와 TokenService
)로그아웃 시 사용된 Access Token
과 Refresh Token
을 Redis에 저장하여 블랙리스트를 관리합니다. 블랙리스트에 등록된 토큰은 이후 재사용이 불가능하게 처리됩니다.
@Service
public class AuthService {
private final RedisTemplate<String, Object> redisTemplate;
private final TokenService tokenService;
public AuthService(RedisTemplate<String, Object> redisTemplate, TokenService tokenService) {
this.redisTemplate = redisTemplate;
this.tokenService = tokenService;
}
// 로그아웃 처리 및 블랙리스트에 토큰 추가
public void logout(String accessToken, String refreshToken) {
addTokenToBlacklist(accessToken);
addTokenToBlacklist(refreshToken);
}
private void addTokenToBlacklist(String token) {
try {
long expiration = tokenService.getExpirationFromToken(token);
if (expiration > 0) {
redisTemplate.opsForValue().set(token, "true", expiration, TimeUnit.MILLISECONDS);
}
} catch (ExpiredJwtException e) {
// 토큰이 이미 만료된 경우 무시
}
}
// 토큰이 블랙리스트에 등록되었는지 확인
private boolean isTokenInBlacklist(String token) {
return Boolean.TRUE.equals(redisTemplate.hasKey(token));
}
}
TokenService
를 통한 토큰 생성 및 검증JWT 토큰의 생성과 검증은 TokenService
에서 처리됩니다. 이 서비스는 JWT의 만료 시간 및 유효성을 확인하고, Redis에 저장된 블랙리스트와 비교하여 유효한 토큰인지 검증합니다.
@Service
public class TokenService {
private final RedisTemplate<String, Object> redisTemplate;
// Access Token 생성
public String createAccessToken(UserDetailsDto user) {
return generateToken(user, accessExpiration);
}
// Refresh Token 생성
public String createRefreshToken(UserDetailsDto user) {
return generateToken(user, refreshExpiration);
}
// Access Token 재발행
public String refreshAccessToken(String refreshToken) {
if (isTokenInBlacklist(refreshToken)) {
throw new IllegalArgumentException("유효하지 않은 Refresh Token입니다.");
}
Jws<Claims> claims = parseToken(refreshToken);
Integer userId = claims.getPayload().get("user_id", Integer.class);
UserDetailsDto user = retrieveUser(Long.valueOf(userId));
return generateToken(user, accessExpiration);
}
private boolean isTokenInBlacklist(String token) {
return Boolean.TRUE.equals(redisTemplate.hasKey(token));
}
}
TokenService
와 AuthService
의 책임 분리TokenService
는 JWT 토큰의 생성과 검증을 전담하며, AuthService
는 블랙리스트 관리와 로그인/로그아웃의 비즈니스 로직을 담당합니다. 이를 통해 서비스의 책임을 분리하고, 각 기능이 명확히 구분됩니다.
로그아웃 시 사용된 Access Token
과 Refresh Token
은 Redis에 저장됩니다. 이후 Redis에서 해당 토큰이 블랙리스트에 있는지 확인하여 토큰의 유효성을 검증합니다. Redis를 사용하여 블랙리스트를 관리함으로써, 중앙화된 저장소에서 토큰의 상태를 효율적으로 관리할 수 있습니다.
private void addTokenToBlacklist(String token) {
long expiration = tokenService.getExpirationFromToken(token);
redisTemplate.opsForValue().set(token, "true", expiration, TimeUnit.MILLISECONDS);
}
private boolean isTokenInBlacklist(String token) {
return Boolean.TRUE.equals(redisTemplate.hasKey(token));
}