本文正在参加「Java主题月 – Java Debug笔记活动」,详情查看 活动链接
背景
事情是这样的我想做个自定义拦截器,在每次请求SpringCloudGateway服务时,gateway拦截到特定的url,去做用户认证鉴权,看是否有操作url的权限。
然而发现在application.yml配置路由规则不生效。如:我想访问gateway的ip:port/user-server/UserService/login 时,想去做鉴权(RequestAuthHeaderGateway里实现),但是没有被拦截到。
注: user-server是我一个springcloud服务的spring.application.name=的值
server:
port: 80
spring:
cloud:
gateway:
discovery:
locator:
lowerCaseServiceId: true # 服务名小写
enabled: true # 表明gateway开启服务注册和发现的功能,并且spring cloud gateway自动根据服务发现为每一个服务创建了一个router
routes:
- id: user
uri: lb://user-server
predicates:
- Path=/user-server/**
filters:
- StripPrefix=1
- RequestAuthHeader
复制代码
出现的问题
访问springcloud组件的gateway的ip:port/user-server/login,并没有被application.yml里的routes的拦截规则Path给拦截到。大家猜猜看看什么原因。
解决问题的前提知识
-
要了解GatewayFilter实现方式
-
要了解GatewayFilterFactory实现方式
-
nacos注册机制
-
了解SpringCloudGateway开启发现注册中心其他服务,并为所有服务创建默认路由规则。
规则命名为/服务名字/*,就能调用到服务下的/*的url
spring:
cloud:
gateway:
discovery:
locator:
lowerCaseServiceId: true # 服务名小写
enabled: true # 表明gateway开启服务注册和发现的功能,并且spring cloud gateway自动根据服务发现为每一个服务创建了一个router
复制代码
- 要知道Path里填写的是,gateway服务ip:port后面的全部uri
spring:
cloud:
gateway:
routes:
- id: user
uri: lb://user-server
predicates:
- Path=/user-server/**
filters:
- StripPrefix=1
- RequestAuthHeader
复制代码
- 自定义的拦截器的代码
这个是我的自定义GatewayFilterFactory代码,用于实现拦截认证的逻辑。这个代码逻辑没问题。
public class RequestAuthHeaderGatewayFilterFactory
extends AbstractGatewayFilterFactory<RequestAuthHeaderGatewayFilterFactory.Config> {
public RequestAuthHeaderGatewayFilterFactory() {
super(Config.class);
}
public static class Config {
// TODO: relaxed HttpStatus converter
private String status;
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}
@Override
public GatewayFilter apply(Config config) {
return new RequestAuthHeaderFilter();
}
}
复制代码
排查问题阶段
- 先找到自己代码被调用的地方
在自定义的RequestAuthHeaderGatewayFilterFactory中,对apply方法进行打断点。
@Override
public GatewayFilter apply(Config config) {
return new RequestAuthHeaderFilter();
}
复制代码
发现被RouteDefinitionRouteLocator类loadGatewayFilters这个方法调用。
@SuppressWarnings("unchecked")
List<GatewayFilter> loadGatewayFilters(String id,
List<FilterDefinition> filterDefinitions) {
ArrayList<GatewayFilter> ordered = new ArrayList<>(filterDefinitions.size());
for (int i = 0; i < filterDefinitions.size(); i++) {
FilterDefinition definition = filterDefinitions.get(i);
GatewayFilterFactory factory = this.gatewayFilterFactories
.get(definition.getName());
if (factory == null) {
throw new IllegalArgumentException(
"Unable to find GatewayFilterFactory with name "
+ definition.getName());
}
if (logger.isDebugEnabled()) {
logger.debug("RouteDefinition " + id + " applying filter "
+ definition.getArgs() + " to " + definition.getName());
}
// @formatter:off
Object configuration = this.configurationService.with(factory)
.name(definition.getName())
.properties(definition.getArgs())
.eventFunction((bound, properties) -> new FilterArgsEvent(
// TODO: why explicit cast needed or java compile fails
RouteDefinitionRouteLocator.this, id, (Map<String, Object>) properties))
.bind();
// @formatter:on
// some filters require routeId
// TODO: is there a better place to apply this?
if (configuration instanceof HasRouteId) {
HasRouteId hasRouteId = (HasRouteId) configuration;
hasRouteId.setRouteId(id);
}
// 就在这里调用我们自定义的GateWayFactory类
GatewayFilter gatewayFilter = factory.apply(configuration);
if (gatewayFilter instanceof Ordered) {
ordered.add(gatewayFilter);
}
else {
ordered.add(new OrderedGatewayFilter(gatewayFilter, i + 1));
}
}
return ordered;
}
复制代码
可以看出在yml里配置的routes拦截器是在这里进行注册。让url在这里匹配命中的拦截器,就是yml里一个id下的每个gatewayFilter实现类。
- 对被调地方进行调查机制
对loadGatewayFilters方法第一行进行打断点。发现,这个方法时定时执行的。
loadGatewayFilters(String id,List<FilterDefinition> filterDefinitions)
复制代码
然而他定时的去做什么呢?
我再看到这个方法的参数id,参数正是我们nacos里所有服务名字。看上面的第二个参数,filterDefinitions是要处理的gateWayfilter。filterDefinitions看这个参数的内容。
[FilterDefinition{name='RewritePath', args={regexp=/properties-server/(?<remaining>.*), replacement=/${remaining}}}]
复制代码
- 发现可疑
我们发现他执行了RewritePathGatewayFilter,拦截的路径为/user-server。这个RewritePathGatewayFilter是用于改写url的。
可是看我之前application.yml里面根本没有配置RewritePath,不会去重写url。那这个重写url是谁操作的呢?
看他的重写正则表达式的规则,用正则表达式 把/user-server/** 内容,替换为/** 。
如:
/user-server/**
复制代码
路径,全部替换为
/**
复制代码
- 发现规律
正好就是SpringCloudGateway的另一个机制,为所有的nacos里的服务,对其servicename生成一个虚拟域名,作为负载均衡的名字,调用这个名字时即可获得这个服务的其中一个ip。这个也是restTemplate/feign调用远程服务时,所需要的填写的一个名字。
然而这个机制被gateway服务执行,则证明是springcloudgateway机制所执行上述定时任务,来作为gateway服务调用其他微服务的机制。
发现正是因为下述代码,开启了gateway定位其他服务的功能了。
spring:
cloud:
gateway:
discovery:
locator:
lowerCaseServiceId: true # 服务名小写
enabled: true # 表明gateway开启服务注册和发现的功能,并且spring cloud gateway自动根据服务发现为每一个服务创建了一个router
复制代码
- 找到冲突原因
就是这里和我配置的application.yml里的所有routes中,Path正好带有服务名的url,发生了冲突。
因为我application.yml的routes填写的path是/user-server/** ,正好与上述代码中enabled开启了gateway服务发现功能,导致执行RewritePathGatewayFilter重写url功能时,匹配的也正好是/user-server/** ,最终形成两个功能冲突
解决方案
方式1.
自己配置application.yml中的routes,匹配的url不要带服务名字。这个方式,可以开启spring.cloud.gateway.locator.enable=true功能。这样就不会形成因路径问题,而形成的冲突。
方式2.
application.yml中不开启gateway的locator功能。因为站在gateway是统一对外网的服务。不应该提供这个功能,因为一旦已开启,nacos的每个服务,都能被访问到,形成对外网的风险。
其次而且根据/服务名/** ,这样的形式访问,不会被routes配置的拦截规则给拦截到,本文就是描述这个问题的原因。
建议只使用routes里配置的规则,这种做法虽然在这个时候Path里填写的路径中,包含不包含服务名字都可以被拦截。但是我还是建议Path不包含服务名字,以免被外网的人,知道服务名字信息。作为减少信息泄露。