网关过滤器
前面有提到,如果用较为简洁的语言来阐述网关,几乎就等同于路由转发+过滤器。路由得转发、容错这一块的内容已经有所提及,该节记录一下网关的过滤器,以及如何使用网关+Nacos 实现一个 IP 黑白名单的访问限制。
Spring Cloud Gateway 的过滤器根据作用范围可以分为 GatewayFilter 和 GlobalFilter 两种。
GatewayFilter 是网关过滤器,是需要通过 spring.cloud.routes.filters 配置在具体路由下,只作用在当前路由上或通过 spring.cloud.default-filters 配置在全局,作用在所有路由上的。
GlobalFilter 则是全局过滤器,不需要在配置文件中配置,作用在所有的路由上,最终通过 GatewayFilterAdapter 包装成 GatewayFilterChain 可识别的过滤器,是网关的核心过滤器,作用在每个路由上,不需要配置系统初始化就可以加载。
对于 GatewayFilter 网关过滤器来说,主要是用于拦截并处理 web 请求,用于切割业务,比如把安全等内容切割出来进行过滤,此处不多赘述。
而全局过滤器,则是将请求的业务及路由的 URI 转换为真是业务服务请求地址的核心过滤器。
这两种过滤器自带许多比较实用的过滤方法,但在有些情况下,会更希望使用自己实现自定义的过滤器。
对于自定义的过滤器来说,有多种实现方法。比如实现两个接口,一个是 GatewayFilter,一个是 Ordered,如下所示为一个自定义过滤器的示例:
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 29 30
| @Component public class AuthGlobalFilter implements GlobalFilter, Ordered {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthGlobalFilter.class);
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getHeaders().getFirst("Authorization"); if (StrUtil.isEmpty(token)) { return chain.filter(exchange); } try { String realToken = token.replace("GaGaDuck: ", ""); JWSObject jwsObject = JWSObject.parse(realToken); String userStr = jwsObject.getPayload().toString(); ServerHttpRequest request = exchange.getRequest().mutate().header("user", userStr).build(); exchange = exchange.mutate().request(request).build(); } catch (ParseException e) { e.printStackTrace(); } return chain.filter(exchange); }
@Override public int getOrder() { return 1; } }
|
这样一个过滤器用于过滤全局的用户,并将登陆用户的 JWT 信息转化为用户信息。检查请求中是否包含 Authorization 头,如果存在,则解析 JWT 并从中提取用户信息,然后将其设置到请求的 Header 中。还提供了一个 getOrder 方法来定义过滤器的执行顺序。
也可以通过 WebFilter 这个接口进行实现,用于处理异步非阻塞的 HTTP 请求,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Component public class IgnoreUrlsRemoveJwtFilter implements WebFilter {
@Resource private IgnoreUrlsConfig ignoreUrlsConfig;
@Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { System.out.println("==================进入到了白名单路径访问的过滤器中=============="); ServerHttpRequest request = exchange.getRequest(); URI uri = request.getURI(); PathMatcher pathMatcher = new AntPathMatcher(); List<String> ignoreUrls = ignoreUrlsConfig.getUrls(); for (String ignoreUrl : ignoreUrls) { if (pathMatcher.match(ignoreUrl, uri.getPath())) { request = exchange.getRequest().mutate().header("Authorization", "").build(); exchange = exchange.mutate().request(request).build(); return chain.filter(exchange); } } return chain.filter(exchange); } }
|
如上是一个检查请求的 URI 是否在白名单中的过滤器,如果在白名单中,便移除请求的 HWT 认证头。此外,还可以在 security 的过滤链中可以设置其的过滤器执行顺序:
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
| @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http.oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .jwtAuthenticationConverter(jwtAuthenticationConverter()) ) .authenticationEntryPoint(restAuthenticationEntryPoint) ); http.addFilterBefore(ignoreUrlsRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION) .addFilterBefore(menuFilter, SecurityWebFiltersOrder.AUTHORIZATION) .authorizeExchange(exchange -> exchange .pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(),String.class)).permitAll() .anyExchange().access(authorizationManager)) .exceptionHandling(exceptionHandling -> exceptionHandling .accessDeniedHandler(restfulAccessDeniedHandler) .authenticationEntryPoint(restAuthenticationEntryPoint)) .csrf(ServerHttpSecurity.CsrfSpec::disable); return http.build(); }
|
这样一条过滤链便是在认证过滤器之前添加了一个过滤器,用于处理忽略的白名单 URI。
对于过滤器来说,看一看其官方的接口:
1 2 3 4 5
| init(FilterConfig) doFilter(ServletRequest, ServletResponse, FilterChain) destroy()
filter(ServerWebExchange exchange, GatewayFilterChain chain)
|
可以看到,其核心方法就是 doFilter 或 filter,那么,当需要实现一个过滤器的时候,便可以从 doFilter 或者 filter 方法下手,进行过滤。比如,由此实现一个对于 IP 黑白名单限制的一个过滤。
网关+Nacos 配置中心得 IP 黑白名单限制
对于一个 IP 黑白名单限制的过滤器来说,首先需要从 request 对象中获得对应的客户端 IP:
1 2 3 4 5
| // 获取request对象 ServerHttpRequest request = exchange.getRequest() ServerHttpResponse response = exchange.getResponse() // 从request对象中获取客户端ip String clientIp = request.getRemoteAddress().getHostString()
|
而后便可以根据黑白名单模式及黑白名单的列表进行判断及过滤:
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
| if (isBlackList && blackList.contains(clientIp)) { System.out.println("blackList1, clientIp = " + clientIp); response.setStatusCode(HttpStatus.UNAUTHORIZED); String data = "Request be denied! IP in blacklist and mode is blackList now!"; DataBuffer wrap = response.bufferFactory().wrap(data.getBytes()); return response.writeWith(Mono.just(wrap)); } else if (!isBlackList && whiteList.contains(clientIp)) { System.out.println("whiteList1, clientIp = " + clientIp); return chain.filter(exchange); } else if (!isBlackList && !whiteList.contains(clientIp)) { System.out.println("whiteList2, clientIp = " + clientIp); response.setStatusCode(HttpStatus.UNAUTHORIZED); String data = "Request be denied! IP not in whitelist and mode is whiteList now!"; DataBuffer wrap = response.bufferFactory().wrap(data.getBytes()); return response.writeWith(Mono.just(wrap)); } else { System.out.println("blackList2, clientIp = " + clientIp); return chain.filter(exchange); }
|
而对于这样一个黑白名单的配置,同样可以采用 nacos 配置中心动态更新的方法:
通过实现一个 loadConfig,载入配置:
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 29 30 31 32 33 34 35 36 37 38 39 40 41
| private void loadConfig() throws NacosException { String blackIpDataId = blackIpList; String whiteIpDataId = whiteIpList; String ipDataId = IPDataId; String group = groupConfig; String configBlack = nacosConfigManager.getConfigService().getConfig(blackIpDataId, group, 5000); String configWhite = nacosConfigManager.getConfigService().getConfig(whiteIpDataId, group, 5000); String configIpManagement = nacosConfigManager.getConfigService().getConfig(ipDataId, group, 5000);
if (Objects.equals(configIpManagement, "true")) { System.out.println("采用黑名单模式限制终端IP"); isBlackList = true; } else { System.out.println("采用白名单模式限制终端IP"); isBlackList = false; }
if (configBlack != null) { System.out.println("now is in blackText"); System.out.println(configBlack); String[] ips = configBlack.split(","); blackList.clear(); for (String ip : ips) { blackList.add(ip.trim()); } }
if (configWhite != null) { System.out.println("now is in whiteText"); System.out.println(configWhite); String[] ips = configWhite.split(","); whiteList.clear(); for (String ip : ips) { whiteList.add(ip.trim()); } } }
|
在此基础上加入监听器监听配置文件的变化,在接收到nacos的变更后便重新加载配置,由此实现IP黑白名单的动态限制。
详细实现可参见https://github.com/gagaducko/springcloud-microservice-security/tree/main/gateway-service