【RuoYi-Vue-Plus】登陆逻辑的实现

RuoYi-Vue-Plus登陆逻辑的实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oHTcV20M-1667466675034)(RuoYi-Vue-Plus登陆.assets/image-20221103100910211.png)]

分析

RuoYi-Vue-Plus采用了 sa-token 框架,可以先了解一下这个框架,不过不了解也没有关系,后面会一步一步分析源码。

一、 找到请求 /login
/**
 * 登录方法
 *
 * @param loginBody 登录信息
 * @return 结果
 */
@Anonymous
@PostMapping("/login")
public R<Map<String, Object>> login(@Validated @RequestBody LoginBody loginBody) {
    Map<String, Object> ajax = new HashMap<>();
    // 生成令牌
    String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                                      loginBody.getUuid());
    // TOKEN = "token";
    ajax.put(Constants.TOKEN, token);
    return R.ok(ajax);
}

这个方法上面有注解@Anonymous,这个注解是若依框架定义的,这里就不介绍了,你只需要知道,方法上加了这个注解代表这个方法不需要认证就可以访问,类上加了这个注解,代表这个类里面所有的方法都不需要登陆就可以访问。

二、 进入loginService.login()方法
/**
 * 登录验证
 *
 * @param username 用户名
 * @param password 密码
 * @param code     验证码
 * @param uuid     唯一标识
 * @return 结果
 */
public String login(String username, String password, String code, String uuid) {
    // 获取到 request 对象
    HttpServletRequest request = ServletUtils.getRequest(); 
    // 验证码的开关是否开启
    boolean captchaEnabled = configService.selectCaptchaEnabled();
    // 验证码开关
    if (captchaEnabled) { // 开启
         // 校验 验证码
        validateCaptcha(username, code, uuid, request);
    }
    // 根据用户名获取用户信息;
    SysUser user = loadUserByUsername(username);
    // BCrypt.checkpw(password, user.getPassword():用户输入的密码和数据库查询的密码一致时,返回true
    checkLogin(LoginType.PASSWORD, username, () -> !BCrypt.checkpw(password, user.getPassword()));
    // 此处可根据登录用户的数据不同 自行创建 loginUser
    LoginUser loginUser = buildLoginUser(user);
    // 生成token
    LoginHelper.loginByDevice(loginUser, DeviceType.PC);

    // 记录登录信息日志
    asyncService.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"), request);
    // 更新用户登录信息
    recordLoginInfo(user.getUserId(), username);
    // 获取用户 Token 值
    return StpUtil.getTokenValue(); // 获取当前会话的token值
}

下面一个方法一个方法分析loginService.login()里面的代码:

2.1、 configService.selectCaptchaEnabled()
/**
 * 获取验证码开关
 *		查询数据库,得到的是 true 或者 false
 * @return true开启,false关闭
  */
@Override
public boolean selectCaptchaEnabled() {
    // 根据 configKey 查询数据库表 sys_config 获取 configValue -> true或者 false
    String captchaEnabled = selectConfigByKey("sys.account.captchaEnabled");
    // 如果是空的话,意思就是默认开启验证码开关
    if (StringUtils.isEmpty(captchaEnabled)) {
        return true;
    }
    // 调用 hutool 工具包的方法 转换为boolean
    return Convert.toBool(captchaEnabled);
}

2.2、validateCaptcha(username, code, uuid, request);
/**
  * 校验验证码
  *
 * @param username 用户名
 * @param code     验证码
 * @param uuid     唯一标识
 */
public void validateCaptcha(String username, String code, String uuid, HttpServletRequest request) {
    // CAPTCHA_CODE_KEY = "captcha_codes:";
    String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.defaultString(uuid, "");
    // 从 Redis 中 根据 key 获取 value
    String captcha = RedisUtils.getCacheObject(verifyKey);
    // 从 Redis 中删除该 key
    RedisUtils.deleteObject(verifyKey);
    // 如果 captcha 为空,说明验证码过期了
    if (captcha == null) {
        asyncService.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"), request);
        throw new CaptchaExpireException();
    }
    // captcha 和 用户输入的 code 不相等,说明验证码输入错误
    if (!code.equalsIgnoreCase(captcha)) {
        asyncService.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"), request);
        throw new CaptchaException();
    }
}

