일단 게이트웨이에서 사용되는 필터 중 아래의 두 가지를 써볼 것이다.
- Global Filter: 모든 api 요청 시 필터 적용(먼저 실행)
- Custom Filter: 설정된 api 요청만 필터 적용
참고로 글로벌 필터랑 커스텀 필터 적용하는 방법은 크게 차이가 안 난다.
filter 생성
나는 전역 필터로 모든 접근(로그인, 회원가입 제외)에 jwt 검증 필터를 실행할 것이다.
AuthenticationFilter
// Global Filter 적용
@Component
@Slf4j
public class AuthenticationFilter extends AbstractGatewayFilterFactory<AuthenticationFilter.Config> {
@Autowired
private RouteValidator validator;
@Autowired
private JwtUtil jwtUtil;
public AuthenticationFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
// pre
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
if (validator.isSecured.test(request)) {
// JWT 검증 로직
List<String> authHeaders = request.getHeaders().get(HttpHeaders.AUTHORIZATION);
if (authHeaders == null || authHeaders.isEmpty()) {
throw new MissingAuthorizationHeaderException();
}
String authHeader = authHeaders.get(0);
if (authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
jwtUtil.validateToken(token);
} catch (Exception e) {
throw new UnauthorizedAccessException();
}
} else {
throw new InvalidAuthorizationHeaderFormatException();
}
}
// post
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
log.info("Custom Post filter: response code: " + response.getStatusCode());
}));
};
}
@Data
public static class Config {
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
}
메소드를 보기 전에 pre, post란
- pre: 엔드포인트까지 가기 전에 작동되는 필터
- post: 엔드포인트를 방문한 후에 작동되는 필터
이렇게 생각하면 된다.
그래서 코드에서는 첫 return에서 pre 필터(JWT 토큰 검증)를 실행하고 두번째 return 때는 post 필터를 작동 시킨다.
그리고 pre filter에서
if (validator.isSecured.test(request)) {
이 부분이 있는데 예외 경로일 경우 그냥 엔드포인트로 가게 하는 것이다.
설명은 바로 아래
예외 경로 설정
로그인과 회원가입 시에는 jwt가 없기 때문에 예외 처리를 해줘야 한다.
RouteValidator
@Component
public class RouteValidator {
public static final List<String> openApiEndpoints = List.of(
"/eureka",
"/coupon/api/v1/coupon/issued-check/",
"/auth/api/register",
"/auth/api/login"
);
public Predicate<ServerHttpRequest> isSecured =
request -> openApiEndpoints
.stream()
.noneMatch(uri -> request.getURI().getPath().contains(uri));
}
openApiEndpoints에 지정하고 싶은 경로를 설정해주면 되는데
예를 들어 쿠폰 api의 모든 요청을 열어두고 싶으면
/coupon/api/**이 아닌
/coupon/api/ 이렇게 해줘야 한다.
JWT 검증
JwtUtil
@Component
public class JwtUtil {
@Value("${jwt.secretKey}")
private String secretKey;
public void validateToken(final String token) {
Jwts.parserBuilder().setSigningKey(getSignKey()).build().parseClaimsJws(token);
}
private Key getSignKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}
application.yml에 적용
GlobalFilter로 적용 시
cloud:
gateway:
default-filters:
- name: AuthenticationFilter
args:
baseMessage: Spring Cloud Gateway Global Filter
preLogger: true
postLogger: true
default-filters에 적용시키면 된다.
CustomFilter 적용 시
cloud:
gateway:
routes:
- id: coupon
uri: lb://COUPON
predicates:
- Path=/coupon/**
filters:
- AuthenticationFilter
예를 들어 쿠폰 서비스에만 JWT 검증 필터를 적용시키고 싶으면 filters에 클래스명 넣어주면 된다.
나중에 예시로 커스텀 필터에서는 유저의 role을 확인 해 특정 api 요청에는 접근을 제한할 수 있다.
실행
유효 JWT로 쿠폰 요청
유효하지 않은 JWT로 쿠폰 요청
참고로 깃허브에 올린 exception은 예외 처리한 부분이다.