wip: 临时提交

This commit is contained in:
Ray.Hao
2025-03-07 21:39:51 +08:00
parent a84f2b9988
commit e01b784a97
13 changed files with 288 additions and 175 deletions

View File

@@ -14,16 +14,21 @@ public interface RedisConstants {
interface RateLimiter {
String IP = "rate_limiter:ip:{}"; // IP限流示例rate_limiter:ip:192.168.1.1
}
/**
* 分布式锁相关键
*/
interface Lock {
String RESUBMIT = "lock:resubmit:{}:{}"; // 防重复提交示例lock:resubmit:methodName:md5Hash
String RESUBMIT = "lock:resubmit:{}:{}"; // 防重复提交示例lock:resubmit:userIdentifier:requestIdentifier
}
/**
* 认证模块
*/
interface Auth {
String ACCESS_TOKEN = "auth:token:access:{}"; // 访问Token
String REFRESH_TOKEN = "auth:token:refresh:{}"; // 刷新Token
String BLACKLIST_TOKEN = "auth:token:blacklist:{}"; // 黑名单Token
}

View File

@@ -10,7 +10,7 @@ import com.youlai.boot.core.security.exception.MyAuthenticationEntryPoint;
import com.youlai.boot.core.security.extension.sms.SmsAuthenticationProvider;
import com.youlai.boot.core.security.extension.wechat.WechatAuthenticationProvider;
import com.youlai.boot.core.security.filter.CaptchaValidationFilter;
import com.youlai.boot.core.security.filter.TokenFilter;
import com.youlai.boot.core.security.filter.TokenAuthenticationFilter;
import com.youlai.boot.core.security.manager.TokenManager;
import com.youlai.boot.core.security.service.SysUserDetailsService;
import com.youlai.boot.system.service.ConfigService;
@@ -94,7 +94,7 @@ public class SecurityConfig {
// 验证码校验过滤器
.addFilterBefore(new CaptchaValidationFilter(redisTemplate, codeGenerator), UsernamePasswordAuthenticationFilter.class)
// 验证和解析过滤器
.addFilterBefore(new TokenFilter(tokenManager), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new TokenAuthenticationFilter(tokenManager), UsernamePasswordAuthenticationFilter.class)
.build();
}

View File

@@ -1,88 +1,108 @@
package com.youlai.boot.config.property;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.List;
/**
* 安全配置属性
*
* @author haoxr
* @author Ray.Hao
* @since 2024/4/18
*/
@Data
@Validated
@ConfigurationProperties(prefix = "security")
public class SecurityProperties {
/**
* 会话方式
* 免认证请求路径白名单
*/
private SessionProperty session;
private List<String> ignoreUrls = new ArrayList<>();
/**
* JWT 配置
* 静态资源路径(不经过安全过滤器)
*/
private JwtProperty jwt;
private List<String> unsecuredUrls = new ArrayList<>();
/**
* Redis-Token 配置
* 认证核心配置
*/
private RedisTokenProperty redisToken;
private Auth auth = new Auth();
/**
* 白名单 URL 集合
*/
private String[] ignoreUrls;
private String[] unsecuredUrls;
/**
* 会话属性
*/
@Data
public static class SessionProperty {
private String type;
public static class Auth {
/**
* 认证策略类型
*/
@NotNull
private AuthType type = AuthType.JWT;
/**
* 访问令牌有效期(秒)
*/
@Min(-1)
private int accessTokenTtl = 3600;
/**
* 刷新令牌有效期(秒)
*/
@Min(-1)
private int refreshTokenTtl = 604800;
/**
* JWT 配置
*/
private JwtConfig jwtConfig = new JwtConfig();
/**
* Redis Token 配置
*/
private RedisTokenConfig redisTokenConfig = new RedisTokenConfig();
@Data
public static class JwtConfig {
/**
* JWT 密钥
*/
@NotBlank
@Size(min = 32, message = "HS256算法密钥至少需要32字符")
private String key;
}
@Data
public static class RedisTokenConfig {
/**
* 最大并发会话数
*/
@Min(-1)
private int maxSessions = 1;
/**
* 会话超限处理策略
*/
private SessionControlStrategy sessionControl = SessionControlStrategy.REVOKE_OLDEST;
}
}
/**
* JWT 配置
* 认证策略类型枚举
*/
@Data
public static class JwtProperty {
/**
* JWT 密钥
*/
private String key;
/**
* 访问令牌有效期(单位:秒)
*/
private Integer accessTokenTimeToLive;
/**
* 刷新令牌有效期(单位:秒)
*/
private Integer refreshTokenTimeToLive;
public enum AuthType {
JWT, REDIS_TOKEN
}
@Data
public static class RedisTokenProperty {
/**
* 是否允许多点登录
*/
private Boolean multiLogin;
/**
* 访问令牌有效期(单位:秒)
*/
private Integer accessTokenTimeToLive;
/**
* 刷新令牌有效期(单位:秒)
*/
private Integer refreshTokenTimeToLive;
/**
* 会话控制策略枚举
*/
public enum SessionControlStrategy {
REVOKE_OLDEST, DENY_NEW
}
}

View File

@@ -2,14 +2,12 @@ package com.youlai.boot.core.aspect;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.jwt.JWTUtil;
import cn.hutool.jwt.RegisteredPayload;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.annotation.RepeatSubmit;
import com.youlai.boot.config.property.SecurityProperties;
import com.youlai.boot.common.util.IPUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -39,59 +37,66 @@ import java.util.concurrent.TimeUnit;
public class RepeatSubmitAspect {
private final RedissonClient redissonClient;
private final SecurityProperties securityProperties;
/**
* 防重复提交切点
*/
@Pointcut("@annotation(repeatSubmit)")
public void repeatSubmitPointCut(RepeatSubmit repeatSubmit) {
log.debug("定义防重复提交切点,注解:{}", repeatSubmit);
}
/**
* 环绕通知:处理防重复提交逻辑
*/
@Around("repeatSubmitPointCut(repeatSubmit)")
@Around(value = "repeatSubmitPointCut(repeatSubmit)", argNames = "pjp,repeatSubmit")
public Object handleRepeatSubmit(ProceedingJoinPoint pjp, RepeatSubmit repeatSubmit) throws Throwable {
String lockKey = buildLockKey();
if (lockKey == null) {
log.warn("无法生成防重复提交锁的 key跳过防重复提交逻辑");
return pjp.proceed();
}
int expire = repeatSubmit.expire(); // 防重提交锁过期时间
int expire = repeatSubmit.expire();
RLock lock = redissonClient.getLock(lockKey);
boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS);
log.info("获取防重复提交锁key{},是否成功:{}", lockKey, locked);
if (!locked) {
log.warn("重复提交请求,锁 key{}", lockKey);
throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST);
}
return pjp.proceed();
}
/**
* 生成防重复提交锁的 key
* @return 锁的 key
*/
private String buildLockKey() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
// 统一校验 Token 格式
if (StrUtil.isBlank(tokenHeader) || !tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX )) {
log.warn("请求头中未找到有效的 Token");
return null;
}
String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length());
String tokenHash = DigestUtil.sha256Hex(rawToken); // 建议替换为 SHA256 更安全
return RedisConstants.RESUBMIT_LOCK_PREFIX
+ tokenHash + ":"
+ request.getMethod() + "-"
+ request.getRequestURI();
// 用户唯一标识
String userIdentifier = getUserIdentifier(request);
// 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法)
String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI());
return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier);
}
/**
* 获取用户唯一标识
* 1. 从请求头中获取 Token使用 SHA-256 加密 Token 作为用户唯一标识
* 2. 如果 Token 为空,使用 IP 作为用户唯一标识
*
* @param request 请求对象
* @return 用户唯一标识
*/
private String getUserIdentifier(HttpServletRequest request) {
// 用户身份唯一标识
String userIdentifier;
// 从请求头中获取 Token
String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token
userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识
} else {
userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识
}
return userIdentifier;
}
}

