欢迎光临散文网 会员登陆 & 注册

实现Spring Cloud Gateway 动态路由和内置过滤器

2023-02-13 14:45 作者:Anyin灬  | 我要投稿

引言

目前我们公司所有的业务服务都接入了Spring Cloud Gateway,而在接入的过程中,肯定会涉及到动态路由这块:路由配置从数据库或者Redis中加载。  同时,我们还实现了一些自定义的过滤器,有GlobalFilterGatewayFilter类型。

  • 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其实已经有很多的内置过滤器了,例如:AddRequestParameterGatewayFilterAddRequestHeaderGatewayFilter等等。这些内置的过滤器都是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

以上,如果有哪里不对,欢迎讨论。


实现Spring Cloud Gateway 动态路由和内置过滤器的评论 (共 条)

分享到微博请遵守国家法律