插曲:上面代码有一个异步调用记录日志asyncService.recordLogininfor()

@Async
@Override
public void recordLogininfor(final String username, final String status, final String message,
                             HttpServletRequest request, final Object... args) {
}

这个方法上@Async注解:

	注解@Async是spring为了方便开发人员进行异步调用的出现的,在方法上加入这个注解,spring会从线程池中获取一个新的线程来执行方法,实现异步调用

	注解@EnableAsync表示开启对异步任务的支持,可以放在springboot的启动类上,也可以放在自定义线程池的配置类上,具体看下文:
    https://www.cnblogs.com/fzhblog/p/14012401.html

    AsyncConfigurerSupport 只定义了两个方法分别用于自定义异步线程池、异步产生的异常捕获,通过实现此类即可实现自定义的异步线程池。


    如果想要@Async默认调用自定义的线程池,关于@Async的默认调用规则,会优先查询实现了AsyncConfigurer这个接口的类或者继承AsyncConfigurerSupport的类,
     所以可以按照如下代码中自定义线程池。新建一个线程池配置类,@EnableAsync在配置类上加,不用在启动类上加也行,用来开启对异步任务的支持。
    可以配置不同的线程池,如果配置了多个线程池,可以用@Async(“name”),那么表示线程池的@Bean的name,来指定用哪个线程池处理,假如只配置了一个线程池,直接用@Async就会用自定义的线程池执行。假如配置了多个线程池,用@Async没指定用哪个线程池,会用默认的SimpleAsyncTaskExecutor来处理。

     参考博客:https://blog.csdn.net/qq_43366581/article/details/125390458?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0-125390458-blog-126278878.pc_relevant_aa&spm=1001.2101.3001.4242.1&utm_relevant_index=3
2.3、loadUserByUsername(username);
private SysUser loadUserByUsername(String username) {
     // 通过用户名查询用户
    SysUser user = userService.selectUserByUserName(username);
    if (ObjectUtil.isNull(user)) {
        log.info("登录用户:{} 不存在.", username);
        throw new UserException("user.not.exists", username);
    } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
        log.info("登录用户:{} 已被删除.", username);
        throw new UserException("user.password.delete", username);
    } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
        log.info("登录用户:{} 已被停用.", username);
        throw new UserException("user.blocked", username);
    }
    return user;
}

作用:查询数据库 查询到 当前用户信息。

2.4、checkLogin()方法
/**
 * 登录校验
 * 如果密码错误,会记录错误次数,如果达到指定次数(默认5次)则账号会被锁定一段时间(默认10分钟)。
 */
private void checkLogin(LoginType loginType, String username, Supplier<Boolean> supplier) {
    HttpServletRequest request = ServletUtils.getRequest();
    // 登录账户密码错误次数 redis key:PWD_ERR_CNT_KEY = "pwd_err_cnt:";
    String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
    // 登录失败:LOGIN_FAIL = "Error";
    String loginFail = Constants.LOGIN_FAIL;

    // 获取用户登录错误次数(可自定义限制策略 例如: key + username + ip)
    Integer errorNumber = RedisUtils.getCacheObject(errorKey);
    // 锁定时间内登录 则踢出
    if (ObjectUtil.isNotNull(errorNumber) && errorNumber.equals(maxRetryCount)) {
        // 如果错误次数不为空,并且等于 yml配置的 密码最大错误次数:maxRetryCount: 5 ,提示:登录重试超出限制提示
        asyncService.recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime), request);
        throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
    }

    if (supplier.get()) {
        // 是否第一次
        errorNumber = ObjectUtil.isNull(errorNumber) ? 1 : errorNumber + 1;
        // 达到规定错误次数 则锁定登录
        if (errorNumber.equals(maxRetryCount)) {
            RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
            asyncService.recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime), request);
            throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
        } else {
            // 未达到规定错误次数 则递增
            // 重新设置 redis 的密码错误次数
            RedisUtils.setCacheObject(errorKey, errorNumber);
            // 记录日志
            asyncService.recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber), request);
            throw new UserException(loginType.getRetryLimitCount(), errorNumber);
        }
    }

    // 登录成功 清空错误次数
    RedisUtils.deleteObject(errorKey);
}

