若依微服务版登录流程源码分析1

362 阅读7分钟

若依微服务版登录流程涉及到很多模块,本章先从网关讲起

验证码

验证码配置

先来看配置中心的网关配置文件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请求到后端

image-20221208162545662.png

image-20221208162559630.png

后端

在网关处会拦截到该请求,之后会被路由到validateCodeHandler,接下来就是生成验证码,并且将verifyKey和code存到redis中,然后把uuid和验证码图片以Base64格式返回给前端。这部分说起来简单,但是涉及到网关启动和接收请求后的一系列handler处理,真要搞明白也比较麻烦,我尽量讲的清楚点

网关模块启动

在网关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方法

image-20221208170004325.png

继续跟进DefaultRouterFunction

image-20221208170104649.png

可以看到,最终是把数据封装进了predicate和handlerFunction两个变量里

image-20221208172032464.png

当时看到这里,再往下debug就开始返回了,我以为这条线已经执行完了,就开始看登陆页面加载的后端代码,发现里面有一个routerFunction变量,其中存放的就是predicate和handlerFunction的值,于是我就开始找routerFunction是在哪里进行赋值的,最后发现是在RouterFunctionMapping中的initRouterFunctions方法中进行的操作,而这一步也是上述网关启动流程的后续操作,所以我们从给predicate和handlerFunction赋值开始接着往后看

首先在initRouterFunctions方法中打断点,然后放开debug,就会进来

在进行后续debug之前我们先要搞清楚initRouterFunctions是在哪里调用的,有来龙才能有去脉,从下图中可以看到,该方法是在afterPropertiesSet方法中调用,而要想实现afterPropertiesSet必须实现InitializingBean接口,然后在AbstractAutowireCapableBeanFactory类的invokeInitMethods方法中进行回调。这里涉及到spring的流程,感兴趣的朋友可以看一下我画的spring全体系图解blog.csdn.net/qq_41683000…

image-20221208173921962.png

image-20221208174830087.png

言归正传,我们继续看initRouterFunctions方法,第一行调用了routerFunctions方法,该方法返回容器中所有的RouterFunction实例,第二行很有意思,用到了stream流的reduce方法,这个方法类似于归并或者叫累积操作,它会把第一个流元素和第二个流元素做操作然后得到一个结果,再把这个结果和第三个流元素进行操作得到结果,再把新结果和第四个元素进行操作得到结果,以此类推,就像套娃,至于做什么操作则完全取决于括号中的自定义内容。要注意的是,我在debug过程中发现reduce有类似短路的操作,如果只有一个流元素,debug进不去,所以我在RouterFunctionConfiguration中注册了两个路由,这样就有了两个流元素,也就可以进入reduce的自定义操作,也就是RouterFunction的andOther方法中

image-20221208180942059.png

image-20221208182808332.png

debug进入andOther方法

image-20221208182846122.png

可以看到DifferentComposedRouterFunction方法中传了两个参数,个人理解,this就是spring容器加载RouterFunction实例时order值最小的那个实例对象,other即第二个流元素。继续跟进DifferentComposedRouterFunction构造方法,把第一个实例赋给first,把第二个实例赋给second

image-20221208183822289.png

initRouterFunctions执行完后routerFunction中的数据是这样的

image-20221208184823870.png

到此routerFunction的初始化执行完成,后面前端获取验证码时会用到这个对象(实际最终会进入DispatcherHandler类获取routerFunction并封装进List类型的handlerMappings,一开始没跟进去,下面踩坑之后会说到)

前端发送获取验证码请求

上面从RouterFunctionConfiguration往后提到的这几个类,都是spring-webflux下的,Spring WebFlux执行流程和核心API介绍:blog.csdn.net/wpc2018/art…

Spring WebFlux 执行流程图:

123.png

从图中可以看到,前端发起请求后,会先进入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进行赋值

image-20221209082732383.png

image-20221209092934558.png

继续执行handle方法,来到这一行,调用getHandler方法,实现类是AbstractHandlerMapping,所以进入AbstractHandlerMapping#getHandler方法,看到是调用了getHandlerInternal方法,继续跟进

image-20221209105157941.png

image-20221209105322727.png

这块我也看不太懂,只需要知道这里最终返回的就是该请求的handle,也就是我们一开始在RouterFunctionConfiguration#routerFunction中传入的自定义validateCodeHandler

image-20221209110249037.png

返回返回再返回,回到DispatcherHandler#handle方法,走到这一行,把handle传进去

image-20221209110452383.png

继续跟进,invokehandler接收到validateCodeHandler,因为validateCodeHandler是HandleFunction类型,所以会调用HandleFunctionAdapter#handle方法来处理,这里我们继续debug跟进

image-20221209110553614.png

该方法第一行把validateCodeHandler强转成HandlerFunction,然后执行handlerFunction.handle,这个handle就是我们自定义的validateCodeHandler里的handle方法了,debug进去

image-20221209111606727.png

终于逃出spring-webflux,回到若依代码中了

image-20221209111740474.png

到这里简单总结一下,这段业务看似很简单,只是启动项目然后打开登录页面,甚至连验证码都还没获取到,但是在网关模块启动过程中,以及前端刷新页面发送请求的过程中发生了一系列比较复杂的操作,涉及到spring-webflux和spring的一些初始化流程和回调方法,真要搞明白还是要费点功夫。下篇我们继续探讨验证码的获取及登录流程