View File

@@ -61,13 +61,14 @@ public class SmsAuthenticationProvider implements AuthenticationProvider {
}
// 校验发送短信验证码的手机号是否与当前登录用户一致
String cachedVerifyCode = (String) redisTemplate.opsForValue().get(RedisConstants.Captcha.SMS_LOGIN_CODE + mobile);
String cacheKey = StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile);
String cachedVerifyCode = (String) redisTemplate.opsForValue().get(cacheKey);
if (!StrUtil.equals(inputVerifyCode, cachedVerifyCode)) {
throw new CaptchaValidationException("验证码错误");
} else {
// 验证成功后删除验证码
redisTemplate.delete(RedisConstants.Captcha.SMS_LOGIN_CODE + mobile);
redisTemplate.delete(cacheKey);
}
// 构建认证后的用户详情信息

View File

@@ -4,7 +4,6 @@ import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.util.ResponseUtils;
import com.youlai.boot.core.security.manager.RedisTokenManager;
import com.youlai.boot.core.security.manager.TokenManager;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
@@ -18,39 +17,57 @@ import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* Token 认证校验过滤器
*
* @author wangtao
* @since 2025/3/6 16:50
*/
public class TokenFilter extends OncePerRequestFilter {
public class TokenAuthenticationFilter extends OncePerRequestFilter {
/**
* Token 管理器
*/
private final TokenManager tokenManager;
public TokenFilter(TokenManager tokenManager) {
public TokenAuthenticationFilter(TokenManager tokenManager) {
this.tokenManager = tokenManager;
}
/**
* 校验 Token 包括验签和是否过期
* 如果 Token 有效 Token 解析为 Authentication 对象并设置到 Spring Security 上下文中
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
try {
if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX )) {
// 去除 Bearer 前缀
token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length());
// 校验 JWT Token 包括验签和是否过期
boolean isValidate = tokenManager.validateToken(token);
if (!isValidate) {
if (StrUtil.isNotBlank(authorizationHeader)
&& authorizationHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
// 剥离Bearer前缀获取原始令牌
String rawToken = authorizationHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
// 执行令牌有效性检查包含密码学验签和过期时间验证
boolean isValidToken = tokenManager.validateToken(rawToken);
if (!isValidToken) {
ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_TOKEN_INVALID);
return;
}
// Token 解析为 Authentication 对象并设置到 Spring Security 上下文中
Authentication authentication = tokenManager.parseToken(token);
// 将令牌解析为Spring Security认证对象
Authentication authentication = tokenManager.parseToken(rawToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
} catch (Exception ex) {
// 安全上下文清除保障防止上下文残留
SecurityContextHolder.clearContext();
ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_TOKEN_INVALID);
return;
}
// Token有效或无Token时继续执行过滤链
// 继续后续过滤器链执行
filterChain.doFilter(request, response);
}
}

View File

@@ -31,12 +31,14 @@ import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* JWT 令牌服务实现
* JWT Token 管理器
* <p>
* 用于生成、解析、校验、刷新 JWT Token
*
* @author Ray.Hao
* @since 2024/11/15
*/
@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt")
@ConditionalOnProperty(value = "security.auth.type", havingValue = "jwt")
@Service
public class JwtTokenManager implements TokenManager {
@@ -44,11 +46,10 @@ public class JwtTokenManager implements TokenManager {
private final RedisTemplate<String, Object> redisTemplate;
private final byte[] secretKey;
public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate<String, Object> redisTemplate) {
this.securityProperties = securityProperties;
this.redisTemplate = redisTemplate;
this.secretKey = securityProperties.getJwt().getKey().getBytes();
this.secretKey = securityProperties.getAuth().getJwtConfig().getKey().getBytes();
}
/**
@@ -59,8 +60,8 @@ public class JwtTokenManager implements TokenManager {
*/
@Override
public AuthenticationToken generateToken(Authentication authentication) {
int accessTokenTimeToLive = securityProperties.getJwt().getAccessTokenTimeToLive();
int refreshTokenTimeToLive = securityProperties.getJwt().getRefreshTokenTimeToLive();
int accessTokenTimeToLive = securityProperties.getAuth().getAccessTokenTtl();
int refreshTokenTimeToLive = securityProperties.getAuth().getRefreshTokenTtl();
String accessToken = generateToken(authentication, accessTokenTimeToLive);
String refreshToken = generateToken(authentication, refreshTokenTimeToLive);
@@ -131,14 +132,18 @@ public class JwtTokenManager implements TokenManager {
*/
@Override
public void blacklistToken(String token) {
if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX )) {
token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length());
if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
}
JWT jwt = JWTUtil.parseToken(token);
JSONObject payloads = jwt.getPayloads();
String jti = payloads.getStr(JWTPayload.JWT_ID);
Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT);
// 黑名单Token Key
String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID);
if (expirationAt != null) {
int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000);
if (expirationAt < currentTimeSeconds) {
@@ -147,22 +152,20 @@ public class JwtTokenManager implements TokenManager {
}
// 计算Token剩余时间将其加入黑名单
int expirationIn = expirationAt - currentTimeSeconds;
redisTemplate.opsForValue().set(RedisConstants.Auth.BLACKLIST_TOKEN + jti, null, expirationIn, TimeUnit.SECONDS);
redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS);
} else {
// 永不过期的Token永久加入黑名单
redisTemplate.opsForValue().set(RedisConstants.Auth.BLACKLIST_TOKEN + jti, null);
redisTemplate.opsForValue().set(blacklistTokenKey, null);
}
;
}
/**
* 刷新令牌
*
* @param refreshToken 刷新令牌
* @return 令牌响应对象
*/
@Override
public AuthenticationToken refreshToken(String refreshToken) {
@@ -172,7 +175,7 @@ public class JwtTokenManager implements TokenManager {
}
Authentication authentication = parseToken(refreshToken);
int accessTokenExpiration = securityProperties.getJwt().getRefreshTokenTimeToLive();
int accessTokenExpiration = securityProperties.getAuth().getRefreshTokenTtl();
String newAccessToken = generateToken(authentication, accessTokenExpiration);
return AuthenticationToken.builder()
@@ -183,7 +186,6 @@ public class JwtTokenManager implements TokenManager {
.build();
}
/**
* 生成 JWT Token
*

View File

@@ -24,19 +24,29 @@ import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* JWT 令牌服务实现
* Redis Token 管理器
* <p>
* 用于生成、解析、校验、刷新 JWT Token
*
* @author Ray.Hao
* @since 2024/11/15
*/
@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token")
@ConditionalOnProperty(value = "security.auth.type", havingValue = "redis-token")
@Service
public class RedisTokenManager implements TokenManager {
// 常量定义
private static final String USER_SESSION_MAP = "user_sessions:%s"; // %s=token_type
private static final String SESSION_KEY = "session:%s:%s"; // %s=token_type,token
private static final String SESSION_QUEUE = "session_queue:%s"; // %s=user_id
private final SecurityProperties securityProperties;
private final RedisTemplate<String, Object> redisTemplate;
public RedisTokenManager(SecurityProperties securityProperties, RedisTemplate<String, Object> redisTemplate) {
public RedisTokenManager(
SecurityProperties securityProperties,
RedisTemplate<String, Object> redisTemplate
) {
this.securityProperties = securityProperties;
this.redisTemplate = redisTemplate;
}
@@ -49,21 +59,74 @@ public class RedisTokenManager implements TokenManager {
*/
@Override
public AuthenticationToken generateToken(Authentication authentication) {
int accessTokenTimeToLive = securityProperties.getRedisToken().getAccessTokenTimeToLive();
int refreshTokenTimeToLive = securityProperties.getRedisToken().getRefreshTokenTimeToLive();
Boolean multiLogin = securityProperties.getRedisToken().getMultiLogin();
int accessTokenTtl = securityProperties.getAuth().getAccessTokenTtl();
String accessToken = generateToken(authentication, TokenKeyEnum.ACCESS_TOKEN_KEY, accessTokenTimeToLive, multiLogin);
String refreshToken = generateToken(authentication, TokenKeyEnum.REFRESH_TOKEN_KEY, refreshTokenTimeToLive, multiLogin);
// 创建新会话
String accessToken = createNewSession(authentication, );
// 创建刷新令牌(独立控制)
String refreshToken = createNewSession(authentication, TokenType.REFRESH,
config.getRefreshTokenTtl(), 1); // 刷新令牌强制单设备
return AuthenticationToken.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.tokenType("Bearer")
.expiresIn(accessTokenTimeToLive)
.expiresIn(accessTokenTtl)
.build();
}
private String createNewSession(Authentication authentication,
TokenType tokenType,
int ttl,
int maxSessions) {
SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
Long userId = userDetails.getUserId();
String token = UUID.randomUUID().toString();
// 会话存储
String sessionKey = keyGenerator.getSessionKey(tokenType, token);
redisTemplate.opsForValue().set(sessionKey, buildOnlineUser(userDetails), ttl, TimeUnit.SECONDS);
// 用户-会话映射
String userMapKey = keyGenerator.getUserSessionMapKey(tokenType);
redisTemplate.opsForHash().put(userMapKey, userId.toString(), token);
redisTemplate.expire(userMapKey, ttl, TimeUnit.SECONDS);
// 多设备控制
enforceMaxSessions(userId, token, tokenType, maxSessions);
return token;
}
private void enforceMaxSessions(Long userId, String currentToken, TokenType tokenType, int maxSessions) {
if (maxSessions <= 0) return;
String sessionQueueKey = keyGenerator.getSessionQueueKey(userId);
long now = System.currentTimeMillis();
// 使用ZSet维护会话队列
redisTemplate.opsForZSet().add(sessionQueueKey, currentToken, now);
redisTemplate.expire(sessionQueueKey, 7, TimeUnit.DAYS);
// 移除超出数量的旧会话
long excess = redisTemplate.opsForZSet().size(sessionQueueKey) - maxSessions;
if (excess > 0) {
Set<String> oldTokens = redisTemplate.opsForZSet().range(sessionQueueKey, 0, excess - 1);
redisTemplate.opsForZSet().removeRange(sessionQueueKey, 0, excess - 1);
// 吊销旧令牌
oldTokens.forEach(oldToken -> {
redisTemplate.delete(keyGenerator.getSessionKey(tokenType, oldToken));
redisTemplate.opsForHash().delete(
keyGenerator.getUserSessionMapKey(tokenType),
userId.toString()
);
});
}
}
/**
* 解析令牌
*
@@ -84,7 +147,6 @@ public class RedisTokenManager implements TokenManager {
userDetails.setDeptId(user.getDeptId());
userDetails.setDataScope(user.getDataScope());
userDetails.setAuthorities(authorities);
return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
}
@@ -97,8 +159,7 @@ public class RedisTokenManager implements TokenManager {
@Override
public boolean validateToken(String token) {
String accessTokenKey = TokenKeyEnum.ACCESS_TOKEN_KEY.getValue() + token;
Boolean hasKey = redisTemplate.hasKey(accessTokenKey);
return hasKey != null && hasKey;
return redisTemplate.hasKey(accessTokenKey);
}
/**
@@ -115,7 +176,7 @@ public class RedisTokenManager implements TokenManager {
throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID);
}
int accessTokenExpiration = securityProperties.getRedisToken().getRefreshTokenTimeToLive();
int accessTokenExpiration = securityProperties.getAuth().getRefreshTokenTtl();
// 生成新的访问令牌
String newAccessToken = generateToken(authentication, TokenKeyEnum.ACCESS_TOKEN_KEY, accessTokenExpiration, true);
@@ -143,14 +204,14 @@ public class RedisTokenManager implements TokenManager {
// 不允许多点登录使用hashmap存储在线用户id和token
if (!multiLogin) {
// 查找当前用户id是否有token有的话说明已经登录了就删除旧的token
String oldToken = (String) redisTemplate.opsForHash().get("userId-token:"+tokenKeyEnum.getValue(), userDetails.getUserId().toString());
String oldToken = (String) redisTemplate.opsForHash().get("userId-token:" + tokenKeyEnum.getValue(), userDetails.getUserId().toString());
if (StringUtils.isNotBlank(oldToken)) {
redisTemplate.opsForHash().delete("userId-token:"+tokenKeyEnum.getValue(), userDetails.getUserId().toString());
redisTemplate.opsForHash().delete("userId-token:" + tokenKeyEnum.getValue(), userDetails.getUserId().toString());
redisTemplate.delete(tokenKeyEnum.getValue() + oldToken);
}
redisTemplate.opsForHash().put("userId-token:"+tokenKeyEnum.getValue(), userDetails.getUserId().toString(), token);
redisTemplate.opsForHash().put("userId-token:" + tokenKeyEnum.getValue(), userDetails.getUserId().toString(), token);
// 设置userId-token的过期时间
redisTemplate.opsForHash().getOperations().expire("userId-token:"+tokenKeyEnum.getValue(), ttl, TimeUnit.SECONDS);
redisTemplate.opsForHash().getOperations().expire("userId-token:" + tokenKeyEnum.getValue(), ttl, TimeUnit.SECONDS);
}
// 存储用户信息
@@ -182,13 +243,13 @@ public class RedisTokenManager implements TokenManager {
OnlineUser user = (OnlineUser) redisTemplate.opsForValue().get(TokenKeyEnum.ACCESS_TOKEN_KEY.getValue() + token);
if (!Objects.isNull(user)) {
Long userId = user.getId();
String refreshToken = (String) redisTemplate.opsForHash().get("userId-token:"+TokenKeyEnum.REFRESH_TOKEN_KEY.getValue(), user.getId().toString());
String refreshToken = (String) redisTemplate.opsForHash().get("userId-token:" + TokenKeyEnum.REFRESH_TOKEN_KEY.getValue(), user.getId().toString());
redisTemplate.delete(TokenKeyEnum.ACCESS_TOKEN_KEY.getValue() + token);
redisTemplate.delete(TokenKeyEnum.REFRESH_TOKEN_KEY.getValue() + refreshToken);
// 删除 userId-token 的hashmap
redisTemplate.opsForHash().delete("userId-token:"+TokenKeyEnum.ACCESS_TOKEN_KEY.getValue(), userId.toString());
redisTemplate.opsForHash().delete("userId-token:"+TokenKeyEnum.REFRESH_TOKEN_KEY.getValue(), userId.toString());
redisTemplate.opsForHash().delete("userId-token:" + TokenKeyEnum.ACCESS_TOKEN_KEY.getValue(), userId.toString());
redisTemplate.opsForHash().delete("userId-token:" + TokenKeyEnum.REFRESH_TOKEN_KEY.getValue(), userId.toString());
}
}
}

View File

@@ -5,9 +5,11 @@ import com.youlai.boot.core.security.model.AuthenticationToken;
import org.springframework.security.core.Authentication;
/**
* 令牌接口
* Token 管理器
* <p>
* 用于生成、解析、校验、刷新 Token
*
* @author Ray
* @author Ray.Hao
* @since 2.16.0
*/
public interface TokenManager {
@@ -57,5 +59,4 @@ public interface TokenManager {
}
}

View File

@@ -43,7 +43,7 @@ public class SysUserDetails implements UserDetails {
private String password;
/**
* 账号是否启用true启用false禁用
* 账号是否启用(true:启用 false:禁用)
*/
private Boolean enabled;

View File

@@ -422,7 +422,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
boolean result = smsService.sendSms(mobile, SmsTypeEnum.CHANGE_MOBILE, templateParams);
if (result) {
// 缓存验证码5分钟有效用于更换手机号校验
String redisCacheKey =StrUtil.format(RedisConstants.Captcha.MOBILE_CODE, mobile);
String redisCacheKey = StrUtil.format(RedisConstants.Captcha.MOBILE_CODE, mobile);
redisTemplate.opsForValue().set(redisCacheKey, code, 5, TimeUnit.MINUTES);
}
return result;
@@ -448,8 +448,9 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
String inputVerifyCode = form.getCode();
String mobile = form.getMobile();
String redisCacheKey = RedisConstants.Captcha.MOBILE_CODE + mobile;
String cachedVerifyCode = redisTemplate.opsForValue().get(redisCacheKey);
String cacheKey = StrUtil.format(RedisConstants.Captcha.MOBILE_CODE, mobile);
String cachedVerifyCode = redisTemplate.opsForValue().get(cacheKey);
if (StrUtil.isBlank(cachedVerifyCode)) {
throw new BusinessException("验证码已过期");
@@ -458,7 +459,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
throw new BusinessException("验证码错误");
}
// 验证完成删除验证码
redisTemplate.delete(redisCacheKey);
redisTemplate.delete(cacheKey);
// 更新手机号码
return this.update(
@@ -482,7 +483,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
mailService.sendMail(email, "邮箱验证码", "您的验证码为:" + code + "请在5分钟内使用");
// 缓存验证码5分钟有效用于更换邮箱校验
String redisCacheKey = StrUtil.format(RedisConstants.Captcha.EMAIL_CODE, email);
String redisCacheKey = StrUtil.format(RedisConstants.Captcha.EMAIL_CODE, email);
redisTemplate.opsForValue().set(redisCacheKey, code, 5, TimeUnit.MINUTES);
}