这个方法主要是登陆校验方法,会记录错误次数,如果达到指定次数(默认5次)则账号会被锁定一段时间(默认10分钟)。

2.5、buildLoginUser(user)方法
/**
 * 构建登录用户
 */
private LoginUser buildLoginUser(SysUser user) {
    LoginUser loginUser = new LoginUser();
    loginUser.setUserId(user.getUserId());
    loginUser.setDeptId(user.getDeptId());
    loginUser.setUsername(user.getUserName());
    loginUser.setUserType(user.getUserType());
    loginUser.setMenuPermission(permissionService.getMenuPermission(user));
    loginUser.setRolePermission(permissionService.getRolePermission(user));
    loginUser.setDeptName(ObjectUtil.isNull(user.getDept()) ? "" : user.getDept().getDeptName());
    List<RoleDTO> roles = BeanUtil.copyToList(user.getRoles(), RoleDTO.class);
    loginUser.setRoles(roles);
    return loginUser;
}

作用:将 SysUser 对象 转换为 LoginUser 对象

2.6、LoginHelper.loginByDevice(loginUser, DeviceType.PC);

这个是重头戏利用Sa-token主要就是在这个里面,在这个里面实现了登陆。

/**
 * 登录系统 基于 设备类型
 * 针对相同用户体系不同设备
 *      SaStorage - 请求作用域:在 SaStorage 中存储的数据只在一次请求范围内有效,请求结束后数据自动清除。使用 SaStorage 时无需处于登录状态。
 * @param loginUser 登录用户信息
 */
public static void loginByDevice(LoginUser loginUser, DeviceType deviceType) {
    // SaHolder是sa-Token的上下文持有类 其中的getStorage()实现了返回当前请求存储器的对象(底层容器操作Bean实现)
    // SaHolder.getStorage():获取当前请求的 [Storage] 对象
    // 将用户信息存入 request 对象中,方便后面信息共享
    // SaHolder.getStorage().get("loginUser"):可以获取到 loginUser 信息
    // LOGIN_USER_KEY = "loginUser";
    SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
    // 调用 Sa-Token 登录方法
    StpUtil.login(loginUser.getLoginId(), deviceType.getDevice());
    // 设置用户数据多级缓存
    setLoginUser(loginUser);
}

SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);

将用户信息存入 request 对象中,方便后面信息共享

后序可以根据 SaHolder.getStorage().get(“loginUser”):可以获取到 loginUser 信息

StpUtil.login(loginUser.getLoginId(), deviceType.getDevice());:调用了 sa-token 框架的登陆方法

进入代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存失败,源站可能有防盗链机制,建议将图片保存下来直接上传下上传(ic28bZSK3LCi-1667466675035)(RuoYi-Vue-Plus登陆.assets/image-20221103104008836.png)(
RuoYi-Vue-Plus登陆.assets/image-20221103104008836.png)]

继续进入:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MO0k5pRm-1667466675035)(RuoYi-Vue-Plus登陆.assets/image-20221103104125162.png)]

继续进入:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kbau3pQG-1667466675036)(RuoYi-Vue-Plus登陆.assets/image-20221103104156096.png)]

终于来到这个方法了,sa-token源码已经写了注释了,这两个方法很关键。我们先进入第一个方法。

2.6.1、创建会话
/**
 * 创建指定账号id的登录会话
 * @param id 登录id,建议的类型:(long | int | String)
 * @param loginModel 此次登录的参数Model 
 * @return 返回会话令牌 
 */
