这篇文章主要讲 PmHub 中如何实现自定义网关全局过滤器以及如何进行接口耗时统计。会从理论到实战再到面试,依次展开。
自定义 SpringCloud Gateway 全局过滤器实现自定义网关统一鉴权,统计接口调用耗时情况。
理论知识
什么是网关?
微服务架构已经是现代应用程序开发的主流方式,同时于我们而言,也是面试中必须掌握的重中之重。
微服务架构是将之前的单体应用拆分成多个小型微服务,每个服务都可以独立部署、扩展和维护。然而,微服务架构也带来了一些挑战,其中之一就是服务间通信的管理。这时候,微服务网关就成为了必不可少的组件。
微服务网关是一个位于微服务架构前端的组件,它充当了所有微服务的入口。微服务网关负责路由请求、负载均衡、安全认证、流量控制、监控和日志记录等任务。微服务网关可以将多个微服务组合成一个统一的API,从而简化了客户端与微服务之间的通信。
API网关我觉得可以理解成是微服务系统的门卫,是微服务架构中一个关键的组件,负责管理和调控外部请求进入内部微服务的流量。为了更好理解,拿个生活中的例子来对比下:
-个大型的购物中心(微服务系统),里面有很多不同的商店(不同的微服务),比如服装店、餐馆、电影院等等。每个商店都有自己独立的入口,这样的好处是每个商店都可以独立运营。但是,如果每个顾客都直接去商店入口没有统一入口,会非常混乱
而且,购物中心需要对每个商店的顾客流量进行管理,比如防止某些商店人满为患或者统一处理会员优惠等。
网关能干嘛?
常见的微服务网关有哪些?
常见的微服务网关实现包括 Spring Cloud Netflix 的 Zuul 和 Spring CloudGateway,以及其他流行的网关如 Kong。
开源网关 Kong Gateway 是一个轻量级的 API 网关,基于 OpenResty + Lua 开发,提供了丰富的功能以及高度灵活的扩展性,可以通过插件的方式扩展 Kong 的功能。
网关介绍
Kong Gateway 有如下优势:
高性能:Kong 基于 Nginx 和 OpenResty,具有非常高的性能和可扩展性,适用于处理高并发和高吞吐量的场景。
插件生态:Kong 提供了丰富的插件,可以轻松实现身份验证、限流、日志记录监控等功能,并且可以通过 Lua 自定义插件。
多语言支持:Kong 支持多种编程语言,通过其插件系统,可以用Lua、Go、
Python 等语言编写插件。
跨平台支持: Kong 可以在多种平台上运行,如Kubernetes、Docker等,适合多种环境部署。
企业支持: Kong 提供企业版,包含更多高级功能和商业支持,适用于需要企业级支持和服务的场景。
但是由于 Kong 学习成本较高,自定义插件要用到 Lua 语言,对 Java 不大友好,所以使用 Kong 作为 Java 升天的网关并不常见。
接下来在 Java 生态下能打的就剩下 SpringCloud Gateway 和 Zuul 这两哥们,但在Cloud 全家桶中,2.x版本 zuul 的升级一直跳票,SpringCloud最后自己研发了一个网关SpringCloud Gateway替代Zuul,所以 SpringCloud Gateway 是原 zuul1.x 版的替代。
新项目就直接用 SpringCloud Gateway 就好了,
下面我们主要讲-讲主流 SpringCloud Gateway 和 Zuu| 这两大网关。
SpringCloud Gateway vs Zuul
Gateway 三大核心
看官网介绍可知,Spring Cloud Gateway 三大核心组件分别是路由(Route)、断言(Predicate)、过滤器(Filter),构成了网关的必要功能。
web 前端请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。
predicate就是我们的匹配条件
filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了。
三大核心组件
路由(Route)
路由是构建网关的基本模块,"它由 ID,目标 URI,一系列的断言和过滤器组成,,如果断言为 true 则匹配该路由。
在 PmHub 中网关的路由配置如下:
spring:cloud:gateway:discovery:locator:lowerCaseServiceId: trueenabled: trueroutes:# 认证中心- id: pmhub-authuri: lb://pmhub-authpredicates:- Path=/auth/**filters:# 验证码处理- CacheRequestFilter# - ValidateCodeFilter- StripPrefix=1# 代码生成- id: pmhub-genuri: lb://pmhub-genpredicates:- Path=/gen/**filters:- StripPrefix=0# 定时任务- id: pmhub-joburi: lb://pmhub-jobpredicates:- Path=/schedule/**filters:- StripPrefix=0# 系统模块- id: pmhub-systemuri: lb://pmhub-systempredicates:- Path=/system/**filters:- StripPrefix=0# 项目模块- id: pmhub-projecturi: lb://pmhub-projectpredicates:- Path=/project/**filters:- StripPrefix=0# 流程模块- id: pmhub-workflowuri: lb://pmhub-workflowpredicates:- Path=/workflow/**filters:- StripPrefix=0
拿认证中心服务来说,id 取的就是 auth 在nacos注册的服务名,这样,请求网关的 URL 中带有[/auth/**」的请求都会被转发到认证中心这个服务上来。
在spring cloud gateway中配置uri有三种方式,包括
·websocket配置方式
·http地址配置方式
·注册中心配置方式
其中 PmHub 中采用的是这种通过 Nacos 配置中心的配置方式
断言(Predicate)
断言可以理解为是匹配规则,比如在 PmHub 中配置的[-Path=/auth/**」就代表所有符合这个路径的规则都会被转发到对应的服务上面来。可以看下官网介绍。
简而言之,Predicate 就是为了实现一组匹配规则,让请求过来找到对应的Route 进行处理。
Spring Cloud Gateway创建 Route 对象时,使用RoutePredicateFactory创建 Predicate对象,Predicate 对象可以赋值给Route.
Spring Cloud Gateway包含许多内置的Route Predicate Factories.
·所有这些断言都匹配 HTTP 请求的不同属性。
多个Route Predicate Factories可以通过逻辑与(and)结合起来一起使用。
路由断言工厂 RoutePredicatefactory包含的主要实现类如图所示,包括Datetime、请求的远端地址、路由权重、请求头、Hos 地址、请求方法、请求路径和请求参数等类型的路由断言。
RoutePredicatefactory整体框架
当然了除了我们定义的规则,也是可以支持一下路由规则的自定义的,以下是一些常用的断言。
·Weight-匹配权重
·Datetime-匹配日期时间之后发生的请求
·Query-匹配查询参数
·Path-匹配请求路径
·Header-匹配具有指定名称的请求头
\d+值匹配正则表达式
当然了,内置的模板不满足需求,也是可以自定义断言规则的,方法也比较简单,按照以下套路即可:
要么继承 AbstractRoutePredicateFactory 抽象类
要么实现 IRoutePredicateFactory:接口
类开头任意取名,但是必须以 RoutePredicateFactory后缀结尾
如下代码:
@componentpublic class MyRoutePredicatefactory extends AbstractRoutePredicatefactory<MyRoutePredicatefactory.config>{public MyRoutePredicateFactory(){super(MyRoutePredicateFactory.Config.class);}@Validatedpublic static class config{@setter@Getter@NotEmptyprivate string userType;//钻、金、银等用户等级}@overridepublic Predicate<ServerwebExchange> apply(MyRoutePredicatefactory.config config){return new Predicate<ServerWebExchange>(){@overridepublic boolean test(ServerwebExchange serverwebExchange){//检查request的参数里面,userType是否为指定的值,符合配置就通过String userType = serverwebExchange.getRequest().getQueryParams().getFirst("userType”);if(userType == null)return false;//如果说参数存在,就和config的数据进行比较if(userType.equals(config.getuserType())){return true;}return false;}};}
}
过滤器(Filter)
网关中的过滤器,有点类似 SpringMVC 里面的拦截器 Interceptor 以及 Servlet 的过滤器,其中[pre」和[post」分别会在请求被执行前调用和被执行后调用,用来修改请求和响应信息。
过滤器也是面试中最常问的知识点,比如记录接口调用时长统计、限流、黑白名单等
有点类似 SpringMVC 里面的拦截器 Interceptor 以及 Servlet 的过网关中的过滤器,其中「pre」和「post」分别会在请求被执行前调用和被执行后调用,用来修改请求和响应信息。
按照类型分的话,过滤器分为全局默认过滤器、单一内置过滤器和自定义过滤器
全局过滤器
全局过滤器作用于所有的路由,不需要单独配置,我们可以用它来实现很多统一化处理的业务需求,比如权限认证,IP 访问限制等等。目前网关统一鉴权 AuthFilter.java 就是采用的全局过滤器。
单独定义只需要实现 GlobalFilter, Ordered 这两个接口就可以了
PmHub 中的具体实现,会放在下面的实战部分进行说明。
单一内置过滤器
单一内置过滤器也可以称为网关过滤器这种过滤器主要是作用于单一路由或者某个路由。
单一内置过滤器
有以下几种常见的单一过滤器
指定请求头内容
可以过滤掉指定请求头的路径,比如我希望此方法只允许请求头中带有“X-Requestpmhub”或者“X-Request-pmhub2”的请求。
public class GatewayFilter{@GetMapping(value ="/pay/gateway/filter")public AjaxResult getGatewayFilter(HttpservletRequest request){String result = "";Enumeration<string> headers = request.getHeaderNames();while(headers.hasMoreElements()){String headName = headers.nextElement();String headValue = request.getHeader(headName);System.out.println("请求头名:"+ headName +"\t\t\t"+"请求头值:"+ headValue);if(headName.equalsIgnorecase("X-Request-pmhub")|| headName.equalsIgnoreCase("x-Request-pmhub2")){result =result+headName +"\t " + headValue + "";}}return AjaxResult.success("getGatewayFilter 过滤器 test:"+result+"\t "+ Dateutil.now());}
}
那就可以在配置中做如下配置即可:
predicates:-Path=/auth/gateway/info/** #断言,路径相匹配的进行路由-id: pmhub routh3 #pay routh3uri:1b://cloud-pmhub-service #匹配后提供服务的路由地址predicates:-Path=/pay/gateway/filter/**filters:-AddRequestHeader=X-Request-pmhub,pmhubValue1 #请求头kv,若一头含有多参则重写一行设置-AddRequestHeader=X-Request-pmhub2,pmhubValue2
那么方法就能针对特定请求头内容做逻辑处理就好了,这样针对于请求头中的内容可以做过滤,可用于其他鉴权等情况。
·指定请求参数
对于特定请求参数进行过滤,只有带有该参数的请求才可执行逻辑。
predicates:
#断言,路径相匹配的进行路由
Path=/auth/gateway/filter/**
filters:
-AddRequestParameter=customerId,9527001 #新增请求参数Parameter:k,v
-RemoveRequestParameter=customerName #删除url请求参数customerName,你传递过来也是nul]
指定回应头
可以添加响应头信息,这样对于下游系统或者 web 可以做相应的逻辑处理和鉴权。这个过滤器应用场景可以无限发挥你的想象。
predicates:
#断言,路径相匹配的进行路由
Path=/auth/gateway/filter/**
filters:
-AddResponseHeader=X-Response-pmhub, BlueResponse #新增响应参数Response-pmhub,并设置值为BlueResponse
指定前缀和路径
很好理解,就是能对前缀和路径进行过滤,还可以进行路径重定向,配置如下
predicates:
#断言,路径相匹配的进行路由
Path=/auth/gateway/filter/**
filters:-PrefixPath=/pmhub # htttp://localhost:6880/pmhub/getway/filter-RedirectTo=302,https://laigeoffer.cn/ #访问htttp://localhost:6880/pmhub/getway/filter跳转到https://laigeoffer.cn/
自定义过滤器
经典面试题:如何统计接口调用耗时情况,说说设计思路?
这里我们就可以利用 gateway 的自定义过滤器功能来实现该功能。需要自定义全局 filter,只需要实现GlobalFilter Ordered 这两个接口,并在 filter 方法中进行接口访问耗时情况统计即可,比如这个 demo:
限流配置
限流,顾名思义,就是对流量进行限制。通过实施限流措施,我们可以有效地管理系统的每秒请求数(QPS),从而实现对系统的保护。
常见的限流算法包括:计数器算法、漏桶算法(Leaky Bucket)、以及令牌桶算法(Token Bucket)。
在Spring Cloud Gateway 中,官方提供了RequestRateLimiterGatewayFilterfactory 过滤器工厂,通过结合 Redis和 Lua 脚本,实现了基于令牌桶的限流方式。
1.添加依赖
2.限流规则,根据URL限流
spring:
redis:
host: localhost
port:6379
password:
cloud:
gateway:
routes:
#系统模块- id: pmhub-system
uri: lb://pmhub-system
predicates:
- Path=/system/**filters:
-StripPrefix=1-name: RequestRateLimiter
args :
redis-rate-limiter.replenishRate:1#令牌桶每秒填充速率redis-rate-limiter.burstcapacity:2#令桶总容量key-resolver:"#{@pathKeyResolver}"#使用 SpEL 表达式按名称引用 bean
... tip
StripPrefix=1配置,表示网关转发到业务模块时候会自动截取前缀。这个配置需要视情况而定
3.编写URL限流配置类
/**
*限流规则配置类
*/
@Configuration
public class KeyResolverConfigration{@Beanpublic KeyResolver pathKeyResolver(){return exchange ->Mono.just(exchange.getRequest().getURI().getPath());
}
4.测试服务验证限流
启动网关服务 PmHubGatewayApplication.java 和系统服务PmHubSystemApplication.java。
因为网关服务有认证鉴权,可以在 gateway 配置中设置一下白名单/system/**在进行测试,多次请求会发现返回 HTTP ERROR 429,同时在 redis 中会存在两个 key,表示限流成功。
request rate limiter.{xxx}.timestamp
{xxx}.tokens
也可以根据其他限流规则来配置,如参数限流,IP限流,配置如下
黑名单配置
顾名思义,黑名单就是那些被禁止访问的URL。为了实现这一功能,可以创建自定义过滤器BlackListUrlFilter,并配置黑名单地址列表blacklistUr。当然,如果有其他需求,还可以实现自定义规则的过滤器来满足特定的过滤要求。
pmhub 中黑名单过滤器配置:
/*** 黑名单过滤器** @author canghe*/
@Component
public class BlackListUrlFilter extends AbstractGatewayFilterFactory<BlackListUrlFilter.Config>
{@Overridepublic GatewayFilter apply(Config config){return (exchange, chain) -> {String url = exchange.getRequest().getURI().getPath();if (config.matchBlacklist(url)){return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "请求地址不允许访问");}return chain.filter(exchange);};}public BlackListUrlFilter(){super(Config.class);}public static class Config{private List<String> blacklistUrl;private List<Pattern> blacklistUrlPattern = new ArrayList<>();public boolean matchBlacklist(String url){return !blacklistUrlPattern.isEmpty() && blacklistUrlPattern.stream().anyMatch(p -> p.matcher(url).find());}public List<String> getBlacklistUrl(){return blacklistUrl;}public void setBlacklistUrl(List<String> blacklistUrl){this.blacklistUrl = blacklistUrl;this.blacklistUrlPattern.clear();this.blacklistUrl.forEach(url -> {this.blacklistUrlPattern.add(Pattern.compile(url.replaceAll("\\*\\*", "(.*?)"), Pattern.CASE_INSENSITIVE));});}}}
以后只要是看哪个 URL不爽,就直接拉进很名单即可。
spring:cloud:gateway:routes:#系统模块- id: pmhub-systemuri: lb://pmhub-systempredicates:- Path=/system/**filters:- StripPrefix=0- name: BlackListUrlFilterargs :blacklistUrl:-/user/list
白名单配置
顾名思义,就是允许访问的地址。且无需登录就能访问。比如登录、注册接口,以及其他的不需要网关做鉴权的接口,都可以放在白名单里面。爱他,就把她放进来吧\(^^)/,在ignore 中设置 whites,表示允许匿名访问。
在全局过滤器中添加以下逻辑即可
跳过不需要验证的路径
if(StringUtils.matches(url,ignorewhite.getWhites())){//放行return chain.filter(exchange);
}
#不校验白名单ignore:
whites :-/auth/logout-/auth/login
以上是关于网关的过滤器以及常用功能的介绍,结合实际项目使用,理解这些概念和使用方法并不是什么难事,而且用会还可以写在简历上去和面试官吹通,简直不要太爽,来个 offer 指日可待。
项目实战
接下来会具体讲一讲在 PmHub 中是如何实现全局过滤器以及是如何进行接口调用耗时统计的吧
如何编写全局过滤器
- 步骤-:新建 AuthFilter类
在网关服务 pmhub-gateway 中 filter 下新建 AuthFilter 类,并实现 GlobalFilter, Ordered 接口。
新建全局过滤器
GlobalFilter 就是 gateway 自带的全局过滤器接口,里面也就只有一个方法,所以我们只要实现这个方法就好
GlobalFilter接口明细
Ordered 接口是 spring 框架的确定优先级的接口。
接口中有两个常量HIGHEST PRECEDENCE和LOWEST PRECEDENCE,分别表示最高优先级和最低优先级。接口的主要功能是通过getOrder()方法返回一个整数,这个整数表示当前对象的优先级。在Spring框架中,如果一个对象实现了Ordered接口,那么它会被赋予一个优先级,Spring容器会根据优先级的高低来决定对象的创建和调用顺序。
Ordered接口详情
项目中用到的Order
- 步骤二:实现 filter接口
在过滤器中主要做几个事情
1、白名单过滤
2、token 鉴权
3、设置用户信息到请求
4、接口调用耗时
代码如下:
@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest request = exchange.getRequest();ServerHttpRequest.Builder mutate = request.mutate();String url = request.getURI().getPath();// 跳过不需要验证的路径if (StringUtils.matches(url, ignoreWhite.getWhites())) {return chain.filter(exchange);}String token = getToken(request);if (StringUtils.isEmpty(token)) {return unauthorizedResponse(exchange, "令牌不能为空");}Claims claims = JwtUtils.parseToken(token);if (claims == null) {return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");}String userkey = JwtUtils.getUserKey(claims);boolean islogin = redisService.hasKey(getTokenKey(userkey));if (!islogin) {return unauthorizedResponse(exchange, "登录状态已过期");}String userid = JwtUtils.getUserId(claims);String username = JwtUtils.getUserName(claims);if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) {return unauthorizedResponse(exchange, "令牌验证失败");}// 设置用户信息到请求addHeader(mutate, SecurityConstants.USER_KEY, userkey);addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);// 内部请求来源参数清除(防止网关携带内部请求标识,造成系统安全风险)removeHeader(mutate, SecurityConstants.FROM_SOURCE);//先记录下访问接口的开始时间exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());return chain.filter(exchange.mutate().request(mutate.build()).build());}
补充知识点:
在这段代码中,Mono<Void> 是一个代表异步操作的响应类型,其中 Void 表示没有返回值。Mono 是 Reactor 框架中的一个类,它是一个 Publisher,代表一个或没有元素的序列。在这种情况下,Mono<Void> 表示一个异步操作,它可能完成,也可能失败,但没有具体的返回值。在 filter 方法中,Mono<Void> 用于处理从请求到响应的流程,它允许你在执行过滤逻辑之后决定是否将请求传递给下一个过滤器或执行其他操作。你可以在 Mono 上应用许多操作符,如 then、switchIfEmpty、onErrorResume 等,以构建复杂的异步处理逻辑。例如,如果请求被授权,你可以使用 Mono<Void> 来决定是继续处理请求还是返回一个错误响应。这种设计提供了一种简洁且响应式的方式来处理异步逻辑
步骤三:bootstrap.yml 配置
配置如下:
# Spring
spring:application:#应用名称name:pmhub-gateway
profiles:#环境配置active:dev
这样就实现了 PmHub 网关的自定义过滤器加加权
如何统计接口调用耗时情况
实际上在 pmhub 中统计接口调用耗时情况看实战部分
//先记录下访问接口的开始时间
exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());
在AuthFilter 在先记录下访问接口的开始时间,当然也可以统计出后结合时序库,将接口调用信息保存在时序库中,然后可以自定义展示时间信息。
return chain.filter(exchange).then(Mono.fromRunnable(()->{Long beginVisitTime = exchange.getAttribute(BEGIN_VISIT_TIME);if(beginVisitTime != null){log.info("访问接口主机:+ exchange.getRequest().getURI().getHost());log.info("访问接口端口:+ exchange.getRequest().getURI().getPort());log.info("访问接口URL:+ exchange.getRequest().getURI().getPath());log.info("访问接口URL参数:+ exchange.getRequest().getURI().getRawQuery());log.info("访问接口时长:"+(System.currentTimeMillis()-beginVisitTime)+“ms");1og.info("我是美丽分割线:##########料料料料#######料料料#######料料##################”);System.out.println();}
}));