实现Spring Cloud Gateway 动态路由和内置过滤器
目前我们公司所有的业务服务都接入了Spring Cloud Gateway,而在接入的过程中,肯定会涉及到动态路由这块:路由配置从数据库或者Redis中加载。 同时,我们还实现了一些自定义的过滤器,有GlobalFilter和GatewayFilter类型。
GlobalFilter全局的过滤器,所有的请求都会经过这种类型的过滤器。GatewayFilter某个路由的过滤器,可以挂载到具体某个路由,非全局。
今天主要介绍下如果实现Spring Cloud Gateway的动态路由以及如何基于GatewayFilter实现一个内置过滤器
在实现动态路由之前,我们稍微阅读了下Spring Cloud Gateway的源码,会发现有一个InMemoryRouteDefinitionRepository类,它有3个实现方法
save保存路由delete删除路由getRouteDefinitions获取路由
这里操作的对象都是RouteDefinition实例,它只是一个路由信息定义,具体的路由实现是Route。
知道了Spring Cloud Gateway对于路由的处理方式,那么我们自己实现一套动态路由就非常简单了。 这里我们新增一个RedisRouteDefinitionLocator类,关键路由加载代码试下如下:
public Mono<Void> refresh(){
// 从Redis加载配置的路由信息
List<SysRouteDTO> routes = cacheTemplate.valueGetList(CommonConstants.SYS_ROUTE_KEY, SysRouteDTO.class);
for(SysRouteDTO route : routes){
try {
// 断言
List<PredicateDefinition> predicates = Lists.newArrayList();
PredicateDefinition predicateDefinition = buildPredicateDefinition(route);
predicates.add(predicateDefinition);
// 过滤器
List<FilterDefinition> filters = Lists.newArrayList();
FilterDefinition stripPrefixFilterDefinition = buildStripPrefixFilterDefinition(route);
filters.add(stripPrefixFilterDefinition);
if(StringUtil.isNotEmpty(route.getFilters())){
List<FilterDefinition> customFilters = JsonUtil.fromListJson(route.getFilters(), FilterDefinition.class);
this.reloadArgs(customFilters);
filters.addAll(customFilters);
}
// 元数据
Map<String, Object> metadata = this.buildMetadata(route);
// 代理路径
String targetUri = StringUtil.isNotEmpty(route.getUrl()) ? route.getUrl() : "lb://" + route.getServiceId();
URI uri = UriComponentsBuilder.fromUriString(targetUri).build().toUri();
// 构建路由信息
RouteDefinition routeDefinition = new RouteDefinition();
routeDefinition.setId(route.getRouteName());
routeDefinition.setPredicates(predicates);
routeDefinition.setUri(uri);
routeDefinition.setFilters(filters);
routeDefinition.setMetadata(metadata);
this.repository.save(Mono.just(routeDefinition)).subscribe();
}catch (Exception ex) {
log.error("路由加载失败: name={}, error={}", ex.getMessage(), ex);
}
}
return Mono.empty();
}
实现-内置过滤器
在Spring Cloud Gateway其实已经有很多的内置过滤器了,例如:AddRequestParameterGatewayFilter、AddRequestHeaderGatewayFilter等等。这些内置的过滤器都是GatewayFilter类型的,有需要才对某个路由进行配置,该路由才会加载该过滤器。
那么内置过滤器在动态路由的场景下,如果加载内置过滤器呢?其实很简单,和动态路由类似,我们把内置过滤器的相关信息,配置到数据库中,在加载路由的时候,把内置过滤器挂载到具体的路由即可。
在我们实现动态路由的代码中,有以下代码片段
if(StringUtil.isNotEmpty(route.getFilters())){
List<FilterDefinition> customFilters = JsonUtil.fromListJson(route.getFilters(), FilterDefinition.class);
this.reloadArgs(customFilters);
filters.addAll(customFilters);
}
在数据库中,我们配置了路由的filter字段,其实是一个json的字符串,同json反序列化为FilterDefinition,通过reloadArgs对内置过滤器进行参数的配置。
这里我们以一个校验验证码的内置过滤器为例。
首先,我们实现一个ValidateImageCodeGatewayFilterFactory类,代码如下:
public class ValidateImageCodeGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
private CacheTemplate cacheTemplate;
public ValidateImageCodeGatewayFilterFactory(CacheTemplate cacheTemplate) {
this.cacheTemplate = cacheTemplate;
}
@Override
public GatewayFilter apply(NameValueConfig config) {
return new ValidateImageCodeGatewayFilter(config, cacheTemplate);
}
}
这个其实是一个工厂类,继承了AbstractNameValueGatewayFilterFactory类,AbstractNameValueGatewayFilterFactory指定了参数的解析类型:NameValueConfig,即我们在数据库的对于自定义过滤器的参数配置,最后会通过NameValueConfig实例传递进来。
然后,我们再实现一个ValidateImageCodeGatewayFilter类,代码如下:
public class ValidateImageCodeGatewayFilter implements GatewayFilter {
private AbstractNameValueGatewayFilterFactory.NameValueConfig config;
private static final String DEFAULT_SSO_LOGIN = "/sso/app/login";
private CacheTemplate cacheTemplate;
public ValidateImageCodeGatewayFilter(AbstractNameValueGatewayFilterFactory.NameValueConfig config,
CacheTemplate cacheTemplate) {
this.config = config;
this.cacheTemplate = cacheTemplate;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String originPath = this.getOriginPath(exchange);
String validatePath = StringUtil.isEmpty(this.config.getValue()) ? DEFAULT_SSO_LOGIN : this.config.getValue();
if(!validatePath.equals(originPath)){
return chain.filter(exchange);
}
try {
this.check(exchange.getRequest());
}catch (CommonBusinessException ex){
ApiBaseResponse resp = new ApiBaseResponse();
resp.setResponseMessage(ex.getErrorMessage());
resp.setResponseCode(ex.getErrorCode());
// 设置响应值
return Mono.defer(() -> Mono.just(exchange.getResponse()))
.flatMap((response) -> {
response.setStatusCode(HttpStatus.OK);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
DataBufferFactory dataBufferFactory = response.bufferFactory();
DataBuffer buffer = dataBufferFactory.wrap(JsonUtil.toJson(resp).getBytes(Charset.defaultCharset()));
return response.writeWith(Mono.just(buffer)).doOnError((error) -> DataBufferUtils.release(buffer));
}
);
}
return chain.filter(exchange);
}
/**
* 验证码校验
* @param request
*/
private void check(ServerHttpRequest request){
String code = request.getQueryParams().getFirst("code");
if(StringUtil.isEmpty(code)){
throw new CommonBusinessException("-1", "验证码不能为空");
}
String randomStr = request.getQueryParams().getFirst("randomStr");
if(StringUtil.isEmpty(randomStr)){
throw new CommonBusinessException("-1", "随机数不能为空");
}
String key = CommonConstants.SYS_GATEWAY_CAPTCHA + randomStr;
String text = cacheTemplate.valueGet(key, String.class);
if(!code.equals(text)){
throw new CommonBusinessException("-1", "验证码错误");
}
cacheTemplate.keyRemove(key);
}
/**
* 获取实际路径
* @param exchange 上下文
* @return
*/
private String getOriginPath(ServerWebExchange exchange){
LinkedHashSet<URI> set = (LinkedHashSet<URI>)exchange.getAttributes().get(ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
if(CollectionUtils.isEmpty(set)){
return "";
}
String originPath = "";
for(URI uri : set){
originPath = uri.getPath();
break;
}
return originPath;
}
}
该过滤器,通过check方法校验了验证码的正确性,如果异常则抛出,然后返回给前端。getOriginPath方法是获取真实的URL,因为过滤器是挂载到具体的某个路由,在我们这个场景是挂载到单点登录SSO服务并且只有登录的接口才需要进行验证码的验证,其他接口不需要,所以这里需要获取当前请求的真实URL。
接着,我们在数据库对具体某个路由filters字段配置该内置过滤器的信息,如下:
[{"name": "ValidateImageCode", "args": { "path":"/sso/app/login" } }]
name字段就是指定了过滤器的名称,即完整的类名
ValidateImageCodeGatewayFilter去掉GatewayFilter即可args是一个map, key是path,value是需要验证的路径
这时候我们运行Spring Cloud Gateway会发现报错,原因是
NameValueConfig实例无法获取到正确的配置信息。
经过再次阅读Spring Cloud Gateway会发现,我们需要配置成如下格式才可以正确的加载配置:
[{"name": "ValidateImageCode", "args": { "_genkey_0":"path", "_genkey_1": "/sso/app/login" } }]
其实在加载动态路由的时候,reloadArgs方法就是做这个处理,代码如下:
private void reloadArgs(List<FilterDefinition> filterDefinitions){
if(CollectionUtils.isEmpty(filterDefinitions)){
return;
}
for(FilterDefinition definition : filterDefinitions){
Map<String, String> args = new HashMap<>();
int i = 0;
for(Map.Entry<String, String> entry : definition.getArgs().entrySet()){
args.put(NameUtils.generateName(i), entry.getKey());
args.put(NameUtils.generateName(i+1), entry.getValue());
i += 2;
}
definition.setArgs(args);
}
}
最后
经过以上说明,您学会了如何处理Spring Cloud Gateway的动态路由和内置过滤器了不?
相关源码地址:https://gitee.com/anyin/anyin-cloud/tree/master/anyin-center-modules/anyin-center-gateway