public String createLoginSession(Object id, SaLoginModel loginModel) {

    SaTokenException.throwByNull(id, "账号id不能为空");

    // ------ 0、前置检查:如果此账号已被封禁. 
    if(isDisable(id)) {
        throw new DisableLoginException(loginType, id, getDisableTime(id));
    }

    // ------ 1、初始化 loginModel 
    SaTokenConfig config = getConfig();
    loginModel.build(config);

    // ------ 2、生成一个token  
    String tokenValue = null;
    // --- 如果允许并发登录 
    if(config.getIsConcurrent()) {
        // 如果配置为共享token, 则尝试从Session签名记录里取出token 
        if(getConfigOfIsShare()) {
            // 为确保 jwt-simple 模式的 token Extra 数据生成不受旧token影响,这里必须确保 is-share 配置项在 ExtraData 为空时才可以生效 
            if(loginModel.getExtraData() == null || loginModel.getExtraData().size() == 0) {
                tokenValue = getTokenValueByLoginId(id, loginModel.getDeviceOrDefault());
            }
        } else {
            // 
        }
    } else {
        // --- 如果不允许并发登录,则将这个账号的历史登录标记为:被顶下线 
        replaced(id, loginModel.getDevice());
    }
    // 如果至此,仍未成功创建tokenValue, 则开始生成一个 
    if(tokenValue == null) {
        if(SaFoxUtil.isEmpty(loginModel.getToken())) {
            tokenValue = createTokenValue(id, loginModel.getDeviceOrDefault(), loginModel.getTimeout(), loginModel.getExtraData());
        } else {
            tokenValue = loginModel.getToken();
        }
    }

    // ------ 3. 获取 User-Session , 续期 
    SaSession session = getSessionByLoginId(id, true);
    session.updateMinTimeout(loginModel.getTimeout());

    // 在 User-Session 上记录token签名 
    session.addTokenSign(tokenValue, loginModel.getDeviceOrDefault());

    // ------ 4. 持久化其它数据 
    // token -> id 映射关系  
    saveTokenToIdMapping(tokenValue, id, loginModel.getTimeout());

    // 写入 [token-last-activity] 
    setLastActivityToNow(tokenValue); 

    // $$ 通知监听器,账号xxx 登录成功 
    SaManager.getSaTokenListener().doLogin(loginType, id, tokenValue, loginModel);

    // 检查此账号会话数量是否超出最大值 
    if(config.getMaxLoginCount() != -1) {
        logoutByMaxLoginCount(id, session, null, config.getMaxLoginCount());
    }

    // 返回Token 
    return tokenValue;
}

下面我们就一个方法一个方法分析这段代码:

0、前置检查:如果此账号已被封禁.

进入这个方法里面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A3JrQQkF-1667466675036)(RuoYi-Vue-Plus登陆.assets/image-20221103105902755.png)]

  • getSaTokenDao()方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aec81hjl-1667466675036)(RuoYi-Vue-Plus登陆.assets/image-20221103110054064.png)]

因为 类 PlusSaTokenDao实现了 SaTokenDao 接口

SaTokenDao 是数据持久层接口,负责所有会话数据的底层写入和读取。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ql14tGGl-1667466675037)(RuoYi-Vue-Plus登陆.assets/image-20221103110014484.png)]

Sa-Token文档:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8oChOF5C-1667466675037)(2 RuoYi-Vue-Plus登陆.assets/image-20221103160726046.png)]

  • splicingKeyDisable(loginId) 方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LocsI1Cm-1667466675037)(RuoYi-Vue-Plus登陆.assets/image-20221103110245287.png)]

getConfig()就是读取到配置文件application.yml里面的值

  • getSaTokenDao().get(splicingKeyDisable(loginId)) != null;从Redis里面查询这个用户是不是被封禁的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5XbIFcKO-1667466675037)(RuoYi-Vue-Plus登陆.assets/image-20221103110724267.png)]

初始化 loginModel

  • getConfig();

    读取application里面的东西封装成 SaTokenConfig对象 SaTokenConfig类型

  • build();

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iiU31AJu-1667466675038)(RuoYi-Vue-Plus登陆.assets/image-20221103111049805.png)]

生成一个token

源码

