网关的作用
- 鉴权, 判断当前用户是否有权限访问当前接口
- 认证, 判断token是否有效, 并转换为jwt token传递到后端服务
- 动态路由, 限流, 分流, 调用统计等
代码调用过程阅读方法: 如何以纯文本方式简单快速记录java代码的调用过程
filter 过滤器
HelperFilter
hzero实现了一套过滤链系统, 可以自定义鉴权逻辑, 默认实现在:
hzero-gateway-helper-default.jar: org.hzero.gateway.helper.filter
理解gateway的关键是掌握它的一系列filter, filter的调用先后顺序:
//HelperChain#doFilter
0. PermissionDisableOrSkipFilter 是否启用权限校验和跳过权限校验路径的过滤器
1. WhiteListFilter 白名单过滤器
2. BlackListFilter 黑名单过滤
3. GetRequestRouteFilter 根据请求前缀获取对应的zuul路由
4. GetPermissionFilter 根据接口uri,method和service获取匹配到的权限
5. CollectSpanFilter 接口调用统计
6. PublicRequestFilter 公共接口的权限校验
7. GetUserDetailsFilter 根据access_token获取对应的userDetails
8. AddJwtFilter 给返回添加JWT token
9. LoginAccessRequestFilter loginAccess请求的权限校验
10. AdminUserPermissionFilter 超级管理员的权限校验
11. MenuPermissionFilter 校验菜单是否分配了API
12. AdminRolePermissionFilter 超级角色的权限校验,超级角色不校验角色-API关系
13. CommonRequestCheckFilter 普通接口(除公共接口,loginAccess接口,内部接口以外的接口)普通用户(超级管理员之外用户)的权限校验
理解:
- 校验权限的是最后一个filter:
CommonRequestCheckFilter
, 检查权限时没有限定当前角色, 任一角色有权限就能访问;- 即使检查角色, 也是检查某层级的角色集合, 而不是当前某一个角色;
hzero.gateway.helper.filter.common-request.checkCurrentRole
默认值为false- 所以角色合并在网关这里是没什么用处的, 相当于一直是合并的;
- 如果拥有超级管理员角色, 会跳过后面的权限校验:
AdminRolePermissionFilter
、AdminUserPermissionFilter
- 只要拥有平台级超级管理员角色(和当前角色无关), 就会被认为是
isSiteSuperRole
, 就不能调用租户级api
调用链图示
token获取
token可以从header的authorization或者queryParams的access_token里传递:
GateWayHelperFilter#filter
responseContext = authenticationHelper.authentication(exchange);
DefaultReactiveAuthenticationHelper#authentication
DefaultReactiveAuthenticationHelper#parse
api调用统计
配置:
hzero.gateway.helper.filter.collect-span.enabled: true
代码位置:
...gateway.helper.filter.CollectSpanFilter
存储结构:
redis db4:
gateway:span:{日期}
:某日期下所有服务的调用总数;
gateway:span:{日期}:{服务}
: 某日期某服务下各个接口的调用次数, 求和等于上面的总数;
比如:
gateway:span:2021-02-05:hzero-admin
rate limit 限流
基于spring cloud gateway的限流
hzero的限流基于spring cloud gateway的RequestRateLimiter
:
Spring Cloud Gateway
如果超过限流, 返回HTTP 429 - Too Many Requests
, response的添加了一些header: X-RateLimit-Remaining、X-RateLimit-Replenish-Rate等;
限流的FilterFactory初始化:
GatewayAutoConfiguration#requestRateLimiterGatewayFilterFactory
@Bean
@ConditionalOnBean({RateLimiter.class, KeyResolver.class})
public RequestRateLimiterGatewayFilterFactory requestRateLimiterGatewayFilterFactory(RateLimiter rateLimiter, PrincipalNameKeyResolver resolver) {
return new RequestRateLimiterGatewayFilterFactory(rateLimiter, resolver);
}
spring cloud gateway限流的核心是:
RequestRateLimiterGatewayFilterFactory#apply
return resolver.resolve(exchange).flatMap(key ->
limiter.isAllowed(route.getId(), key).flatMap(response -> {
其中的关键是resolver
和limiter
, resolver
从请求里提取一个key, 比如用户名、角色名、url, limiter
根据key决定能否访问;
hzero对resolver
和limiter
做了扩展:
KeyResolver
实现的子类:
CombinedKeyResolver
OriginKeyResolver
RoleKeyResolver
TenantKeyResolver
UrlKeyResolver
UserKeyResolver
可以对用户名、角色名、租户id、url、或者复合信息做限流;
key对应的请求限流多少, 保存在 gateway.ratelimit.limiter.EnhancedRedisRateLimiter.Config#replenishRateMap
, 这个数据序列化为json, 保存在数据库路由配置的extend_config_map
里:
hzero的org.hzero.gateway.ratelimit.limiter.EnhancedRedisRateLimiter
类是从spring cloud gateway里复制粘贴出来的;
限流动态配置的实现
spring cloud gateway的限流是在application.yml里进行配置, hzero里application.yml里关于RequestRateLimiter
的配置全部注释了, 配置来自于数据库hadm_service_route
;
通过动态修改gateway的路由, 添加RequestRateLimiter
filter;
刷新限流接口:
hadm/v1/gateway-rate-limits/config/refresh
GatewayRateLimitSiteController#refresh
GatewayRateLimitServiceImpl#refresh
//step1:匹配路由,将限流配置合并到路由的额外配置(extend_config_map)中
//step2:通知更新服务
配置示例:
{"filters":
[ "{\"name\":\"RequestRateLimiter\",
\"args\":{\"rate-limiter\":\"#{@enhancedRedisRateLimiter}\",
\"key-resolver\":\"#{new org.hzero.gateway.ratelimit.dimension.CombinedKeyResolver(new org.hzero.gateway.ratelimit.dimension.UrlKeyResolver(\\\"{1}\\\"))}\",
\"redis-rate-limiter.replenishRate\":\"1\",
\"redis-rate-limiter.replenishRateMap.\\\"url.hfle.v1.files.summary0._size-{1}\\\"\":\"1\"}}"
]}
数据库模型
- hadm_gw_rate_limit 网关限流设置, 限流方式
- hadm_gw_rate_limit_line 网关限流设置行明细, 限流路由
- hadm_gw_rate_limit_dim 网关限流设置行明细, 限流规则
- 关系(根据界面上的名称)
- 限流方式 - 限流路由: 1-n
- 限流路由 - 限流规则: 1-n
UrlKeyResolver详解
//org.hzero.gateway.ratelimit.dimension.UrlKeyResolver
//
//构造函数入参的url部分, 问号?前的部分
private String interestUrl;
//
//构造函数入参的参数部分, queryparm的名称
private String[] interestParamKeys;
//
//interestUrl把{i}替换为\\S+, 并且Pattern.compile, 所以只匹配path, 不匹配参数
private Pattern interestUrlPattern;
//入参格式: /v1/{1}/invoke?namespace={2}&serverCode={3}&interfaceCode={4}
//`{i}`表示变量, 从1开始;
public UrlKeyResolver(String interestUrlTemplate) {}
url替换:
return PREFIX + urlKey
.replaceAll("\\?", "._")
.replaceAll("/", ".")
.replaceAll("&", "_")
.replaceAll("=", "-");
示例:
/demo/test?name={1}
url.demo.test._name-{1}
灰度发布
hzero-starter-dynamic-route并未开源, 结合网上的文章自行实现;
资料
Spring Cloud Gateway 扩展支持多版本控制及灰度发布
实现原理
- 核心思想是通过修改
Ribbon
选择节点的方法,因为不论是网关层、还是 feign 调用、还是使用LoadBalanced
标注的RestTemplate
,最终都会通过 Ribbon 来从注册中心选择一个节点进行服务请求。 - 自定义
CustomMetadataRule
,继承自ZoneAvoidanceRule
,覆盖choose
方法。在choose
方法中首先拉取所有服务节点,然后根据当前规则所匹配的节点组ID来获取匹配的节点组。
ribbon代码
ribbon自动配置:
RibbonAutoConfiguration#loadBalancerClient
return new RibbonLoadBalancerClient(springClientFactory());
RibbonLoadBalancerClient#choose
gateway的实例选择过程:
LoadBalancerClientFilter#filter
final ServiceInstance instance = choose(exchange);
LoadBalancerClientFilter#choose
return loadBalancer.choose(((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost());
RibbonLoadBalancerClient#choose
RibbonLoadBalancerClient#getServer(java.lang.String)
RibbonLoadBalancerClient#getServer(com.netflix.loadbalancer.ILoadBalancer)
ZoneAwareLoadBalancer#chooseServer
return super.chooseServer(key);
BaseLoadBalancer#chooseServer
return rule.choose(key); //默认rule是ZoneAvoidanceRule
CustomMetadataRule#choose //自定义的rule
启用CustomMetadataRule
的配置:
@RibbonClients(
defaultConfiguration = {CustomMetadataRule.class}
)
...route.RibbonAutoConfiguration
ribbonRule bean的默认配置, 默认是ZoneAvoidanceRule
org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration#ribbonRule
@ConditionalOnMissingBean
public IRule ribbonRule(IClientConfig config) {
ZoneAvoidanceRule rule = new ZoneAvoidanceRule();