若依微服务版登录流程涉及到很多模块,本章先从网关讲起
先来看配置中心的网关配置文件ruoyi-gateway-dev.yml,其中有这么一段
# 安全配置
security:
# 验证码
captcha:
enabled: true
type: math
这段配置什么作用呢,就是将CaptchaProperties配置的enable和type初始化,CaptchaProperties内容如下,这两个变量后面会用到,先记下来
package com.zhy.gateway.config.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
/**
* 验证码配置
*
* @author zhy
*/
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "security.captcha")
public class CaptchaProperties
{
/**
* 验证码开关
*/
private Boolean enabled;
/**
* 验证码类型(math 数组计算 char 字符)
*/
private String type;
public Boolean getEnabled()
{
return enabled;
}
public void setEnabled(Boolean enabled)
{
this.enabled = enabled;
}
public String getType()
{
return type;
}
public void setType(String type)
{
this.type = type;
}
}
在ruoyi-ui/src/views/login.vue中会发送验证码请求和获取Cookie,getCodeImg方法会发送路径为"/code"的get请求到后端
在网关处会拦截到该请求,之后会被路由到validateCodeHandler,接下来就是生成验证码,并且将verifyKey和code存到redis中,然后把uuid和验证码图片以Base64格式返回给前端。这部分说起来简单,但是涉及到网关启动和接收请求后的一系列预处理和其他操作,真要搞明白也比较麻烦,我尽量讲的清楚点
网关模块启动
在网关module中,config包下有个配置类叫RouterFunctionConfiguration,用于注册路由配置信息,当前端发起请求后会与注册的路由信息进行匹配
来看一下这个类
package com.ruoyi.gateway.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import com.ruoyi.gateway.handler.ValidateCodeHandler;
/**
* 路由配置信息
*
* @author ruoyi
*/
@Configuration
public class RouterFunctionConfiguration
{
// 注入一个 ValidateCodeHandler的对象,相当于Collectors中注入的service,是用来封装逻辑的
@Autowired
private ValidateCodeHandler validateCodeHandler;
@SuppressWarnings("rawtypes")
@Bean
public RouterFunction routerFunction()
{
/** Spring框架给我们提供了两种http端点暴露方式来隐藏servlet原理:
* 一种是基于注解的形式@Controller或@RestController以及其他的注解如@RequestMapping、@GetMapping等等。
* 另外一种是基于路由配置RouterFunction和HandlerFunction的,称为“函数式WEB”。
* 这行代码会把code请求转发到validateCodeHandler里去处理
*/
return RouterFunctions.route(
RequestPredicates.GET("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
validateCodeHandler);
}
}
debug跟进RouterFunctions.route方法
继续跟进DefaultRouterFunction
可以看到,最终是把数据封装进了predicate和handlerFunction两个变量里
当时看到这里,再往下debug就开始返回了,我以为这条线已经执行完了,就开始看登陆页面加载的后端代码,发现里面有一个routerFunction变量,其中存放的就是predicate和handlerFunction的值,于是我就开始找routerFunction是在哪里进行赋值的,最后发现是在RouterFunctionMapping中的initRouterFunctions方法中进行的操作,而这一步也是上述网关启动流程的后续操作,所以我们从给predicate和handlerFunction赋值开始接着往后看
首先在initRouterFunctions方法中打断点,然后放开debug,就会进来
在进行后续debug之前我们先要搞清楚initRouterFunctions是在哪里调用的,有来龙才能有去脉,从下图中可以看到,该方法是在afterPropertiesSet方法中调用,而要想实现afterPropertiesSet必须实现InitializingBean接口,然后在AbstractAutowireCapableBeanFactory类的invokeInitMethods方法中进行回调。这里涉及到spring的流程,感兴趣的朋友可以看一下我画的spring全体系图解https://blog.csdn.net/qq_41683000/article/details/128241074?spm=1001.2014.3001.5502
言归正传,我们继续看initRouterFunctions方法,第一行调用了routerFunctions方法,该方法返回容器中所有的RouterFunction实例,第二行很有意思,用到了stream流的reduce方法,这个方法类似于归并或者叫累积操作,它会把第一个流元素和第二个流元素做操作然后得到一个结果,再把这个结果和第三个流元素进行操作得到结果,再把新结果和第四个元素进行操作得到结果,以此类推,就像套娃,至于做什么操作则完全取决于括号中的自定义内容。要注意的是,我在debug过程中发现reduce有类似短路的操作,如果只有一个流元素,debug进不去,所以我在RouterFunctionConfiguration中注册了两个路由,这样就有了两个流元素,也就可以进入reduce的自定义操作,也就是RouterFunction的andOther方法中
debug进入andOther方法
可以看到DifferentComposedRouterFunction方法中传了两个参数,个人理解,this就是spring容器加载RouterFunction实例时order值最小的那个实例对象,other即第二个流元素。继续跟进DifferentComposedRouterFunction构造方法,把第一个实例赋给first,把第二个实例赋给second
initRouterFunctions执行完后routerFunction中的数据是这样的
到此routerFunction的初始化执行完成,后面前端获取验证码时会用到这个对象(实际最终会进入DispatcherHandler类获取routerFunction并封装进List类型的handlerMappings,一开始没跟进去,下面踩坑之后会说到)
上面从RouterFunctionConfiguration往后提到的这几个类,都是spring-webflux下的,Spring WebFlux执行流程和核心API介绍:https://blog.csdn.net/wpc2018/article/details/122640470
Spring WebFlux 执行流程图:
从图中可以看到,前端发起请求后,会先进入DispatcherHandler,所以我们进入这个类看看,看到以Handler结尾命名的处理器条件反射地会先看handle方法
首先进入handle方法
handle方法逐行解读
@Override
//ServerWebExchange:存放HTTP请求-响应交互的协定。提供对HTTP请求和响应的访问,还公开其他与服务器端处理相关的属性和功能,如请求属性。
public Mono<Void> handle(ServerWebExchange exchange) {
//handlerMappings:处理映射器,根据请求路径映射到handle
if (this.handlerMappings == null) {
//如果处理映射器为空,创建一个NotFound错误
return createNotFoundError();
}
//通过参数获得请求,如果请求是有效的 CORS 飞行前请求,则返回true
if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {
//通过查找和应用与预期实际请求匹配的 CORS 配置来处理飞行前请求,返回一个Mono
return handlePreFlight(exchange);
}
return Flux.fromIterable(this.handlerMappings)
//返回该请求的handle
.concatMap(mapping -> mapping.getHandler(exchange))
//假如支持多个路径模式,使用第一个而忽略其他的
.next()
.switchIfEmpty(createNotFoundError())
//执行具体的业务方法
.flatMap(handler -> invokeHandler(exchange, handler))
//返回最终处理的结果
.flatMap(result -> handleResult(exchange, result));
}
debug打个断点,前端刷新登录页面,果然进来了
进来之后会发现有个handlerMappings集合,里面有个元素叫RouterFunctionMappings集合,等等,这个集合里的routerFunction不就是之前我们保存的吗,又是什么时候被放到handlerMappings里的呢?带着疑问我往上翻了一下,就在handle方法的上面,有个setApplicationContext,看到这个名字,熟悉spring的人肯定会想到原来是继承了ApplicationContextAware,如此一来spring在执行前置处理器时就会走到这从而对handlerMappings进行赋值
继续执行handle方法,来到这一行,调用getHandler方法,实现类是AbstractHandlerMapping,所以进入AbstractHandlerMapping#getHandler方法,看到是调用了getHandlerInternal方法,继续跟进
这块我也看不太懂,只需要知道这里最终返回的就是该请求的handle,也就是我们一开始在RouterFunctionConfiguration#routerFunction中传入的自定义validateCodeHandler
返回返回再返回,回到DispatcherHandler#handle方法,走到这一行,把handle传进去
继续跟进,invokehandler接收到validateCodeHandler,因为validateCodeHandler是HandleFunction类型,所以会调用HandleFunctionAdapter#handle方法来处理,这里我们继续debug跟进
该方法第一行把validateCodeHandler强转成HandlerFunction,然后执行handlerFunction.handle,这个handle就是我们自定义的validateCodeHandler里的handle方法了,debug进去
终于逃出spring-webflux,回到若依代码中了
到这里简单总结一下,这段业务看似很简单,只是启动项目然后打开登录页面,甚至连验证码都还没获取到,但是在网关模块启动过程中,以及前端刷新页面发送请求的过程中发生了一系列比较复杂的操作,涉及到spring-webflux和spring的一些初始化流程和回调方法,真要搞明白还是要费点功夫。下篇我们继续探讨验证码的获取及登录流程