// ------ 2、生成一个token  
String tokenValue = null;
// --- 如果允许并发登录 
if(config.getIsConcurrent()) { // 从配置文件application.yml读取是否允许并发
    
    // getConfigOfIsShare()=> getConfig().getIsShare();:从配置文件读取是否在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
    // 如果配置为共享token, 则尝试从Session签名记录里取出token 
    if(getConfigOfIsShare()) {
        // 为确保 jwt-simple 模式的 token Extra 数据生成不受旧token影响,这里必须确保 is-share 配置项在 ExtraData 为空时才可以生效 
        if(loginModel.getExtraData() == null || loginModel.getExtraData().size() == 0) {
            tokenValue = getTokenValueByLoginId(id, loginModel.getDeviceOrDefault());
        }
    } else {
        // 
    }
} 
else {
    // --- 如果不允许并发登录,则将这个账号的历史登录标记为:被顶下线 
    replaced(id, loginModel.getDevice());
}
// 如果至此,仍未成功创建tokenValue, 则开始生成一个 
if(tokenValue == null) {
    // SaFoxUtil 是 sa-token 内部的工具类
    if(SaFoxUtil.isEmpty(loginModel.getToken())) {
        tokenValue = createTokenValue(id, loginModel.getDeviceOrDefault(), loginModel.getTimeout(), loginModel.getExtraData());
    } else {
        tokenValue = loginModel.getToken();
    }
}
  • tokenValue = createTokenValue(id, loginModel.getDeviceOrDefault(), loginModel.getTimeout(), loginModel.getExtraData());

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jr9t8xZC-1667466675039)(RuoYi-Vue-Plus登陆.assets/image-20221103111826811.png)]

  • SaJwtUtil.createToken(this.loginType, loginId, extraData, this.jwtSecretKey());

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0ysgsuXz-1667466675039)(RuoYi-Vue-Plus登陆.assets/image-20221103111928988.png)]

  • 总结:经过上面的步骤生成token了

获取 User-Session , 续期

SaSession session = getSessionByLoginId(id, true);
session.updateMinTimeout(loginModel.getTimeout());
	
// 在 User-Session 上记录token签名 
session.addTokenSign(tokenValue, loginModel.getDeviceOrDefault());

这三个方法还没执行前

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gbwCgG5x-1667466675039)(RuoYi-Vue-Plus登陆.assets/image-20221103115427255.png)]

① getSessionByLoginId(id, true);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o0p5VzBk-1667466675039)(RuoYi-Vue-Plus登陆.assets/image-20221103112101174.png)]

  • getSessionBySessionId(splicingKeySession(loginId), isCreate);
/** 
	 * 获取指定key的Session, 如果Session尚未创建,isCreate=是否新建并返回
	 * @param sessionId SessionId
	 * @param isCreate 是否新建
	 * @return Session对象 
	 */
public SaSession getSessionBySessionId(String sessionId, boolean isCreate) {
    // 先获取有没有 session
    SaSession session = getSaTokenDao().getSession(sessionId);
    if(session == null && isCreate) {
        // 创建 session
        session = SaStrategy.me.createSession.apply(sessionId);
        // 写入 session 
        getSaTokenDao().setSession(session, getConfig().getTimeout());
    }
    return session;
}

getSaTokenDao().setSession();

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fqp6bUQg-1667466675039)(RuoYi-Vue-Plus登陆.assets/image-20221103112521254.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iKHOILMz-1667466675040)(RuoYi-Vue-Plus登陆.assets/image-20221103112620238.png)]

② session.updateMinTimeout(loginModel.getTimeout());

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Flb3ei9Z-1667466675040)(RuoYi-Vue-Plus登陆.assets/image-20221103113211547.png)]

可以在项目里面任意位置使用:SaManager.getSaTokenDao().getSession(“Authorization:login:session:sys_user:1”)获取下面东西:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aiVqzWgg-1667466675040)(RuoYi-Vue-Plus登陆.assets/image-20221103113818176.png)]

③ session.addTokenSign(tokenValue, loginModel.getDeviceOrDefault());

在 User-Session 上记录token签名

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TgIwjA8O-1667466675040)(RuoYi-Vue-Plus登陆.assets/image-20221103113745126.png)]

经过上面的代码已经存入 session 中了

持久化其它数据

// ------ 4. 持久化其它数据 
// token -> id 映射关系  
saveTokenToIdMapping(tokenValue, id, loginModel.getTimeout());

// 写入 [token-last-activity] 
setLastActivityToNow(tokenValue); 

// $$ 通知监听器,账号xxx 登录成功 
SaManager.getSaTokenListener().doLogin(loginType, id, tokenValue, loginModel);

// 检查此账号会话数量是否超出最大值 
if(config.getMaxLoginCount() != -1) {
   logoutByMaxLoginCount(id, session, null, config.getMaxLoginCount());
}

