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

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

验证码

验证码配置

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

若依微服务版登录流程源码分析1_第1张图片

若依微服务版登录流程源码分析1_第2张图片

后端

在网关处会拦截到该请求,之后会被路由到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方法

若依微服务版登录流程源码分析1_第3张图片

继续跟进DefaultRouterFunction

若依微服务版登录流程源码分析1_第4张图片

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

若依微服务版登录流程源码分析1_第5张图片

当时看到这里,再往下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

若依微服务版登录流程源码分析1_第6张图片

若依微服务版登录流程源码分析1_第7张图片

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

在这里插入图片描述

若依微服务版登录流程源码分析1_第8张图片

debug进入andOther方法

在这里插入图片描述

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

若依微服务版登录流程源码分析1_第9张图片

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

在这里插入图片描述

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

前端发送获取验证码请求

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

Spring WebFlux 执行流程图:

若依微服务版登录流程源码分析1_第10张图片

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

若依微服务版登录流程源码分析1_第11张图片

若依微服务版登录流程源码分析1_第12张图片

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

若依微服务版登录流程源码分析1_第13张图片

若依微服务版登录流程源码分析1_第14张图片

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

若依微服务版登录流程源码分析1_第15张图片

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

若依微服务版登录流程源码分析1_第16张图片

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

若依微服务版登录流程源码分析1_第17张图片

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

若依微服务版登录流程源码分析1_第18张图片

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

若依微服务版登录流程源码分析1_第19张图片

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

你可能感兴趣的:(Java,微服务,java,spring,boot,若依)