接上篇,后端接收到“/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执行具体的业务逻辑了,到此若依的登录流程源码分析完毕