① saveTokenToIdMapping(tokenValue, id, loginModel.getTimeout());

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vRViF1aq-1667466675041)(RuoYi-Vue-Plus登陆.assets/image-20221103114106284.png)]

  • splicingKeyTokenValue(tokenValue)
/**  
	 * 拼接key: tokenValue 持久化 token-id
	 * @param tokenValue token值
	 * @return key
	 */
public String splicingKeyTokenValue(String tokenValue) {
    return getConfig().getTokenName() + ":" + loginType + ":token:" + tokenValue;
}
  • getSaTokenDao().set(splicingKeyTokenValue(tokenValue), String.valueOf(loginId), timeout);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BTS2pXV8-1667466675041)(RuoYi-Vue-Plus登陆.assets/image-20221103114238275.png)]

  • 总结:将 token 存入 Redis了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HPaMJwOF-1667466675042)(2 RuoYi-Vue-Plus登陆.assets/image-20221103163559081.png)]

② setLastActivityToNow(tokenValue);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CGXiALQx-1667466675042)(RuoYi-Vue-Plus登陆.assets/image-20221103114418849.png)]

  • splicingKeyLastActivityTime(tokenValue)
/** 
	 * 拼接key: 指定token的最后操作时间 持久化 
	 * @param tokenValue token值
	 * @return key
	 */
public String splicingKeyLastActivityTime(String tokenValue) {
    return getConfig().getTokenName() + ":" + loginType + ":last-activity:" + tokenValue;
}
  • getSaTokenDao().set(splicingKeyLastActivityTime(tokenValue), String.valueOf(System.currentTimeMillis()), getConfig().getTimeout());

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SL9pNdZJ-1667466675042)(RuoYi-Vue-Plus登陆.assets/image-20221103114535201.png)]

  • 总结:存入 Redis :

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NIyKS6na-1667466675042)(2 RuoYi-Vue-Plus登陆.assets/image-20221103163852778.png)]

③ SaManager.getSaTokenListener().doLogin(loginType, id, tokenValue, loginModel);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p0IXaRmn-1667466675042)(RuoYi-Vue-Plus登陆.assets/image-20221103114315106.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zkpTpUZP-1667466675042)(RuoYi-Vue-Plus登陆.assets/image-20221103114643706.png)]

  • 总结:存入Redis:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qK7mxLj2-1667466675042)(2 RuoYi-Vue-Plus登陆.assets/image-20221103164014835.png)]

④ logoutByMaxLoginCount(id, session, null, config.getMaxLoginCount());

/**
 * 会话注销,根据账号id 和 设备类型 和 最大同时在线数量 
 * 
 * @param loginId 账号id 
 * @param session 此账号的 Session 对象,可填写null,框架将自动获取 
 * @param device 设备类型 (填null代表注销所有设备类型) 
 * @param maxLoginCount 保留最近的几次登录 
 */
public void logoutByMaxLoginCount(Object loginId, SaSession session, String device, int maxLoginCount) {
    if(session == null) {
        session = getSessionByLoginId(loginId, false);
        if(session == null) {
            return;
        }
    }
    List<TokenSign> list = session.tokenSignListCopyByDevice(device);
    // 遍历操作 
    for (int i = 0; i < list.size(); i++) {
        // 只操作前n条 
        if(i >= list.size() - maxLoginCount) {
            continue;
        }
        // 清理: token签名、token最后活跃时间 
        String tokenValue = list.get(i).getValue();
        session.removeTokenSign(tokenValue); 
        clearLastActivity(tokenValue); 	
        // 删除Token-Id映射 & 清除Token-Session 
        deleteTokenToIdMapping(tokenValue);
        deleteTokenSession(tokenValue);
        SaManager.getSaTokenListener().doLogout(loginType, loginId, tokenValue);
    }
    // 注销 Session 
    session.logoutByTokenSignCountToZero();
}
  • session.logoutByTokenSignCountToZero();
/** 当Session上的tokenSign数量为零时,注销会话 */
public void logoutByTokenSignCountToZero() {
    if (tokenSignList.size() == 0) {
        logout();
    }
}
2.6.2、在当前客户端注入Token
/**
 * 在当前会话写入当前TokenValue 
 * @param tokenValue token值 
 * @param cookieTimeout Cookie存活时间(秒)
 */
