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

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

生成验证码

进入ValidateCodeServiceImpl#createCaptcha

在这里插入图片描述

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

/**
* 生成验证码
*/
@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,最后保存登录日志

/**
 * 登录
 */
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

@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执行具体的业务逻辑了,在controlle的方法上如果添加了@RequiresLogin、@RequiresPermissions、@RequiresRoles等注解则会分别进行登录认证、权限认证和角色认证,具体认证逻辑是在切面类中执行,详细过程可以参考我之前写的若依权限校验源码分析。到此若依的登录流程源码分析完毕

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
若依微服务验证码请求超时可能是由于以下几个原因引起的: 1. 网络连接问题:首先,需要确保你的网络连接正常,可以尝试重新连接网络或更换网络环境,然后再次发送验证码请求。 2. 服务端响应延迟:若依微服务可能存在服务端响应延迟的情况。你可以尝试等待一段时间后再次发送验证码请求,或者联系若依微服务的开发人员咨询。 3. 服务器负载过高:如果服务器负载过高,可能会导致验证码请求超时。你可以尝试在非高峰时段再次发送验证码请求。 4. 配置错误:请确保你在发送验证码请求时使用的URL和参数是正确的。你可以检查你的配置文件,确认URL和参数的正确性。 如果以上方法都没有解决问题,建议联系若依微服务的技术支持团队寻求进一步的帮助和解决方案。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [若依cloud微服务本--postman请求,token获取及过期时间设置](https://blog.csdn.net/gonglouxue/article/details/107938446)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [RuoYi(若依) 微服务分离 启动及常见问题总结](https://blog.csdn.net/qq_37203082/article/details/116115918)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [若依源码分析(二)-登录流程源码分析](https://blog.csdn.net/liupantao/article/details/128458876)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

紫荆之后-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值