gateway重构
背景
当前需要把统一鉴权的业务流程提取到网关,做统一的认证,这样各个服务不在使用spring mvc在拦截器中进行处理。虽然以往的项目中基于servlet提取了公共的组件,但每次升级,各个服务都需要升级jar包,而且鉴权类的工作细化到具体服务确实不恰当。
服务流程设计
首先一个请求先到达gateway然后从gateway中获得鉴权结果,最后得出鉴权失败直接响应客户端,或鉴权成功直接转发到具体服务后将响应结果原路返回给客户端。
网关详细设计
网关平台技术组件选择了spring gateway(后面简称gateway),目前根据请求的url信息,去进行gateway predicte path的验证,如果路由可以匹配,则进行filter过滤链中的鉴权处理。
之前版本的鉴权是基于spring mvc的拦截器的,而且在鉴权流程中完全依赖于HttpServletRequest和HttpServletResponse两个组件。所以核心业务流程的代码可复用性不高,而部分的util工具类中有些方法是可以进行服用的,目前阶段可以直接引用老版本的jar包来进行使用;这样能够将部分辅助业务逻辑保留。
本次使用gateway组件,而鉴权阶段可能涉及多个步骤,如:1准备上下文,2 收集LoginType,SiteID信息 3 验证passport等。。。
目前多个步骤可以散落在gateway的不同filter中,也可以自己封装核心鉴权流程业务到同一个filter中,而不同的filter用来划分请求和响应环境的不同功能,比如除了鉴权之外的负载均衡,以及限流等其它业务。
而且目前参考老版本基于拦截器的实现,觉得很有必要将核心业务逻辑分离,本次的研发将不在完全依赖于gateway,以便于后期改用其它基础平台框架,而导致业务代码耦合性增大。
- 分离鉴权业务逻辑以及gateway之间的耦合
首先从老版本的servlt强依赖入手,那么话说回来,gateway中使用netty作为底层的通讯组件,spring-webflux作为web项目的基础,那么gateway中使用ServerWebExchange作为请求响应信息的承载对象,那么在这里我们需要进行封装;避免日后不在使用geteway时发生类似的问题,导致没法和框架的通讯对象解绑。
在这里我们抽象出RequestInfo以及Response对象为约定规范,而对应不同平台的请求对象和响应对象只需要进行一次包装即可,实现接口约定中的方法;这样我们在使用到具体的业务流程类中可以使用我们约定接口的包装对象,这样即使后面更换基础平台,再次遇到基础组件被更换时,由于我们没有强依赖仅仅需要书写对应的wrap即可,不需要大刀阔斧改造核心业务类。
- 自定义gateway的route定义流程
基于gateway默认提供的RouteDefinitionRepository接口,去自定义getRouteDefinitions方法,这样通过发布RefreshRoutesEvent事件即可更新本地路由;gateway会去自主调用getRouteDefinitions方法返回多个RouteDefinitions,然后通过gateway内部提供的RouteDefinitionRouteLocator类中的convert方法转换成Route对象,在这个过程中gateway实际是提供了一种针对于route的map缓存机制,这样我们可以根据需要,定期通过发布事件去刷新缓存获得最新的route信息列表,当然route信息可以来自于redis,mysql等第三方存储介质。
上面的缓存机制源于CachingRouteLocator这个类。该类的定义如下:
而每一次请求都会调用CachingRouteLocator的getRoutes方法,根据请求信息去Map缓存列表中,匹配已经缓存的Route,匹配成功后进行后续处理;
那么上面的@Bean定义部分我们可以看到,这个RouteLocator接口实现,我们是可以自己去设计的,可以按你的方式自定义这个getRoutes的环节。
那么在获取路由列表时也提供了公共的接口规范,这样与实际的route的存储容器松耦合。
- gateway整体的运行流程
目前网关中的核心类如下:
网关的主体流程分几个部分:
- gateway请求上下文载体ServerWebExchange
- 鉴权流程数据传递类ContextBridge
- 登录时的ads请求信息载体LoginContext
在gateway匹配到路由信息以后,请求转到filter中,首先我们收集请求中的基本信息;封装LoginContext中的对象中,将相关的对象在ContextBridge中持有引用。
这三个方法是匹配路由成功以后必须执行的,如果鉴权成功以后,后面的服务是要获取LoginContext的。
除了上面的3个方法以外,后面还会加入siteid以及loginType等信息的获取。
上面提到的都是登录信息的收集,那么实际的鉴权流程核心在AuthenticationConfiguration配置类中,该类会将继承PlatformCheck抽象类,所有bean收集到List中,然后根据接口中约定getOrder方法排序执行,鉴权逻辑,在每一个鉴权的主题中关注当前的业务逻辑,实现不同鉴权阶段的拆分,如果鉴权失败可以直接返回false,并调用方法:
ResponseUtil.buildResponseFinish(responseInfo,InterceptorUtils.ErrorResult(-2002, "pc_passport认证失败,检查cookie信息thor"));
进行gateway直接响应的设置,那么最后鉴权阶段的filter中我们就可以通过承载对象ContextBridge获取到已经设置好的responseInfo,并直接响应给客户端。
Ps:下面的核心流程中解决了一个重要问题,就是在原始的拦截器流程中,存在两种流程:
1 可能直接结束掉流程的代码;
2 一旦进入某个if分支后,一定会结束掉流程的代码;
那么基于当前的场景,我们在统一验证流程的基类中添加3个方法,check方法 skipExecute方法 execute方法;如果存在可能执行结束的逻辑时,将业务的实现写在check方法中,这时skipExcute方法可以设置为true,那么execute方法始终被跳过,此时check逻辑返回为false时则流程结束,check返回为true时流程继续。
如果遇到进入if分支逻辑以后一定会结束掉流程的场景,那么在check方法始终返回true即可,然后if如果成立则skipExecute返回false,证明execute一定会执行并返回最终结果结束掉流程,后面的所有业务实体将不在继续流转。如:下图step3。
目前gateway中passport认证类已经做好,后面有其它的鉴权步骤,可以模仿PassportPlatformCheck实现类继承PlatformCheck抽象类来做,这样暂时不用关注gatewayFilter的其它核心流程。