public void setTokenValue(String tokenValue, int cookieTimeout){

    // 如果 tokenValue 是 空,直接返回
    if(SaFoxUtil.isEmpty(tokenValue)) {
        return;
    }

    // 1. 将token保存到[存储器]里  
    setTokenValueToStorage(tokenValue);

    // 2. 将 Token 保存到 [Cookie] 里 
    // 获取到 application.yml配置文件里面的 is-read-cookie: false
    if (getConfig().getIsReadCookie()) {
        setTokenValueToCookie(tokenValue, cookieTimeout);
    }
}

下面一个方法一个方法分析上面的代码:

setTokenValueToStorage(tokenValue);

/**
 * 将 Token 保存到 [Storage] 里 
 * @param tokenValue token值 
 */
public void setTokenValueToStorage(String tokenValue){
    // 1. 将token保存到[存储器]里  
    SaStorage storage = SaHolder.getStorage();

    // 2. 如果打开了 Token 前缀模式,则拼接上前缀
    // 获取到 application.yml配置文件里面的 token-prefix: "Bearer"
    String tokenPrefix = getConfig().getTokenPrefix();
    if(SaFoxUtil.isEmpty(tokenPrefix) == false) { // tokenPrefix不是空
        // tokenPrefix + SaTokenConsts.TOKEN_CONNECTOR_CHAT + tokenValue= 【 Bearer tokenValue】
        // this.request.setAttribute(key, value);
        storage.set(splicingKeyJustCreatedSave(), tokenPrefix + SaTokenConsts.TOKEN_CONNECTOR_CHAT + tokenValue);	
    } else {
        storage.set(splicingKeyJustCreatedSave(), tokenValue);	
    }

    // 3. 写入 (无前缀) 
    // 常量key标记: 如果token为本次请求新创建的,则以此字符串为key存储在当前request中(不拼接前缀,纯Token)
	// public static final String JUST_CREATED_NOT_PREFIX = "JUST_CREATED_NOT_PREFIX_"; 	
    storage.set(SaTokenConsts.JUST_CREATED_NOT_PREFIX, tokenValue);  
}
/**
 * 如果token为本次请求新创建的,则以此字符串为key存储在当前request中 
 * @return key
 */
public String splicingKeyJustCreatedSave() {
    // 常量key标记: 如果token为本次请求新创建的,则以此字符串为key存储在当前request中 
	// public static final String JUST_CREATED = "JUST_CREATED_"; 	
    return SaTokenConsts.JUST_CREATED;
}
  • 总结:存入 SaHolder.getStorage()里面的两个数据,一个是带有token头 一个是没有带有token头

之后就可以在项目里面获取到

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7ZNs2LJd-1667466675042)(RuoYi-Vue-Plus登陆.assets/image-20221103134702619.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T15xHzkO-1667466675043)(RuoYi-Vue-Plus登陆.assets/image-20221103134730437.png)]

setTokenValueToCookie(tokenValue, cookieTimeout);

/**
 * 将 Token 保存到 [Cookie] 里 
 * @param tokenValue token值 
 * @param cookieTimeout Cookie存活时间(秒)
 */
public void setTokenValueToCookie(String tokenValue, int cookieTimeout){
    SaCookieConfig cfg = getConfig().getCookie();
    SaCookie cookie = new SaCookie()
        .setName(getTokenName())
        .setValue(tokenValue)
        .setMaxAge(cookieTimeout)
        .setDomain(cfg.getDomain())
        .setPath(cfg.getPath())
        .setSecure(cfg.getSecure())
        .setHttpOnly(cfg.getHttpOnly())
        .setSameSite(cfg.getSameSite())
        ;
    // 写入响应头时使用的key:String HEADER_NAME = "Set-Cookie";
    // 写入指定Cookie
    SaHolder.getResponse().addCookie(cookie);
}

setLoginUser(loginUser);设置用户数据多级缓存

/**
* 设置用户数据(多级缓存)
*/
public static void setLoginUser(LoginUser loginUser) {
    // 存入session中
    // StpUtil.getTokenSession():获取当前会话的Session,如果Session尚未创建,则新建并返回
    //  StpUtil.getTokenSession().get():从 session 中获取
    StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser); // LOGIN_USER_KEY = "loginUser";
}

