学习笔记—微服务—技术栈实践(6)—网关过滤器+Nacos的IP黑白名单限制

网关过滤器

  前面有提到,如果用较为简洁的语言来阐述网关,几乎就等同于路由转发+过滤器。路由得转发、容错这一块的内容已经有所提及,该节记录一下网关的过滤器,以及如何使用网关+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) {
// 如果token为空,则八行
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (StrUtil.isEmpty(token)) {
return chain.filter(exchange);
}
try {
//从token中解析用户信息并设置到Header中去
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();
//白名单路径移除JWT请求头
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
// security的过滤链
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
.authenticationEntryPoint(restAuthenticationEntryPoint)
);
// 增加过滤器过滤忽略的白名单Url
http.addFilterBefore(ignoreUrlsRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION)
// 增加过滤器过滤菜单校验的请求
.addFilterBefore(menuFilter, SecurityWebFiltersOrder.AUTHORIZATION)
.authorizeExchange(exchange -> exchange
// 直接在properties中配置的白名单
.pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(),String.class)).permitAll()
// 通过鉴权管理器配置的放行权限名单
.anyExchange().access(authorizationManager))
.exceptionHandling(exceptionHandling -> exceptionHandling
// 返回未授权处理的信息
.accessDeniedHandler(restfulAccessDeniedHandler)
// 返回未未认证处理
.authenticationEntryPoint(restAuthenticationEntryPoint))
// 禁用 CSRF
.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
// 如果在黑名单模式同时IP在黑名单的时候
if (isBlackList && blackList.contains(clientIp)) {
// 拒绝黑名单上IP的访问,返回信息
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)) {
// 如果说是白名单模式并且白名单中有这个IP的时候
// 可以访问
// 合法请求,放⾏,执⾏后续的过滤器
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 {
// 剩下的情况,一个是黑名单模式,但是这个IP不在黑名单中
System.out.println("blackList2, clientIp = " + clientIp);
return chain.filter(exchange);
}

  而对于这样一个黑白名单的配置,同样可以采用 nacos 配置中心动态更新的方法:

1
@RefreshScope

  通过实现一个 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;
}

// 加入nacos中黑名单的IP
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());
}
}

// 加入nacos中白名单的ip
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


学习笔记—微服务—技术栈实践(6)—网关过滤器+Nacos的IP黑白名单限制
https://gagaducko.github.io/2024/09/13/学习笔记—微服务—技术栈实践-6-—网关-Nacos的IP黑白名单限制/
作者
gagaduck
发布于
2024年9月13日
许可协议