RuoYi-Vue-Plus登陆逻辑的实现
分析
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 框架的登陆方法
进入代码:
继续进入:
继续进入:
终于来到这个方法了,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、前置检查:如果此账号已被封禁.
进入这个方法里面
- getSaTokenDao()方法
因为 类 PlusSaTokenDao实现了 SaTokenDao 接口
SaTokenDao 是数据持久层接口,负责所有会话数据的底层写入和读取。
Sa-Token文档:
- splicingKeyDisable(loginId) 方法
getConfig()就是读取到配置文件application.yml里面的值
- getSaTokenDao().get(splicingKeyDisable(loginId)) != null;从Redis里面查询这个用户是不是被封禁的
初始化 loginModel
-
getConfig();
读取application里面的东西封装成
SaTokenConfig
对象 SaTokenConfig类型 -
build();
生成一个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());
- SaJwtUtil.createToken(this.loginType, loginId, extraData, this.jwtSecretKey());
- 总结:经过上面的步骤生成token了
获取 User-Session , 续期
SaSession session = getSessionByLoginId(id, true);
session.updateMinTimeout(loginModel.getTimeout());
// 在 User-Session 上记录token签名
session.addTokenSign(tokenValue, loginModel.getDeviceOrDefault());
这三个方法还没执行前
① getSessionByLoginId(id, true);
- 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();
② session.updateMinTimeout(loginModel.getTimeout());
可以在项目里面任意位置使用:SaManager.getSaTokenDao().getSession(“Authorization:login:session:sys_user:1”)获取下面东西:
③ session.addTokenSign(tokenValue, loginModel.getDeviceOrDefault());
在 User-Session 上记录token签名
经过上面的代码已经存入 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());
- 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);
- 总结:将 token 存入 Redis了
② setLastActivityToNow(tokenValue);
- 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());
- 总结:存入 Redis :
③ SaManager.getSaTokenListener().doLogin(loginType, id, tokenValue, loginModel);
- 总结:存入Redis:
④ 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头
之后就可以在项目里面获取到
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";
}
执行完上面的代码后:
redis里面:
后序可以根据 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值
进入代码:
继续进入:
/**
* 获取当前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 信息。