执行完上面的代码后:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1TU2dh6s-1667466675043)(2 RuoYi-Vue-Plus登陆.assets/image-20221103170359234.png)]

redis里面:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uZ7njgy5-1667466675043)(2 RuoYi-Vue-Plus登陆.assets/image-20221103170928767.png)]

后序可以根据 StpUtil.getTokenSession().get(LOGIN_USER_KEY): 获取 loginUser

2.7、 asyncService.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message(“user.login.success”), request);

记录日志

跟前面记录的方式一样 还是采取异步的方式

2.8、recordLoginInfo(user.getUserId(), username);

更新用户登录信息

/**
 * 记录登录信息
 *		主要是修改了登陆时间、Ip信息
 * @param userId 用户ID
 */
public void recordLoginInfo(Long userId, String username) {
    SysUser sysUser = new SysUser();
    sysUser.setUserId(userId);
    sysUser.setLoginIp(ServletUtils.getClientIP());
    sysUser.setLoginDate(DateUtils.getNowDate());
    sysUser.setUpdateBy(username);
    userService.updateUserProfile(sysUser);
}

修改数据库表 sys_user :主要是修改了登陆时间、Ip信息

2.9、 StpUtil.getTokenValue()

获取当前会话的token值

进入代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VV2Bxdia-1667466675043)(RuoYi-Vue-Plus登陆.assets/image-20221103141841313.png)]

继续进入:

/**
 * 获取当前TokenValue
 * @return 当前tokenValue
 */
public String getTokenValue(){
    // 获取当前TokenValue (不裁剪前缀)
    String tokenValue = getTokenValueNotCut();

    // 2. 如果打开了前缀模式,则裁剪掉 
    // 从 application.yml配置文件中获取到前缀: token-prefix: "Bearer"
    String tokenPrefix = getConfig().getTokenPrefix();
    if(SaFoxUtil.isEmpty(tokenPrefix) == false) {
        // 如果token并没有按照指定的前缀开头,则视为未提供token 
        if(SaFoxUtil.isEmpty(tokenValue) || tokenValue.startsWith(tokenPrefix + SaTokenConsts.TOKEN_CONNECTOR_CHAT) == false) {
            tokenValue = null;
        } else {
            // 则裁剪掉前缀 
            tokenValue = tokenValue.substring(tokenPrefix.length() + SaTokenConsts.TOKEN_CONNECTOR_CHAT.length());
        }
    }

    // 3. 返回 
    return tokenValue;
}

上面代码最主要的方法就是:getTokenValueNotCut():

getTokenValueNotCut(): 获取当前TokenValue (不裁剪前缀)

/**
 * 获取当前TokenValue (不裁剪前缀)
 * @return / 
 */
public String getTokenValueNotCut(){
    // 0. 获取相应对象 
    SaStorage storage = SaHolder.getStorage();
    SaRequest request = SaHolder.getRequest();
    // 获取 application.yml 文件封装成的对象
    SaTokenConfig config = getConfig();
    // 这个方法获取配置文件里面的:token-name: Authorization
    String keyTokenName = getTokenName();
    String tokenValue = null;

    // 1. 尝试从Storage里读取
    // splicingKeyJustCreatedSave():String JUST_CREATED = "JUST_CREATED_"; 前面 2.6.2 看过了可以获取到	
    if(storage.get(splicingKeyJustCreatedSave()) != null) {
        tokenValue = String.valueOf(storage.get(splicingKeyJustCreatedSave()));
    }
    // 2. 尝试从请求体里面读取 
    if(tokenValue == null && config.getIsReadBody()){
        tokenValue = request.getParam(keyTokenName);
    }
    // 3. 尝试从header里读取 
    if(tokenValue == null && config.getIsReadHead()){
        tokenValue = request.getHeader(keyTokenName);
    }
    // 4. 尝试从cookie里读取 
    if(tokenValue == null && config.getIsReadCookie()){
        tokenValue = request.getCookieValue(keyTokenName);
    }

    // 5. 返回 
    return tokenValue;
}
三、封装成前端需要的对象返回

就是返回给了前端 token 信息,不带头的 token 信息。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值