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

news/2024/7/19 11:13:57 标签: java, 源码, 若依, ruoyi, 微服务

接上篇,后端接收到“/code”请求并将其转发至ValidateCodeHandler处理

生成验证码

进入ValidateCodeServiceImpl#createCaptcha

在这里插入图片描述

这块代码比较简单,就不多赘述

java">/**
* 生成验证码
*/
@Override
public AjaxResult createCaptcha() throws IOException, CaptchaException {
	AjaxResult ajax = AjaxResult.success();
	// 获取配置文件中配置的验证码开关
	boolean captchaEnabled = captchaProperties.getEnabled();
	ajax.put("captchaEnabled", captchaEnabled);
	if (!captchaEnabled) {
		return ajax;
	}

	String uuid = IdUtils.simpleUUID();
	// 生成验证key,作为稍后存到redis中的key
	String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;

	String capStr = null, code = null;
	BufferedImage image = null;

	// 获取配置文件中配置的验证码类型
	String captchaType = captchaProperties.getType();
    // 生成验证码
    if ("math".equals(captchaType)) {
   	 	// 生成验证码表达式和对应值
    	String capText = captchaProducerMath.createText();
    	// 截取验证码表达式
   	 	capStr = capText.substring(0, capText.lastIndexOf("@"));
    	// 截取值
    	code = capText.substring(capText.lastIndexOf("@") + 1);
    	// 将表达式转换成图片
    	image = captchaProducerMath.createImage(capStr);
    }
    else if ("char".equals(captchaType)) {
    	capStr = code = captchaProducer.createText();
    	image = captchaProducer.createImage(capStr);
    }

    redisService.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
    // 转换流信息写出
    FastByteArrayOutputStream os = new FastByteArrayOutputStream();
    try
    {
    	ImageIO.write(image, "jpg", os);
    }
    catch (IOException e)
    {
    	return AjaxResult.error(e.getMessage());
    }

    ajax.put("uuid", uuid);
    ajax.put("img", Base64.encode(os.toByteArray()));
    return ajax;
}

发送登录请求

前端handleLogin方法,将用户信息放入cookie,调用login.js的login方法,请求路径为“/auth/login”

在这里插入图片描述

在这里插入图片描述

处理登录请求

客户端向 Spring Cloud Gateway 发出请求。如果Gateway Handler Mapping确定请求与路由匹配,则将其发送到Gateway Web Handler 处理程序。此处理程序通过特定于请求的Fliter链运行请求

在filter包下有几个过滤器链,首先会进入AuthFilter,过滤配置文件中配置的白名单,然后进入ValidateCodeFilter校验验证码

在这里插入图片描述

在这里插入图片描述

进入ValidateCodeServiceImpl#checkCaptcha方法,从redis中取出验证码表达式的值和前端传过来的做比对,逻辑较简单

在这里插入图片描述

验证码校验通过,经过一连串的Filter之后会将请求转发到auth模块下的TokenController#login,开始验证用户信息并创建令牌

在这里插入图片描述

进入login方法,经过几个必要的判断后,第28行开始查询用户信息,这里用feign远程调用了ruoyi-modules-system模块下的SysUserController#info接口,然后将密码错误重试日志入库,并将重试次数存入redis,最后保存登录日志

java">/**
 * 登录
 */
