🔨 修改认证逻辑.
This commit is contained in:
@@ -25,6 +25,7 @@ package org.dromara.visor.module.infra.api.impl;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.dromara.visor.common.constant.ErrorMessage;
|
import org.dromara.visor.common.constant.ErrorMessage;
|
||||||
import org.dromara.visor.common.utils.Assert;
|
import org.dromara.visor.common.utils.Assert;
|
||||||
|
import org.dromara.visor.common.utils.Requests;
|
||||||
import org.dromara.visor.module.infra.api.AuthenticationApi;
|
import org.dromara.visor.module.infra.api.AuthenticationApi;
|
||||||
import org.dromara.visor.module.infra.entity.domain.SystemUserDO;
|
import org.dromara.visor.module.infra.entity.domain.SystemUserDO;
|
||||||
import org.dromara.visor.module.infra.entity.dto.user.SystemUserAuthDTO;
|
import org.dromara.visor.module.infra.entity.dto.user.SystemUserAuthDTO;
|
||||||
@@ -57,7 +58,11 @@ public class AuthenticationApiImpl implements AuthenticationApi {
|
|||||||
result.setUsername(user.getUsername());
|
result.setUsername(user.getUsername());
|
||||||
result.setNickname(user.getNickname());
|
result.setNickname(user.getNickname());
|
||||||
// 检查用户密码
|
// 检查用户密码
|
||||||
boolean passRight = authenticationService.checkUserPassword(user, password, addFailedCount);
|
boolean passRight = authenticationService.checkUserPassword(user, password);
|
||||||
|
if (!passRight && addFailedCount) {
|
||||||
|
// 发送站内信
|
||||||
|
authenticationService.addLoginFailedCount(user.getUsername(), Requests.getIdentity());
|
||||||
|
}
|
||||||
result.setPassRight(passRight);
|
result.setPassRight(passRight);
|
||||||
Assert.isTrue(passRight, ErrorMessage.USERNAME_PASSWORD_ERROR);
|
Assert.isTrue(passRight, ErrorMessage.USERNAME_PASSWORD_ERROR);
|
||||||
// 检查用户状态
|
// 检查用户状态
|
||||||
|
|||||||
@@ -63,9 +63,8 @@ public class AuthenticationController {
|
|||||||
@PermitAll
|
@PermitAll
|
||||||
@Operation(summary = "登录")
|
@Operation(summary = "登录")
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public UserLoginVO login(@Validated @RequestBody UserLoginRequest request,
|
public UserLoginVO login(@Validated @RequestBody UserLoginRequest request) {
|
||||||
HttpServletRequest servletRequest) {
|
return authenticationService.login(request);
|
||||||
return authenticationService.login(request, servletRequest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OperatorLog(AuthenticationOperatorType.LOGOUT)
|
@OperatorLog(AuthenticationOperatorType.LOGOUT)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import org.dromara.visor.module.infra.enums.LoginTokenStatusEnum;
|
import org.dromara.visor.common.entity.RequestIdentityModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录 token 缓存
|
* 登录 token 缓存
|
||||||
@@ -42,14 +42,19 @@ import org.dromara.visor.module.infra.enums.LoginTokenStatusEnum;
|
|||||||
public class LoginTokenDTO {
|
public class LoginTokenDTO {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户id
|
* userId
|
||||||
*/
|
*/
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
*/
|
||||||
|
private String username;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* token 状态
|
* token 状态
|
||||||
*
|
*
|
||||||
* @see LoginTokenStatusEnum
|
* @see org.dromara.visor.module.infra.enums.LoginTokenStatusEnum
|
||||||
*/
|
*/
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
@@ -59,13 +64,13 @@ public class LoginTokenDTO {
|
|||||||
private Integer refreshCount;
|
private Integer refreshCount;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 原始登录身份
|
* 原始登录留痕信息
|
||||||
*/
|
*/
|
||||||
private LoginTokenIdentityDTO origin;
|
private RequestIdentityModel origin;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 覆盖登录身份
|
* 覆盖登录刘海信息
|
||||||
*/
|
*/
|
||||||
private LoginTokenIdentityDTO override;
|
private RequestIdentityModel override;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ import cn.orionsec.kit.lang.utils.time.Dates;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.dromara.visor.common.constant.ErrorCode;
|
import org.dromara.visor.common.constant.ErrorCode;
|
||||||
|
import org.dromara.visor.common.entity.RequestIdentityModel;
|
||||||
import org.dromara.visor.module.infra.entity.dto.LoginTokenDTO;
|
import org.dromara.visor.module.infra.entity.dto.LoginTokenDTO;
|
||||||
import org.dromara.visor.module.infra.entity.dto.LoginTokenIdentityDTO;
|
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
@@ -53,9 +53,9 @@ public enum LoginTokenStatusEnum {
|
|||||||
OTHER_DEVICE(1) {
|
OTHER_DEVICE(1) {
|
||||||
@Override
|
@Override
|
||||||
public RuntimeException toException(LoginTokenDTO token) {
|
public RuntimeException toException(LoginTokenDTO token) {
|
||||||
LoginTokenIdentityDTO override = token.getOverride();
|
RequestIdentityModel override = token.getOverride();
|
||||||
return ErrorCode.USER_OTHER_DEVICE_LOGIN.exception(
|
return ErrorCode.USER_OTHER_DEVICE_LOGIN.exception(
|
||||||
Dates.format(new Date(override.getLoginTime()), Dates.MD_HM),
|
Dates.format(new Date(override.getTimestamp()), Dates.MD_HM),
|
||||||
override.getAddress(),
|
override.getAddress(),
|
||||||
override.getLocation());
|
override.getLocation());
|
||||||
}
|
}
|
||||||
@@ -68,9 +68,9 @@ public enum LoginTokenStatusEnum {
|
|||||||
SESSION_OFFLINE(2) {
|
SESSION_OFFLINE(2) {
|
||||||
@Override
|
@Override
|
||||||
public RuntimeException toException(LoginTokenDTO token) {
|
public RuntimeException toException(LoginTokenDTO token) {
|
||||||
LoginTokenIdentityDTO override = token.getOverride();
|
RequestIdentityModel override = token.getOverride();
|
||||||
return ErrorCode.USER_OFFLINE.exception(
|
return ErrorCode.USER_OFFLINE.exception(
|
||||||
Dates.format(new Date(override.getLoginTime()), Dates.MD_HM),
|
Dates.format(new Date(override.getTimestamp()), Dates.MD_HM),
|
||||||
override.getAddress(),
|
override.getAddress(),
|
||||||
override.getLocation());
|
override.getLocation());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,12 +82,13 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
|
|||||||
if (tokenInfo == null) {
|
if (tokenInfo == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
Long loginTime = tokenInfo.getOrigin().getTimestamp();
|
||||||
try {
|
try {
|
||||||
// 检查 token 状态
|
// 检查 token 状态
|
||||||
this.checkTokenStatus(tokenInfo);
|
this.checkTokenStatus(tokenInfo);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// token 失效则删除
|
// token 失效则删除
|
||||||
RedisUtils.delete(UserCacheKeyDefine.LOGIN_TOKEN.format(tokenInfo.getId(), tokenInfo.getOrigin().getLoginTime()));
|
RedisUtils.delete(UserCacheKeyDefine.LOGIN_TOKEN.format(tokenInfo.getId(), loginTime));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
// 获取登录信息
|
// 获取登录信息
|
||||||
@@ -98,7 +99,7 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
|
|||||||
// 检查用户状态
|
// 检查用户状态
|
||||||
UserStatusEnum.checkUserStatus(user.getStatus());
|
UserStatusEnum.checkUserStatus(user.getStatus());
|
||||||
// 设置登录时间戳
|
// 设置登录时间戳
|
||||||
user.setTimestamp(tokenInfo.getOrigin().getLoginTime());
|
user.setTimestamp(loginTime);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.dromara.visor.module.infra.service;
|
package org.dromara.visor.module.infra.service;
|
||||||
|
|
||||||
|
import org.dromara.visor.common.entity.RequestIdentityModel;
|
||||||
import org.dromara.visor.common.security.LoginUser;
|
import org.dromara.visor.common.security.LoginUser;
|
||||||
import org.dromara.visor.module.infra.entity.domain.SystemUserDO;
|
import org.dromara.visor.module.infra.entity.domain.SystemUserDO;
|
||||||
import org.dromara.visor.module.infra.entity.dto.LoginTokenDTO;
|
import org.dromara.visor.module.infra.entity.dto.LoginTokenDTO;
|
||||||
@@ -42,11 +43,10 @@ public interface AuthenticationService {
|
|||||||
/**
|
/**
|
||||||
* 登录
|
* 登录
|
||||||
*
|
*
|
||||||
* @param request request
|
* @param request request
|
||||||
* @param servletRequest servletRequest
|
|
||||||
* @return login
|
* @return login
|
||||||
*/
|
*/
|
||||||
UserLoginVO login(UserLoginRequest request, HttpServletRequest servletRequest);
|
UserLoginVO login(UserLoginRequest request);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登出
|
* 登出
|
||||||
@@ -83,12 +83,19 @@ public interface AuthenticationService {
|
|||||||
/**
|
/**
|
||||||
* 检查用户密码
|
* 检查用户密码
|
||||||
*
|
*
|
||||||
* @param user user
|
* @param user user
|
||||||
* @param password password
|
* @param password password
|
||||||
* @param addFailedCount addFailedCount
|
|
||||||
* @return passRight
|
* @return passRight
|
||||||
*/
|
*/
|
||||||
boolean checkUserPassword(SystemUserDO user, String password, boolean addFailedCount);
|
boolean checkUserPassword(SystemUserDO user, String password);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加登录失败次数
|
||||||
|
*
|
||||||
|
* @param username username
|
||||||
|
* @param identity identity
|
||||||
|
*/
|
||||||
|
void addLoginFailedCount(String username, RequestIdentityModel identity);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查用户状态
|
* 检查用户状态
|
||||||
|
|||||||
@@ -22,29 +22,26 @@
|
|||||||
*/
|
*/
|
||||||
package org.dromara.visor.module.infra.service.impl;
|
package org.dromara.visor.module.infra.service.impl;
|
||||||
|
|
||||||
import cn.orionsec.kit.lang.annotation.Keep;
|
|
||||||
import cn.orionsec.kit.lang.define.wrapper.Pair;
|
import cn.orionsec.kit.lang.define.wrapper.Pair;
|
||||||
import cn.orionsec.kit.lang.utils.Booleans;
|
import cn.orionsec.kit.lang.utils.Booleans;
|
||||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||||
import cn.orionsec.kit.lang.utils.Strings;
|
|
||||||
import cn.orionsec.kit.lang.utils.collect.Lists;
|
|
||||||
import cn.orionsec.kit.lang.utils.crypto.Signatures;
|
import cn.orionsec.kit.lang.utils.crypto.Signatures;
|
||||||
import cn.orionsec.kit.lang.utils.time.Dates;
|
import cn.orionsec.kit.lang.utils.time.Dates;
|
||||||
import cn.orionsec.kit.web.servlet.web.Servlets;
|
|
||||||
import com.alibaba.fastjson.JSON;
|
import com.alibaba.fastjson.JSON;
|
||||||
import org.dromara.visor.common.config.ConfigStore;
|
import org.dromara.visor.common.config.ConfigStore;
|
||||||
import org.dromara.visor.common.constant.ConfigKeys;
|
import org.dromara.visor.common.constant.ConfigKeys;
|
||||||
import org.dromara.visor.common.constant.Const;
|
import org.dromara.visor.common.constant.Const;
|
||||||
import org.dromara.visor.common.constant.ErrorMessage;
|
import org.dromara.visor.common.constant.ErrorMessage;
|
||||||
import org.dromara.visor.common.constant.ExtraFieldConst;
|
import org.dromara.visor.common.constant.ExtraFieldConst;
|
||||||
|
import org.dromara.visor.common.entity.RequestIdentity;
|
||||||
|
import org.dromara.visor.common.entity.RequestIdentityModel;
|
||||||
import org.dromara.visor.common.security.LoginUser;
|
import org.dromara.visor.common.security.LoginUser;
|
||||||
import org.dromara.visor.common.security.UserRole;
|
import org.dromara.visor.common.security.UserRole;
|
||||||
import org.dromara.visor.common.utils.AesEncryptUtils;
|
import org.dromara.visor.common.utils.AesEncryptUtils;
|
||||||
import org.dromara.visor.common.utils.Assert;
|
import org.dromara.visor.common.utils.Assert;
|
||||||
import org.dromara.visor.common.utils.IpUtils;
|
import org.dromara.visor.common.utils.Requests;
|
||||||
import org.dromara.visor.framework.biz.operator.log.core.utils.OperatorLogs;
|
import org.dromara.visor.framework.biz.operator.log.core.utils.OperatorLogs;
|
||||||
import org.dromara.visor.framework.redis.core.utils.RedisStrings;
|
import org.dromara.visor.framework.redis.core.utils.RedisStrings;
|
||||||
import org.dromara.visor.framework.redis.core.utils.RedisUtils;
|
|
||||||
import org.dromara.visor.framework.security.core.utils.SecurityUtils;
|
import org.dromara.visor.framework.security.core.utils.SecurityUtils;
|
||||||
import org.dromara.visor.module.common.config.AppLoginConfig;
|
import org.dromara.visor.module.common.config.AppLoginConfig;
|
||||||
import org.dromara.visor.module.infra.api.SystemMessageApi;
|
import org.dromara.visor.module.infra.api.SystemMessageApi;
|
||||||
@@ -54,8 +51,8 @@ import org.dromara.visor.module.infra.dao.SystemUserRoleDAO;
|
|||||||
import org.dromara.visor.module.infra.define.cache.UserCacheKeyDefine;
|
import org.dromara.visor.module.infra.define.cache.UserCacheKeyDefine;
|
||||||
import org.dromara.visor.module.infra.define.message.SystemUserMessageDefine;
|
import org.dromara.visor.module.infra.define.message.SystemUserMessageDefine;
|
||||||
import org.dromara.visor.module.infra.entity.domain.SystemUserDO;
|
import org.dromara.visor.module.infra.entity.domain.SystemUserDO;
|
||||||
|
import org.dromara.visor.module.infra.entity.dto.LoginFailedDTO;
|
||||||
import org.dromara.visor.module.infra.entity.dto.LoginTokenDTO;
|
import org.dromara.visor.module.infra.entity.dto.LoginTokenDTO;
|
||||||
import org.dromara.visor.module.infra.entity.dto.LoginTokenIdentityDTO;
|
|
||||||
import org.dromara.visor.module.infra.entity.dto.message.SystemMessageDTO;
|
import org.dromara.visor.module.infra.entity.dto.message.SystemMessageDTO;
|
||||||
import org.dromara.visor.module.infra.entity.request.user.UserLoginRequest;
|
import org.dromara.visor.module.infra.entity.request.user.UserLoginRequest;
|
||||||
import org.dromara.visor.module.infra.entity.vo.UserLoginVO;
|
import org.dromara.visor.module.infra.entity.vo.UserLoginVO;
|
||||||
@@ -63,7 +60,6 @@ import org.dromara.visor.module.infra.enums.LoginTokenStatusEnum;
|
|||||||
import org.dromara.visor.module.infra.enums.UserStatusEnum;
|
import org.dromara.visor.module.infra.enums.UserStatusEnum;
|
||||||
import org.dromara.visor.module.infra.service.AuthenticationService;
|
import org.dromara.visor.module.infra.service.AuthenticationService;
|
||||||
import org.dromara.visor.module.infra.service.UserPermissionService;
|
import org.dromara.visor.module.infra.service.UserPermissionService;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
@@ -98,10 +94,6 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
|||||||
@Resource
|
@Resource
|
||||||
private SystemMessageApi systemMessageApi;
|
private SystemMessageApi systemMessageApi;
|
||||||
|
|
||||||
@Keep
|
|
||||||
@Resource
|
|
||||||
private RedisTemplate<String, String> redisTemplate;
|
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ConfigStore configStore;
|
private ConfigStore configStore;
|
||||||
|
|
||||||
@@ -110,25 +102,29 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
|||||||
// 监听并且设置缓存过期时间
|
// 监听并且设置缓存过期时间
|
||||||
configStore.int32(ConfigKeys.LOGIN_LOGIN_SESSION_TIME).onChange((v, b) -> this.setCacheExpireTime());
|
configStore.int32(ConfigKeys.LOGIN_LOGIN_SESSION_TIME).onChange((v, b) -> this.setCacheExpireTime());
|
||||||
configStore.int32(ConfigKeys.LOGIN_REFRESH_INTERVAL).onChange((v, b) -> this.setCacheExpireTime());
|
configStore.int32(ConfigKeys.LOGIN_REFRESH_INTERVAL).onChange((v, b) -> this.setCacheExpireTime());
|
||||||
|
configStore.int32(ConfigKeys.LOGIN_LOGIN_FAILED_LOCK_TIME).onChange((v, b) -> this.setCacheExpireTime());
|
||||||
this.setCacheExpireTime();
|
this.setCacheExpireTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserLoginVO login(UserLoginRequest request, HttpServletRequest servletRequest) {
|
public UserLoginVO login(UserLoginRequest request) {
|
||||||
// 获取登录信息
|
// 获取登录痕迹
|
||||||
String remoteAddr = IpUtils.getRemoteAddr(servletRequest);
|
String username = request.getUsername();
|
||||||
String location = IpUtils.getLocation(remoteAddr);
|
RequestIdentityModel identity = Requests.getIdentity();
|
||||||
String userAgent = Servlets.getUserAgent(servletRequest);
|
|
||||||
// 设置日志上下文的用户 否则登录失败不会记录日志
|
// 设置日志上下文的用户 否则登录失败不会记录日志
|
||||||
OperatorLogs.setUser(SystemUserConvert.MAPPER.toLoginUser(request));
|
OperatorLogs.setUser(SystemUserConvert.MAPPER.toLoginUser(request));
|
||||||
// 登录前检查
|
// 登录前检查
|
||||||
SystemUserDO user = this.preCheckLogin(request.getUsername(), request.getPassword());
|
SystemUserDO user = this.preCheckLogin(username, request.getPassword());
|
||||||
// 重新设置日志上下文
|
// 重新设置日志上下文
|
||||||
OperatorLogs.setUser(SystemUserConvert.MAPPER.toLoginUser(user));
|
OperatorLogs.setUser(SystemUserConvert.MAPPER.toLoginUser(user));
|
||||||
// 用户密码校验
|
// 用户密码校验
|
||||||
boolean passRight = this.checkUserPassword(user, request.getPassword(), true);
|
boolean passRight = this.checkUserPassword(user, request.getPassword());
|
||||||
// 发送站内信
|
if (!passRight) {
|
||||||
this.sendLoginFailedErrorMessage(passRight, user, remoteAddr, location);
|
// 增加登录失败次数
|
||||||
|
this.addLoginFailedCount(username, identity);
|
||||||
|
// 登录失败发送站内信
|
||||||
|
this.sendLoginFailedErrorMessage(user, identity);
|
||||||
|
}
|
||||||
Assert.isTrue(passRight, ErrorMessage.USERNAME_PASSWORD_ERROR);
|
Assert.isTrue(passRight, ErrorMessage.USERNAME_PASSWORD_ERROR);
|
||||||
// 用户状态校验
|
// 用户状态校验
|
||||||
this.checkUserStatus(user);
|
this.checkUserStatus(user);
|
||||||
@@ -139,14 +135,13 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
|||||||
this.deleteUserCache(user);
|
this.deleteUserCache(user);
|
||||||
// 重设用户缓存
|
// 重设用户缓存
|
||||||
this.setUserCache(user);
|
this.setUserCache(user);
|
||||||
long current = System.currentTimeMillis();
|
|
||||||
// 不允许多端登录
|
// 不允许多端登录
|
||||||
if (Booleans.isFalse(appLoginConfig.getAllowMultiDevice())) {
|
if (Booleans.isFalse(appLoginConfig.getAllowMultiDevice())) {
|
||||||
// 无效化其他缓存
|
// 无效化其他缓存
|
||||||
this.invalidOtherDeviceToken(id, current, remoteAddr, location, userAgent);
|
this.invalidOtherDeviceToken(id, identity);
|
||||||
}
|
}
|
||||||
// 生成 loginToken
|
// 生成 loginToken
|
||||||
String token = this.generatorLoginToken(user, current, remoteAddr, location, userAgent);
|
String token = this.generatorLoginToken(user, identity);
|
||||||
return UserLoginVO.builder()
|
return UserLoginVO.builder()
|
||||||
.token(token)
|
.token(token)
|
||||||
.build();
|
.build();
|
||||||
@@ -169,16 +164,16 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
|||||||
// 删除 loginToken & refreshToken
|
// 删除 loginToken & refreshToken
|
||||||
String loginKey = UserCacheKeyDefine.LOGIN_TOKEN.format(id, current);
|
String loginKey = UserCacheKeyDefine.LOGIN_TOKEN.format(id, current);
|
||||||
String refreshKey = UserCacheKeyDefine.LOGIN_REFRESH.format(id, current);
|
String refreshKey = UserCacheKeyDefine.LOGIN_REFRESH.format(id, current);
|
||||||
redisTemplate.delete(Lists.of(loginKey, refreshKey));
|
RedisStrings.delete(loginKey, refreshKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LoginUser getLoginUser(Long id) {
|
public LoginUser getLoginUser(Long id) {
|
||||||
|
// 查询缓存用户信息
|
||||||
String userInfoKey = UserCacheKeyDefine.USER_INFO.format(id);
|
String userInfoKey = UserCacheKeyDefine.USER_INFO.format(id);
|
||||||
String userInfoCache = redisTemplate.opsForValue().get(userInfoKey);
|
LoginUser loginUser = RedisStrings.getJson(userInfoKey, UserCacheKeyDefine.USER_INFO);
|
||||||
// 缓存存在
|
if (loginUser != null) {
|
||||||
if (userInfoCache != null) {
|
return loginUser;
|
||||||
return JSON.parseObject(userInfoCache, LoginUser.class);
|
|
||||||
}
|
}
|
||||||
// 查询用户信息
|
// 查询用户信息
|
||||||
SystemUserDO user = systemUserDAO.selectById(id);
|
SystemUserDO user = systemUserDAO.selectById(id);
|
||||||
@@ -198,22 +193,21 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
|||||||
}
|
}
|
||||||
// 获取登录 key value
|
// 获取登录 key value
|
||||||
String loginKey = UserCacheKeyDefine.LOGIN_TOKEN.format(pair.getKey(), pair.getValue());
|
String loginKey = UserCacheKeyDefine.LOGIN_TOKEN.format(pair.getKey(), pair.getValue());
|
||||||
String loginCache = redisTemplate.opsForValue().get(loginKey);
|
LoginTokenDTO loginCache = RedisStrings.getJson(loginKey, UserCacheKeyDefine.LOGIN_TOKEN);
|
||||||
if (loginCache != null) {
|
if (loginCache != null) {
|
||||||
return JSON.parseObject(loginCache, LoginTokenDTO.class);
|
return loginCache;
|
||||||
}
|
}
|
||||||
// loginToken 不存在 需要查询 refreshToken
|
// loginToken 不存在 需要查询 refreshToken
|
||||||
if (Booleans.isFalse(appLoginConfig.getAllowRefresh())) {
|
if (Booleans.isFalse(appLoginConfig.getAllowRefresh())) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String refreshKey = UserCacheKeyDefine.LOGIN_REFRESH.format(pair.getKey(), pair.getValue());
|
String refreshKey = UserCacheKeyDefine.LOGIN_REFRESH.format(pair.getKey(), pair.getValue());
|
||||||
String refreshCache = redisTemplate.opsForValue().get(refreshKey);
|
LoginTokenDTO refresh = RedisStrings.getJson(refreshKey, UserCacheKeyDefine.LOGIN_REFRESH);
|
||||||
// 未查询到刷新key直接返回
|
// 未查询到 refreshToken 直接返回
|
||||||
if (refreshCache == null) {
|
if (refresh == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// 执行续签操作
|
// 执行续签操作
|
||||||
LoginTokenDTO refresh = JSON.parseObject(refreshCache, LoginTokenDTO.class);
|
|
||||||
int refreshCount = refresh.getRefreshCount() + 1;
|
int refreshCount = refresh.getRefreshCount() + 1;
|
||||||
refresh.setRefreshCount(refreshCount);
|
refresh.setRefreshCount(refreshCount);
|
||||||
// 设置登录缓存
|
// 设置登录缓存
|
||||||
@@ -223,7 +217,7 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
|||||||
RedisStrings.setJson(refreshKey, UserCacheKeyDefine.LOGIN_REFRESH, refresh);
|
RedisStrings.setJson(refreshKey, UserCacheKeyDefine.LOGIN_REFRESH, refresh);
|
||||||
} else {
|
} else {
|
||||||
// 大于等于续签最大次数 则删除
|
// 大于等于续签最大次数 则删除
|
||||||
redisTemplate.delete(refreshKey);
|
RedisStrings.delete(refreshKey);
|
||||||
}
|
}
|
||||||
return refresh;
|
return refresh;
|
||||||
}
|
}
|
||||||
@@ -236,11 +230,14 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
|||||||
}
|
}
|
||||||
// 检查登录失败次数锁定
|
// 检查登录失败次数锁定
|
||||||
if (Booleans.isTrue(appLoginConfig.getLoginFailedLock())) {
|
if (Booleans.isTrue(appLoginConfig.getLoginFailedLock())) {
|
||||||
String failedCountKey = UserCacheKeyDefine.LOGIN_FAILED_COUNT.format(username);
|
String loginFailedKey = UserCacheKeyDefine.LOGIN_FAILED.format(username);
|
||||||
String failedCount = redisTemplate.opsForValue().get(failedCountKey);
|
LoginFailedDTO loginFailed = RedisStrings.getJson(loginFailedKey, UserCacheKeyDefine.LOGIN_FAILED);
|
||||||
if (failedCount != null
|
Integer failedCount = Optional.ofNullable(loginFailed)
|
||||||
&& Integer.parseInt(failedCount) >= appLoginConfig.getLoginFailedLockThreshold()) {
|
.map(LoginFailedDTO::getFailedCount)
|
||||||
throw Exceptions.argument(ErrorMessage.MAX_LOGIN_FAILED);
|
.orElse(null);
|
||||||
|
// 检查是否超过失败次数
|
||||||
|
if (failedCount != null && failedCount >= appLoginConfig.getLoginFailedLockThreshold()) {
|
||||||
|
Assert.lt(failedCount, appLoginConfig.getLoginFailedLockThreshold(), ErrorMessage.MAX_LOGIN_FAILED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 获取登录用户
|
// 获取登录用户
|
||||||
@@ -254,16 +251,32 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean checkUserPassword(SystemUserDO user, String password, boolean addFailedCount) {
|
public boolean checkUserPassword(SystemUserDO user, String password) {
|
||||||
// 检查密码
|
return user.getPassword().equals(Signatures.md5(password));
|
||||||
boolean passRight = user.getPassword().equals(Signatures.md5(password));
|
}
|
||||||
if (!passRight && addFailedCount) {
|
|
||||||
// 刷新登录失败缓存
|
@Override
|
||||||
String failedCountKey = UserCacheKeyDefine.LOGIN_FAILED_COUNT.format(user.getUsername());
|
public void addLoginFailedCount(String username, RequestIdentityModel identity) {
|
||||||
redisTemplate.opsForValue().increment(failedCountKey);
|
// 过期时间
|
||||||
RedisUtils.setExpire(failedCountKey, appLoginConfig.getLoginFailedLockTime(), TimeUnit.MINUTES);
|
long expireTime = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(UserCacheKeyDefine.LOGIN_FAILED.getTimeout());
|
||||||
|
// 刷新登录失败缓存
|
||||||
|
String loginFailedKey = UserCacheKeyDefine.LOGIN_FAILED.format(username);
|
||||||
|
LoginFailedDTO loginFailed = RedisStrings.getJson(loginFailedKey, UserCacheKeyDefine.LOGIN_FAILED);
|
||||||
|
if (loginFailed == null) {
|
||||||
|
// 首次登录失败
|
||||||
|
loginFailed = LoginFailedDTO.builder()
|
||||||
|
.username(username)
|
||||||
|
.failedCount(1)
|
||||||
|
.expireTime(expireTime)
|
||||||
|
.origin(identity)
|
||||||
|
.build();
|
||||||
|
} else {
|
||||||
|
// 非首次登录失败
|
||||||
|
loginFailed.setExpireTime(expireTime);
|
||||||
|
loginFailed.setFailedCount(loginFailed.getFailedCount() + 1);
|
||||||
}
|
}
|
||||||
return passRight;
|
// 重新设置缓存
|
||||||
|
RedisStrings.setJson(loginFailedKey, UserCacheKeyDefine.LOGIN_FAILED, loginFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -275,33 +288,30 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
|||||||
/**
|
/**
|
||||||
* 发送登录失败错误消息
|
* 发送登录失败错误消息
|
||||||
*
|
*
|
||||||
* @param passRight passRight
|
* @param user user
|
||||||
* @param user user
|
* @param identity identity
|
||||||
* @param remoteAddr remoteAddr
|
|
||||||
* @param location location
|
|
||||||
*/
|
*/
|
||||||
private void sendLoginFailedErrorMessage(boolean passRight, SystemUserDO user,
|
private void sendLoginFailedErrorMessage(SystemUserDO user, RequestIdentity identity) {
|
||||||
String remoteAddr, String location) {
|
|
||||||
if (passRight) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 检查是否开启登录失败发信
|
// 检查是否开启登录失败发信
|
||||||
if (!Booleans.isTrue(appLoginConfig.getLoginFailedSend())) {
|
if (!Booleans.isTrue(appLoginConfig.getLoginFailedSend())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String failedCountKey = UserCacheKeyDefine.LOGIN_FAILED_COUNT.format(user.getUsername());
|
String loginFailedKey = UserCacheKeyDefine.LOGIN_FAILED.format(user.getUsername());
|
||||||
String failedCountStr = redisTemplate.opsForValue().get(failedCountKey);
|
LoginFailedDTO loginFailed = RedisStrings.getJson(loginFailedKey, UserCacheKeyDefine.LOGIN_FAILED);
|
||||||
if (failedCountStr == null || !Strings.isInteger(failedCountStr)) {
|
Integer failedCount = Optional.ofNullable(loginFailed)
|
||||||
|
.map(LoginFailedDTO::getFailedCount)
|
||||||
|
.orElse(null);
|
||||||
|
if (failedCount == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 直接用相等 因为只触发一次
|
// 直接用相等 因为只触发一次
|
||||||
if (!Integer.valueOf(failedCountStr).equals(appLoginConfig.getLoginFailedSendThreshold())) {
|
if (!failedCount.equals(appLoginConfig.getLoginFailedSendThreshold())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 发送站内信
|
// 发送站内信
|
||||||
Map<String, Object> params = new HashMap<>();
|
Map<String, Object> params = new HashMap<>();
|
||||||
params.put(ExtraFieldConst.ADDRESS, remoteAddr);
|
params.put(ExtraFieldConst.ADDRESS, identity.getAddress());
|
||||||
params.put(ExtraFieldConst.LOCATION, location);
|
params.put(ExtraFieldConst.LOCATION, identity.getLocation());
|
||||||
params.put(ExtraFieldConst.TIME, Dates.current());
|
params.put(ExtraFieldConst.TIME, Dates.current());
|
||||||
SystemMessageDTO message = SystemMessageDTO.builder()
|
SystemMessageDTO message = SystemMessageDTO.builder()
|
||||||
.receiverId(user.getId())
|
.receiverId(user.getId())
|
||||||
@@ -334,9 +344,9 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
|||||||
// 用户信息缓存
|
// 用户信息缓存
|
||||||
String userInfoKey = UserCacheKeyDefine.USER_INFO.format(user.getId());
|
String userInfoKey = UserCacheKeyDefine.USER_INFO.format(user.getId());
|
||||||
// 登录失败次数缓存
|
// 登录失败次数缓存
|
||||||
String loginFailedCountKey = UserCacheKeyDefine.LOGIN_FAILED_COUNT.format(user.getUsername());
|
String loginFailedCountKey = UserCacheKeyDefine.LOGIN_FAILED.format(user.getUsername());
|
||||||
// 删除缓存
|
// 删除缓存
|
||||||
redisTemplate.delete(Lists.of(userInfoKey, loginFailedCountKey));
|
RedisStrings.delete(userInfoKey, loginFailedCountKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -385,21 +395,16 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
|||||||
/**
|
/**
|
||||||
* 无效化其他登录信息
|
* 无效化其他登录信息
|
||||||
*
|
*
|
||||||
* @param id id
|
* @param id id
|
||||||
* @param loginTime loginTime
|
* @param identity identity
|
||||||
* @param remoteAddr remoteAddr
|
|
||||||
* @param location location
|
|
||||||
* @param userAgent userAgent
|
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("ALL")
|
@SuppressWarnings("ALL")
|
||||||
private void invalidOtherDeviceToken(Long id, long loginTime,
|
private void invalidOtherDeviceToken(Long id, RequestIdentityModel identity) {
|
||||||
String remoteAddr, String location, String userAgent) {
|
|
||||||
// 获取登录信息
|
// 获取登录信息
|
||||||
Set<String> loginKeyList = RedisUtils.scanKeys(UserCacheKeyDefine.LOGIN_TOKEN.format(id, "*"));
|
Set<String> loginKeyList = RedisStrings.scanKeys(UserCacheKeyDefine.LOGIN_TOKEN.format(id, "*"));
|
||||||
if (!loginKeyList.isEmpty()) {
|
if (!loginKeyList.isEmpty()) {
|
||||||
// 获取有效登录信息
|
// 获取有效登录信息
|
||||||
List<LoginTokenDTO> loginTokenInfoList = redisTemplate.opsForValue()
|
List<LoginTokenDTO> loginTokenInfoList = RedisStrings.getList(loginKeyList)
|
||||||
.multiGet(loginKeyList)
|
|
||||||
.stream()
|
.stream()
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.map(s -> JSON.parseObject(s, LoginTokenDTO.class))
|
.map(s -> JSON.parseObject(s, LoginTokenDTO.class))
|
||||||
@@ -407,47 +412,45 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
|||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
// 修改登录信息
|
// 修改登录信息
|
||||||
for (LoginTokenDTO loginTokenInfo : loginTokenInfoList) {
|
for (LoginTokenDTO loginTokenInfo : loginTokenInfoList) {
|
||||||
String deviceLoginKey = UserCacheKeyDefine.LOGIN_TOKEN.format(id, loginTokenInfo.getOrigin().getLoginTime());
|
String deviceLoginKey = UserCacheKeyDefine.LOGIN_TOKEN.format(id, loginTokenInfo.getOrigin().getTimestamp());
|
||||||
loginTokenInfo.setStatus(LoginTokenStatusEnum.OTHER_DEVICE.getStatus());
|
loginTokenInfo.setStatus(LoginTokenStatusEnum.OTHER_DEVICE.getStatus());
|
||||||
loginTokenInfo.setOverride(new LoginTokenIdentityDTO(loginTime, remoteAddr, location, userAgent));
|
loginTokenInfo.setOverride(identity);
|
||||||
RedisStrings.setJson(deviceLoginKey, UserCacheKeyDefine.LOGIN_TOKEN, loginTokenInfo);
|
RedisStrings.setJson(deviceLoginKey, UserCacheKeyDefine.LOGIN_TOKEN, loginTokenInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 删除续签信息
|
// 删除续签信息
|
||||||
if (Booleans.isTrue(appLoginConfig.getAllowRefresh())) {
|
if (Booleans.isTrue(appLoginConfig.getAllowRefresh())) {
|
||||||
RedisUtils.scanKeysDelete(UserCacheKeyDefine.LOGIN_REFRESH.format(id, "*"));
|
RedisStrings.scanKeysDelete(UserCacheKeyDefine.LOGIN_REFRESH.format(id, "*"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成 loginToken
|
* 生成 loginToken
|
||||||
*
|
*
|
||||||
* @param user user
|
* @param user user
|
||||||
* @param loginTime loginTime
|
* @param identity identity
|
||||||
* @param remoteAddr remoteAddr
|
|
||||||
* @param location location
|
|
||||||
* @param userAgent userAgent
|
|
||||||
* @return loginToken
|
* @return loginToken
|
||||||
*/
|
*/
|
||||||
private String generatorLoginToken(SystemUserDO user, long loginTime,
|
private String generatorLoginToken(SystemUserDO user, RequestIdentityModel identity) {
|
||||||
String remoteAddr, String location, String userAgent) {
|
|
||||||
Long id = user.getId();
|
Long id = user.getId();
|
||||||
|
Long timestamp = identity.getTimestamp();
|
||||||
// 生成 loginToken
|
// 生成 loginToken
|
||||||
String loginKey = UserCacheKeyDefine.LOGIN_TOKEN.format(id, loginTime);
|
String loginKey = UserCacheKeyDefine.LOGIN_TOKEN.format(id, timestamp);
|
||||||
LoginTokenDTO loginValue = LoginTokenDTO.builder()
|
LoginTokenDTO loginValue = LoginTokenDTO.builder()
|
||||||
.id(id)
|
.id(id)
|
||||||
|
.username(user.getUsername())
|
||||||
.status(LoginTokenStatusEnum.OK.getStatus())
|
.status(LoginTokenStatusEnum.OK.getStatus())
|
||||||
.refreshCount(0)
|
.refreshCount(0)
|
||||||
.origin(new LoginTokenIdentityDTO(loginTime, remoteAddr, location, userAgent))
|
.origin(new RequestIdentityModel(timestamp, identity.getAddress(), identity.getLocation(), identity.getUserAgent()))
|
||||||
.build();
|
.build();
|
||||||
RedisStrings.setJson(loginKey, UserCacheKeyDefine.LOGIN_TOKEN, loginValue);
|
RedisStrings.setJson(loginKey, UserCacheKeyDefine.LOGIN_TOKEN, loginValue);
|
||||||
// 生成 refreshToken
|
// 生成 refreshToken
|
||||||
if (Booleans.isTrue(appLoginConfig.getAllowRefresh())) {
|
if (Booleans.isTrue(appLoginConfig.getAllowRefresh())) {
|
||||||
String refreshKey = UserCacheKeyDefine.LOGIN_REFRESH.format(id, loginTime);
|
String refreshKey = UserCacheKeyDefine.LOGIN_REFRESH.format(id, timestamp);
|
||||||
RedisStrings.setJson(refreshKey, UserCacheKeyDefine.LOGIN_REFRESH, loginValue);
|
RedisStrings.setJson(refreshKey, UserCacheKeyDefine.LOGIN_REFRESH, loginValue);
|
||||||
}
|
}
|
||||||
// 返回token
|
// 返回token
|
||||||
return AesEncryptUtils.encryptBase62(id + ":" + loginTime);
|
return AesEncryptUtils.encryptBase62(id + ":" + timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -459,6 +462,10 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
|||||||
UserCacheKeyDefine.LOGIN_TOKEN.setTimeout(loginSessionTime);
|
UserCacheKeyDefine.LOGIN_TOKEN.setTimeout(loginSessionTime);
|
||||||
UserCacheKeyDefine.LOGIN_REFRESH.setTimeout(loginSessionTime + appLoginConfig.getRefreshInterval());
|
UserCacheKeyDefine.LOGIN_REFRESH.setTimeout(loginSessionTime + appLoginConfig.getRefreshInterval());
|
||||||
}
|
}
|
||||||
|
Integer loginFailedLockTime = appLoginConfig.getLoginFailedLockTime();
|
||||||
|
if (loginFailedLockTime != null) {
|
||||||
|
UserCacheKeyDefine.LOGIN_FAILED.setTimeout(loginFailedLockTime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ public class SystemUserServiceImpl implements SystemUserService {
|
|||||||
// 用户列表
|
// 用户列表
|
||||||
UserCacheKeyDefine.USER_LIST.getKey(),
|
UserCacheKeyDefine.USER_LIST.getKey(),
|
||||||
// 登录失败次数
|
// 登录失败次数
|
||||||
UserCacheKeyDefine.LOGIN_FAILED_COUNT.format(request.getUsername())
|
UserCacheKeyDefine.LOGIN_FAILED.format(request.getUsername())
|
||||||
);
|
);
|
||||||
return record.getId();
|
return record.getId();
|
||||||
}
|
}
|
||||||
@@ -179,7 +179,7 @@ public class SystemUserServiceImpl implements SystemUserService {
|
|||||||
log.info("SystemUserService-updateUserStatus effect: {}, updateRecord: {}", effect, JSON.toJSONString(updateRecord));
|
log.info("SystemUserService-updateUserStatus effect: {}, updateRecord: {}", effect, JSON.toJSONString(updateRecord));
|
||||||
// 改为启用则删除登录失败次数缓存
|
// 改为启用则删除登录失败次数缓存
|
||||||
if (UserStatusEnum.ENABLED.equals(status)) {
|
if (UserStatusEnum.ENABLED.equals(status)) {
|
||||||
RedisUtils.delete(UserCacheKeyDefine.LOGIN_FAILED_COUNT.format(record.getUsername()));
|
RedisUtils.delete(UserCacheKeyDefine.LOGIN_FAILED.format(record.getUsername()));
|
||||||
}
|
}
|
||||||
// 更新用户缓存中的 status
|
// 更新用户缓存中的 status
|
||||||
RedisStrings.<LoginUser>processSetJson(UserCacheKeyDefine.USER_INFO, s -> {
|
RedisStrings.<LoginUser>processSetJson(UserCacheKeyDefine.USER_INFO, s -> {
|
||||||
@@ -320,7 +320,7 @@ public class SystemUserServiceImpl implements SystemUserService {
|
|||||||
int effect = systemUserDAO.updateById(update);
|
int effect = systemUserDAO.updateById(update);
|
||||||
log.info("SystemUserService-resetPassword record: {}, effect: {}", JSON.toJSONString(update), effect);
|
log.info("SystemUserService-resetPassword record: {}, effect: {}", JSON.toJSONString(update), effect);
|
||||||
// 删除登录失败次数缓存
|
// 删除登录失败次数缓存
|
||||||
RedisUtils.delete(UserCacheKeyDefine.LOGIN_FAILED_COUNT.format(record.getUsername()));
|
RedisUtils.delete(UserCacheKeyDefine.LOGIN_FAILED.format(record.getUsername()));
|
||||||
// 删除登录缓存
|
// 删除登录缓存
|
||||||
RedisUtils.scanKeysDelete(UserCacheKeyDefine.LOGIN_TOKEN.format(id, "*"));
|
RedisUtils.scanKeysDelete(UserCacheKeyDefine.LOGIN_TOKEN.format(id, "*"));
|
||||||
// 删除续签信息
|
// 删除续签信息
|
||||||
@@ -375,7 +375,7 @@ public class SystemUserServiceImpl implements SystemUserService {
|
|||||||
// 用户信息缓存
|
// 用户信息缓存
|
||||||
deleteKeys.add(UserCacheKeyDefine.USER_INFO.format(id));
|
deleteKeys.add(UserCacheKeyDefine.USER_INFO.format(id));
|
||||||
// 登录失败次数
|
// 登录失败次数
|
||||||
deleteKeys.add(UserCacheKeyDefine.LOGIN_FAILED_COUNT.format(s.getUsername()));
|
deleteKeys.add(UserCacheKeyDefine.LOGIN_FAILED.format(s.getUsername()));
|
||||||
// 登录 token
|
// 登录 token
|
||||||
deleteKeys.addAll(RedisUtils.scanKeys(UserCacheKeyDefine.LOGIN_TOKEN.format(id, "*")));
|
deleteKeys.addAll(RedisUtils.scanKeys(UserCacheKeyDefine.LOGIN_TOKEN.format(id, "*")));
|
||||||
// 刷新 token
|
// 刷新 token
|
||||||
|
|||||||
Reference in New Issue
Block a user