public LoginUser login(String username, String password) throws Exception {
    //前端密码解密
    password = RsaUtils.decryptByPrivateKey(password);
    // 用户名或密码为空 错误
    if (StringUtils.isAnyBlank(username, password))
    {
        recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户/密码必须填写");
        throw new ServiceException("用户/密码必须填写");
    }
    // 密码如果不在指定范围内 错误
    if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
            || password.length() > UserConstants.PASSWORD_MAX_LENGTH)
    {
        recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户密码不在指定范围");
        throw new ServiceException("用户密码不在指定范围");
    }
    // 用户名不在指定范围内 错误
    if (username.length() < UserConstants.USERNAME_MIN_LENGTH
            || username.length() > UserConstants.USERNAME_MAX_LENGTH)
    {
        recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户名不在指定范围");
        throw new ServiceException("用户名不在指定范围");
    }
    // 查询用户信息,feign调用接口
    R<LoginUser> userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER);

    if (StringUtils.isNull(userResult) || StringUtils.isNull(userResult.getData()))
    {
        recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "登录用户不存在");
        throw new ServiceException("登录用户:" + username + " 不存在");
    }

    if (R.FAIL == userResult.getCode())
    {
        throw new ServiceException(userResult.getMsg());
    }

    LoginUser userInfo = userResult.getData();
    SysUser user = userResult.getData().getSysUser();
    if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
    {
        recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "对不起,您的账号已被删除");
        throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
    }
    if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
    {
        recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户已停用,请联系管理员");
        throw new ServiceException("对不起,您的账号:" + username + " 已停用");
    }
    // 登录账户密码错误次数缓存键名
    passwordService.validate(user, password);
    // 记录登录信息
    recordLogService.recordLogininfor(username, Constants.LOGIN_SUCCESS, "登录成功");
    return userInfo;
}

看下查询用户信息的代码

在这里插入图片描述

获取用户权限,admin拥有所有权限,如果是其他角色根据userid查询

在这里插入图片描述

获取用户菜单权限,admin拥有所有权限,其他用户如果有多角色,则要给该用户所属的每个角色设置权限

在这里插入图片描述

login方法执行完后回到TokenController,下一步就是获取登录token,进入具体方法ruoyi-common-security模块下的TokenService#createToken

该方法主要操作:生成随机uuid作为token、刷新令牌有效期、jwt对数据进行加密并返回

在这里插入图片描述

其他代码一目了然,只需要再看下refreshToken方法,将 token 和用户的角色权限信息存储到 redis

在这里插入图片描述

登录功能到这里就结束了,总的来说做了两件事,一是验证用户信息,二是创建token并返回给前端,当前端再发起任意请求时都会携带token到后端,后端将token转化为userId、userName存储到请求头中;根据 token 查询redis缓存中的权限并和目标资源上标注的权限名称做比对,比对成功即鉴权成功。后面这部分我们以若依中获取用户信息为例来走一遍流程

首先还是来到AuthFIlter

java">@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
    ServerHttpRequest request = exchange.getRequest();
    ServerHttpRequest.Builder mutate = request.mutate();

    String url = request.getURI().getPath();
    // 不需要验证的路径直接执行过滤器链,白名单中配置,如login
    if (StringUtils.matches(url, ignoreWhite.getWhites()))
    {
        return chain.filter(exchange);
    }
    // 从请求头中获取token
    String token = getToken(request);
    if (StringUtils.isEmpty(token))
    {
        return unauthorizedResponse(exchange, "令牌不能为空");
    }
    // 从令牌中获取数据声明
    Claims claims = JwtUtils.parseToken(token);
    if (claims == null)
    {
        return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
    }
    // 从令牌中获取用户标识,userkey即为登录时作为token的uuid
    String userkey = JwtUtils.getUserKey(claims);
    // 判断令牌是否过期,getTokenKey方法内拼接login_tokens:uuid,这就是登录时存到redis中的key,value为用户信息
    boolean islogin = redisService.hasKey(getTokenKey(userkey));
    if (!islogin)
    {
        return unauthorizedResponse(exchange, "登录状态已过期");
    }
    // 从数据声明中获取用户信息
    String userid = JwtUtils.getUserId(claims);
    String username = JwtUtils.getUserName(claims);
    if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username))
    {
        return unauthorizedResponse(exchange, "令牌验证失败");
    }

    // 设置用户信息到请求,后面拦截器会从请求header中获取token,并根据token去redis中获取user保存到SecurityContextHolder,
    // 也因此我们可以在其他地方用SecurityUtils.getLoginUser()直接获取用户信息
    addHeader(mutate, SecurityConstants.USER_KEY, userkey);
    addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
    addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
    // 内部请求来源参数清除
    removeHeader(mutate, SecurityConstants.FROM_SOURCE);
    return chain.filter(exchange.mutate().request(mutate.build()).build());
}

执行完过滤器之后会进入WebMvcConfig,该类实现了WebMvcConfigurer,WebMvcConfigurer是一个mvc的配置类,我们可以在里面进行自定义拦截器、视图解析器、静态资源处理等操作,附上sprngmvc流程图

在这里插入图片描述

在这里插入图片描述

进入HeaderInterceptor,该类顶级接口是HandlerInterceptor,它是SpringWebMVC的处理器拦截器,类似于Servlet开发中的过滤器Filter,用于处理器进行预处理和后处理

在preHandle方法里从请求头中获取userid,username和userkey保存到SecurityContextHolder,然后获取token,并根据token去redis中获取user,验证用户有效期,最后将完整user对象保存到SecurityContextHolder,SecurityContextHolder中的方法都是静态方法,所以我们后面可以全局获取用户信息

在这里插入图片描述

拦截器执行完之后,接下来就进入controller执行具体的业务逻辑了,到此若依的登录流程源码分析完毕


http://www.niftyadmin.cn/n/16378.html

相关文章

IMX6ULL学习笔记(13)——GPIO接口使用【汇编方式】

一、GPIO简介 i.MX6ULL 芯片的 GPIO 被分成 5 组,并且每组 GPIO 的数量不尽相同&#xff0c;例如 GPIO1 拥有 32 个引脚&#xff0c; GPIO2 拥有 22 个引脚&#xff0c; 其他 GPIO 分组的数量以及每个 GPIO 的功能请参考 《i.MX 6UltraLite Applications Processor Reference M…

表格解析 概览

表格解析发展至今&#xff0c;仍然是一项很年轻的研究领域&#xff0c;出现了大量解决方案&#xff0c;常用的技术包括&#xff1a;线检测、box检测、分割、多模态融合、GCN、img2seq。以下按我的理解梳理一下表格解析各个流派&#xff0c;从中了解这项任务背后所采用的技术。在…

二叉堆(优先队列)与堆排序的完整实现(C++)

tags: DSA C Python 写在前面 记录一下二叉堆和堆排序, 堆(二叉堆)作为一种基本数据结构, 常在lc周赛三题位置出现, 遇到了我只能干着急, 必须好好学一下了. 参考算法导论(第三版). 这里说的堆指的是数据结构(抽象概念), 而不是程序执行时候的堆区(内存实体) 二叉堆介绍 二叉…

0123 双指针 Day12

剑指 Offer 25. 合并两个排序的链表 输入两个递增排序的链表&#xff0c;合并这两个链表并使新链表中的节点仍然是递增排序的。 示例1&#xff1a; 输入&#xff1a;1->2->4, 1->3->4 输出&#xff1a;1->1->2->3->4->4 /*** Definition for si…

mybatis以及mybatisplus批量插入问题

1. 思路分析&#xff1a; 批量插入是我们日常开放经常会使用到的场景&#xff0c;一般情况下我们也会有两种方案进行实施&#xff0c;如下所示。 方案一 就是用 for 循环循环插入&#xff1a; 优点&#xff1a;JDBC 中的 PreparedStatement 有预编译功能&#xff0c;预编译之…

组件显示,路由切换-过渡动画

1.安装过渡动画模块npm install react-transition-group --save; 2.在需要执行过渡的组件中导入动画模块; import{ CSSTransition } from "react-transition-group" 3.在需要过渡的标签外层添加CSSTranstion组件 <CSSTransition in{bool} timeout{2000} className…

【JavaWeb开发-Servlet】拾起海中的漂流瓶超强版

目录 原版&#xff1a; 一、思路&#xff1a; 二、实现&#xff1a; 三、资源分享 四、部署服务器时记得修改文件路径 原版&#xff1a; 【JavaWeb开发-Servlet】拾起海中的漂流瓶增强版_代码骑士的博客-CSDN博客【代码】【JavaWeb开发-Servlet】拾起海中的漂流瓶增强版…

微信小程序|反编译

一、下载网易模拟器 MuMu模拟器官网_安卓模拟器_网易手游模拟器 根据自己的系统选择对应的软件进行安装。 安装成功后,如下: 二、再模拟器上面安装对应的软件(微信、RE文件管理器) 1. 打开应用中心,搜索 RE文件管理器和微信,分别进行下载 2. 打开微信,输入帐号进行…