diff --git a/src/main/java/com/youlai/boot/common/constant/RedisConstants.java b/src/main/java/com/youlai/boot/common/constant/RedisConstants.java
index b753deaf..717a7614 100644
--- a/src/main/java/com/youlai/boot/common/constant/RedisConstants.java
+++ b/src/main/java/com/youlai/boot/common/constant/RedisConstants.java
@@ -26,10 +26,16 @@ public interface RedisConstants {
* 认证模块
*/
interface Auth {
-
- String ACCESS_TOKEN = "auth:token:access:{}"; // 访问Token
- String REFRESH_TOKEN = "auth:token:refresh:{}"; // 刷新Token
- String BLACKLIST_TOKEN = "auth:token:blacklist:{}"; // 黑名单Token
+ // 存储访问令牌对应的用户信息(accessToken -> OnlineUser)
+ String ACCESS_TOKEN_USER = "auth:token:access:{}";
+ // 存储刷新令牌对应的用户信息(refreshToken -> OnlineUser)
+ String REFRESH_TOKEN_USER = "auth:token:refresh:{}";
+ // 用户与访问令牌的映射(userId -> accessToken)
+ String USER_ACCESS_TOKEN = "auth:user:access:{}";
+ // 用户与刷新令牌的映射(userId -> refreshToken
+ String USER_REFRESH_TOKEN = "auth:user:refresh:{}";
+ // 黑名单 Token(用于退出登录或注销)
+ String BLACKLIST_TOKEN = "auth:token:blacklist:{}";
}
/**
diff --git a/src/main/java/com/youlai/boot/common/enums/TokenKeyEnum.java b/src/main/java/com/youlai/boot/common/enums/TokenKeyEnum.java
deleted file mode 100644
index d4aa10e6..00000000
--- a/src/main/java/com/youlai/boot/common/enums/TokenKeyEnum.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.youlai.boot.common.enums;
-
-import lombok.Getter;
-
-/**
- * @Description TODO
- * @Author wangtao
- * @Date 2025/2/27 14:48
- */
-@Getter
-public enum TokenKeyEnum {
- ACCESS_TOKEN_KEY("access_token:"),
- REFRESH_TOKEN_KEY ("refresh_token:");
-
- private final String value;
-
- TokenKeyEnum(String value) {
- this.value = value;
- }
-}
diff --git a/src/main/java/com/youlai/boot/config/SecurityConfig.java b/src/main/java/com/youlai/boot/config/SecurityConfig.java
index 395436a0..523eee07 100644
--- a/src/main/java/com/youlai/boot/config/SecurityConfig.java
+++ b/src/main/java/com/youlai/boot/config/SecurityConfig.java
@@ -11,7 +11,7 @@ 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.TokenAuthenticationFilter;
-import com.youlai.boot.core.security.manager.TokenManager;
+import com.youlai.boot.core.security.token.TokenManager;
import com.youlai.boot.core.security.service.SysUserDetailsService;
import com.youlai.boot.system.service.ConfigService;
import com.youlai.boot.system.service.UserService;
diff --git a/src/main/java/com/youlai/boot/config/property/SecurityProperties.java b/src/main/java/com/youlai/boot/config/property/SecurityProperties.java
index cadd6acc..665ccf0f 100644
--- a/src/main/java/com/youlai/boot/config/property/SecurityProperties.java
+++ b/src/main/java/com/youlai/boot/config/property/SecurityProperties.java
@@ -1,108 +1,111 @@
package com.youlai.boot.config.property;
-
import jakarta.validation.constraints.Min;
-import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
-import jakarta.validation.constraints.Size;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
-import java.util.ArrayList;
-import java.util.List;
-
/**
- * 安全配置属性
+ * 安全模块配置属性类
+ *
+ *
ignoreUrls = new ArrayList<>();
+ private SessionConfig session;
/**
- * 静态资源路径(不经过安全过滤器)
+ * 安全白名单路径(完全绕过安全过滤器)
+ * 示例值:/api/v1/auth/login/**, /ws/**
*/
- private List unsecuredUrls = new ArrayList<>();
+ @NotEmpty
+ private String[] ignoreUrls;
/**
- * 认证核心配置
+ * 非安全端点路径(允许匿名访问的API)
+ * 示例值:/doc.html, /v3/api-docs/**
*/
- private Auth auth = new Auth();
+ @NotEmpty
+ private String[] unsecuredUrls;
+ /**
+ * 会话配置嵌套类
+ */
@Data
- public static class Auth {
+ public static class SessionConfig {
/**
* 认证策略类型
+ *
+ * - jwt - 基于JWT的无状态认证
+ * - redis-token - 基于Redis的有状态认证
+ *
*/
@NotNull
- private AuthType type = AuthType.JWT;
+ private String type;
/**
- * 访问令牌有效期(秒)
+ * 访问令牌有效期(单位:秒)
+ * 默认值:3600(1小时)
+ * -1 表示永不过期
*/
@Min(-1)
- private int accessTokenTtl = 3600;
+ private Integer accessTokenTimeToLive = 3600;
/**
- * 刷新令牌有效期(秒)
+ * 刷新令牌有效期(单位:秒)
+ * 默认值:604800(7天)
+ * -1 表示永不过期
*/
@Min(-1)
- private int refreshTokenTtl = 604800;
+ private Integer refreshTokenTimeToLive = 604800;
/**
- * JWT 配置
+ * JWT 配置项
*/
- private JwtConfig jwtConfig = new JwtConfig();
+ private JwtConfig jwt;
/**
- * Redis Token 配置
+ * Redis令牌配置项
*/
- 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;
- }
+ private RedisTokenConfig redisToken;
}
/**
- * 认证策略类型枚举
+ * JWT 配置嵌套类
*/
- public enum AuthType {
- JWT, REDIS_TOKEN
+ @Data
+ public static class JwtConfig {
+ /**
+ * JWT签名密钥
+ * HS256算法要求至少32个字符
+ * 示例:SecretKey012345678901234567890123456789
+ */
+ @NotNull
+ private String secretKey;
}
/**
- * 会话控制策略枚举
+ * Redis令牌配置嵌套类
*/
- public enum SessionControlStrategy {
- REVOKE_OLDEST, DENY_NEW
+ @Data
+ public static class RedisTokenConfig {
+ /**
+ * 是否允许多设备同时登录
+ * true - 允许同一账户多设备登录(默认)
+ * false - 新登录会使旧令牌失效
+ */
+ private Boolean allowMultiLogin = true;
}
}
diff --git a/src/main/java/com/youlai/boot/core/security/filter/TokenAuthenticationFilter.java b/src/main/java/com/youlai/boot/core/security/filter/TokenAuthenticationFilter.java
index 037fe7ee..9cbbd0a6 100644
--- a/src/main/java/com/youlai/boot/core/security/filter/TokenAuthenticationFilter.java
+++ b/src/main/java/com/youlai/boot/core/security/filter/TokenAuthenticationFilter.java
@@ -4,7 +4,7 @@ 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.TokenManager;
+import com.youlai.boot.core.security.token.TokenManager;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
diff --git a/src/main/java/com/youlai/boot/core/security/manager/RedisTokenManager.java b/src/main/java/com/youlai/boot/core/security/manager/RedisTokenManager.java
deleted file mode 100644
index 80dce8f6..00000000
--- a/src/main/java/com/youlai/boot/core/security/manager/RedisTokenManager.java
+++ /dev/null
@@ -1,255 +0,0 @@
-package com.youlai.boot.core.security.manager;
-
-import cn.hutool.core.convert.Convert;
-import com.youlai.boot.common.enums.TokenKeyEnum;
-import com.youlai.boot.common.exception.BusinessException;
-import com.youlai.boot.common.result.ResultCode;
-import com.youlai.boot.config.property.SecurityProperties;
-import com.youlai.boot.core.security.model.AuthenticationToken;
-import com.youlai.boot.core.security.model.OnlineUser;
-import com.youlai.boot.core.security.model.SysUserDetails;
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.GrantedAuthority;
-import org.springframework.security.core.authority.SimpleGrantedAuthority;
-import org.springframework.stereotype.Service;
-
-import java.util.Objects;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
-
-/**
- * Redis Token 管理器
- *
- * 用于生成、解析、校验、刷新 JWT Token
- *
- * @author Ray.Hao
- * @since 2024/11/15
- */
-@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 redisTemplate;
-
- public RedisTokenManager(
- SecurityProperties securityProperties,
- RedisTemplate redisTemplate
- ) {
- this.securityProperties = securityProperties;
- this.redisTemplate = redisTemplate;
- }
-
- /**
- * 生成令牌
- *
- * @param authentication 用户认证信息
- * @return
- */
- @Override
- public AuthenticationToken generateToken(Authentication authentication) {
- int accessTokenTtl = securityProperties.getAuth().getAccessTokenTtl();
-
- // 创建新会话
- String accessToken = createNewSession(authentication, );
-
- // 创建刷新令牌(独立控制)
- String refreshToken = createNewSession(authentication, TokenType.REFRESH,
- config.getRefreshTokenTtl(), 1); // 刷新令牌强制单设备
-
- return AuthenticationToken.builder()
- .accessToken(accessToken)
- .refreshToken(refreshToken)
- .tokenType("Bearer")
- .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 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()
- );
- });
- }
- }
-
-
- /**
- * 解析令牌
- *
- * @param token JWT Token
- * @return
- */
- @Override
- public Authentication parseToken(String token) {
- String accessTokenKey = TokenKeyEnum.ACCESS_TOKEN_KEY.getValue() + token;
- OnlineUser user = (OnlineUser) redisTemplate.opsForValue().get(accessTokenKey);
- Set authorities = user.getAuthorities()
- .stream()
- .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority)))
- .collect(Collectors.toSet());
- SysUserDetails userDetails = new SysUserDetails();
- userDetails.setUserId(user.getId());
- userDetails.setUsername(user.getUsername());
- userDetails.setDeptId(user.getDeptId());
- userDetails.setDataScope(user.getDataScope());
- userDetails.setAuthorities(authorities);
- return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
- }
-
- /**
- * 验证令牌
- *
- * @param token JWT Token
- * @return
- */
- @Override
- public boolean validateToken(String token) {
- String accessTokenKey = TokenKeyEnum.ACCESS_TOKEN_KEY.getValue() + token;
- return redisTemplate.hasKey(accessTokenKey);
- }
-
- /**
- * 刷新令牌
- *
- * @param token 刷新令牌
- * @return
- */
- @Override
- public AuthenticationToken refreshToken(String token) {
- String refreshTokenKey = TokenKeyEnum.REFRESH_TOKEN_KEY.getValue() + token;
- Authentication authentication = (Authentication) redisTemplate.opsForValue().get(refreshTokenKey);
- if (authentication == null) {
- throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID);
- }
-
- int accessTokenExpiration = securityProperties.getAuth().getRefreshTokenTtl();
- // 生成新的访问令牌
- String newAccessToken = generateToken(authentication, TokenKeyEnum.ACCESS_TOKEN_KEY, accessTokenExpiration, true);
-
- return AuthenticationToken.builder()
- .accessToken(newAccessToken)
- .refreshToken(token)
- .expiresIn(accessTokenExpiration)
- .build();
- }
-
- /**
- * 创建令牌
- *
- * @param authentication 认证信息
- * @param tokenKeyEnum 令牌类型
- * @param ttl 有效期
- * @param multiLogin 是否允许多点登录
- * @return
- */
- private String generateToken(Authentication authentication, TokenKeyEnum tokenKeyEnum, int ttl, boolean multiLogin) {
- // 获取用户信息
- SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
- String token = UUID.randomUUID().toString();
- String tokenKey = tokenKeyEnum.getValue() + token;
- // 不允许多点登录,使用hashmap存储在线用户id和token
- if (!multiLogin) {
- // 查找当前用户id是否有token,有的话,说明已经登录了,就删除旧的token
- 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.delete(tokenKeyEnum.getValue() + oldToken);
- }
- redisTemplate.opsForHash().put("userId-token:" + tokenKeyEnum.getValue(), userDetails.getUserId().toString(), token);
- // 设置userId-token的过期时间
- redisTemplate.opsForHash().getOperations().expire("userId-token:" + tokenKeyEnum.getValue(), ttl, TimeUnit.SECONDS);
- }
-
- // 存储用户信息
- OnlineUser user = new OnlineUser();
- user.setId(userDetails.getUserId());
- user.setUsername(userDetails.getUsername());
- user.setDeptId(userDetails.getDeptId());
- user.setDataScope(userDetails.getDataScope());
-
- // claims 中添加角色信息
- Set roles = authentication.getAuthorities().stream()
- .map(GrantedAuthority::getAuthority)
- .collect(Collectors.toSet());
- user.setAuthorities(roles);
- redisTemplate.opsForValue().set(tokenKey, user, ttl, TimeUnit.SECONDS);
- return token;
- }
-
- /**
- * 清除redis中的用户信息
- *
- * @param token Redis Token
- */
- @Override
- public void blacklistToken(String token) {
- /**
- * 根据token,删除当前用户的accessToken和refreshToken,以及 userId-token 的hashmap
- */
- 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());
-
- 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());
- }
- }
-}
diff --git a/src/main/java/com/youlai/boot/core/security/model/OnlineUser.java b/src/main/java/com/youlai/boot/core/security/model/OnlineUser.java
index 8fe99b2c..d695c653 100644
--- a/src/main/java/com/youlai/boot/core/security/model/OnlineUser.java
+++ b/src/main/java/com/youlai/boot/core/security/model/OnlineUser.java
@@ -1,18 +1,45 @@
package com.youlai.boot.core.security.model;
+import lombok.AllArgsConstructor;
import lombok.Data;
+import lombok.NoArgsConstructor;
import java.util.Set;
/**
+ * 在线用户信息对象
+ *
* @author wangtao
* @since 2025/2/27 10:31
*/
@Data
-public class OnlineUser{
- private Long id;
- private Long deptId;
+@NoArgsConstructor
+@AllArgsConstructor
+public class OnlineUser {
+
+ /**
+ * 用户ID
+ */
+ private Long userId;
+
+ /**
+ * 用户名
+ */
private String username;
+
+ /**
+ * 部门ID
+ */
+ private Long deptId;
+
+ /**
+ * 数据权限范围
+ * 定义用户可访问的数据范围,如全部、本部门或自定义范围
+ */
private Integer dataScope;
+
+ /**
+ * 角色权限集合
+ */
private Set authorities;
}
diff --git a/src/main/java/com/youlai/boot/core/security/manager/JwtTokenManager.java b/src/main/java/com/youlai/boot/core/security/token/JwtTokenManager.java
similarity index 93%
rename from src/main/java/com/youlai/boot/core/security/manager/JwtTokenManager.java
rename to src/main/java/com/youlai/boot/core/security/token/JwtTokenManager.java
index 6a1b4af5..02b3b894 100644
--- a/src/main/java/com/youlai/boot/core/security/manager/JwtTokenManager.java
+++ b/src/main/java/com/youlai/boot/core/security/token/JwtTokenManager.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.core.security.manager;
+package com.youlai.boot.core.security.token;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
@@ -38,7 +38,7 @@ import java.util.stream.Collectors;
* @author Ray.Hao
* @since 2024/11/15
*/
-@ConditionalOnProperty(value = "security.auth.type", havingValue = "jwt")
+@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt")
@Service
public class JwtTokenManager implements TokenManager {
@@ -49,7 +49,7 @@ public class JwtTokenManager implements TokenManager {
public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) {
this.securityProperties = securityProperties;
this.redisTemplate = redisTemplate;
- this.secretKey = securityProperties.getAuth().getJwtConfig().getKey().getBytes();
+ this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes();
}
/**
@@ -60,8 +60,8 @@ public class JwtTokenManager implements TokenManager {
*/
@Override
public AuthenticationToken generateToken(Authentication authentication) {
- int accessTokenTimeToLive = securityProperties.getAuth().getAccessTokenTtl();
- int refreshTokenTimeToLive = securityProperties.getAuth().getRefreshTokenTtl();
+ int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive();
+ int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive();
String accessToken = generateToken(authentication, accessTokenTimeToLive);
String refreshToken = generateToken(authentication, refreshTokenTimeToLive);
@@ -175,7 +175,7 @@ public class JwtTokenManager implements TokenManager {
}
Authentication authentication = parseToken(refreshToken);
- int accessTokenExpiration = securityProperties.getAuth().getRefreshTokenTtl();
+ int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive();
String newAccessToken = generateToken(authentication, accessTokenExpiration);
return AuthenticationToken.builder()
diff --git a/src/main/java/com/youlai/boot/core/security/token/RedisTokenManager.java b/src/main/java/com/youlai/boot/core/security/token/RedisTokenManager.java
new file mode 100644
index 00000000..50c22c7e
--- /dev/null
+++ b/src/main/java/com/youlai/boot/core/security/token/RedisTokenManager.java
@@ -0,0 +1,229 @@
+package com.youlai.boot.core.security.token;
+
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import com.youlai.boot.common.constant.RedisConstants;
+import com.youlai.boot.common.exception.BusinessException;
+import com.youlai.boot.common.result.ResultCode;
+import com.youlai.boot.config.property.SecurityProperties;
+import com.youlai.boot.core.security.model.AuthenticationToken;
+import com.youlai.boot.core.security.model.OnlineUser;
+import com.youlai.boot.core.security.model.SysUserDetails;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.stereotype.Service;
+
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * Redis Token 管理器
+ *
+ * 用于生成、解析、校验、刷新 JWT Token
+ *
+ * @author Ray.Hao
+ * @since 2024/11/15
+ */
+@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token")
+@Service
+public class RedisTokenManager implements TokenManager {
+
+ // 安全配置属性
+ private final SecurityProperties securityProperties;
+ private final RedisTemplate redisTemplate;
+
+ public RedisTokenManager(SecurityProperties securityProperties,
+ RedisTemplate redisTemplate) {
+ this.securityProperties = securityProperties;
+ this.redisTemplate = redisTemplate;
+ }
+
+ @Override
+ public AuthenticationToken generateToken(Authentication authentication) {
+ SysUserDetails user = (SysUserDetails) authentication.getPrincipal();
+ int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive();
+ int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive();
+
+ // 生成随机令牌
+ String accessToken = IdUtil.fastSimpleUUID();
+ String refreshToken = IdUtil.fastSimpleUUID();
+
+ // 构建用户在线信息(不包含密码)
+ OnlineUser onlineUser = buildOnlineUser(user);
+
+ // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间
+ redisTemplate.opsForValue().set(
+ StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken),
+ onlineUser,
+ accessTtl,
+ TimeUnit.SECONDS
+ );
+ redisTemplate.opsForValue().set(
+ StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken),
+ onlineUser,
+ refreshTtl,
+ TimeUnit.SECONDS
+ );
+
+ // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌
+ Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin();
+ if (!allowMultiLogin) {
+ Long userId = user.getUserId();
+ String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId);
+ // 获取当前用户已有的访问令牌
+ String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey);
+ if (oldAccessToken != null) {
+ // 删除旧的访问令牌对应的用户信息缓存
+ redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken));
+ }
+ // 更新用户与访问令牌的映射
+ redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS);
+ }
+ // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话
+ String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId());
+ redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS);
+
+ return AuthenticationToken.builder()
+ .accessToken(accessToken)
+ .refreshToken(refreshToken)
+ .expiresIn(accessTtl)
+ .build();
+ }
+
+ /**
+ * 根据 token 解析用户信息
+ *
+ * @param token JWT Token
+ * @return
+ */
+ @Override
+ public Authentication parseToken(String token) {
+ // 根据访问令牌从 Redis 中获取在线用户信息
+ String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token);
+ OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey);
+
+ if (onlineUser == null) return null;
+
+ // 构建用户权限集合
+ Set authorities = onlineUser.getAuthorities().stream()
+ .map(SimpleGrantedAuthority::new)
+ .collect(Collectors.toSet());
+
+ // 构建用户详情对象
+ SysUserDetails userDetails = new SysUserDetails();
+ userDetails.setUserId(onlineUser.getUserId());
+ userDetails.setUsername(onlineUser.getUsername());
+ userDetails.setDeptId(onlineUser.getDeptId());
+ userDetails.setDataScope(onlineUser.getDataScope());
+ userDetails.setAuthorities(authorities);
+
+ return new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
+ }
+
+ /**
+ * 校验 Token 是否有效
+ *
+ * @param token 访问令牌
+ * @return
+ */
+ @Override
+ public boolean validateToken(String token) {
+ String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token);
+ return redisTemplate.hasKey(tokenKey);
+ }
+
+ /**
+ * 刷新令牌
+ *
+ * @param refreshToken 刷新令牌
+ * @return
+ */
+ @Override
+ public AuthenticationToken refreshToken(String refreshToken) {
+ // 根据刷新令牌获取在线用户信息
+ String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken);
+ OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey);
+
+ if (onlineUser == null) {
+ throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID);
+ }
+
+ // 获取当前用户的旧访问令牌(如果存在)
+ String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId());
+ String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey);
+ if (oldAccessToken != null) {
+ // 删除旧的访问令牌记录
+ redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken));
+ }
+
+ // 生成新访问令牌
+ String newAccessToken = IdUtil.fastSimpleUUID();
+ int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive();
+ redisTemplate.opsForValue().set(
+ StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken),
+ onlineUser,
+ accessTtl,
+ TimeUnit.SECONDS
+ );
+
+ // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话)
+ redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS);
+
+ return AuthenticationToken.builder()
+ .accessToken(newAccessToken)
+ .refreshToken(refreshToken)
+ .expiresIn(accessTtl)
+ .build();
+ }
+
+
+ /**
+ * 将 Token 加入黑名单
+ *
+ * @param token 访问令牌
+ */
+ @Override
+ public void blacklistToken(String token) {
+ // 删除访问令牌对应的在线用户信息缓存
+ String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token);
+ OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey);
+
+ if (onlineUser != null) {
+ Long userId = onlineUser.getUserId();
+
+ // 删除访问令牌缓存和用户与访问令牌的映射
+ redisTemplate.delete(accessKey);
+ redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId));
+
+ // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存
+ String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId);
+ String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey);
+ if (refreshToken != null) {
+ redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken));
+ redisTemplate.delete(userRefreshKey);
+ }
+ }
+ }
+
+ /**
+ * 构建 OnlineUser 对象
+ */
+ private OnlineUser buildOnlineUser(SysUserDetails user) {
+ Long userId = user.getUserId();
+ Set roles = user.getAuthorities().stream()
+ .map(GrantedAuthority::getAuthority)
+ .collect(Collectors.toSet());
+ return new OnlineUser(
+ userId,
+ user.getUsername(),
+ user.getDeptId(),
+ user.getDataScope(),
+ roles
+ );
+ }
+}
diff --git a/src/main/java/com/youlai/boot/core/security/manager/TokenManager.java b/src/main/java/com/youlai/boot/core/security/token/TokenManager.java
similarity index 94%
rename from src/main/java/com/youlai/boot/core/security/manager/TokenManager.java
rename to src/main/java/com/youlai/boot/core/security/token/TokenManager.java
index 0fd979f8..1fc090ac 100644
--- a/src/main/java/com/youlai/boot/core/security/manager/TokenManager.java
+++ b/src/main/java/com/youlai/boot/core/security/token/TokenManager.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.core.security.manager;
+package com.youlai.boot.core.security.token;
import com.youlai.boot.core.security.model.AuthenticationToken;
@@ -25,7 +25,7 @@ public interface TokenManager {
/**
* 解析 Token 获取认证信息
*
- * @param token JWT Token
+ * @param token Token
* @return 用户认证信息
*/
Authentication parseToken(String token);
diff --git a/src/main/java/com/youlai/boot/shared/auth/service/impl/AuthServiceImpl.java b/src/main/java/com/youlai/boot/shared/auth/service/impl/AuthServiceImpl.java
index 77138ce6..a2a09261 100644
--- a/src/main/java/com/youlai/boot/shared/auth/service/impl/AuthServiceImpl.java
+++ b/src/main/java/com/youlai/boot/shared/auth/service/impl/AuthServiceImpl.java
@@ -17,7 +17,7 @@ import com.youlai.boot.shared.auth.enums.CaptchaTypeEnum;
import com.youlai.boot.core.security.model.AuthenticationToken;
import com.youlai.boot.shared.auth.model.CaptchaInfo;
import com.youlai.boot.shared.auth.service.AuthService;
-import com.youlai.boot.core.security.manager.TokenManager;
+import com.youlai.boot.core.security.token.TokenManager;
import com.youlai.boot.shared.sms.enums.SmsTypeEnum;
import com.youlai.boot.shared.sms.service.SmsService;
import lombok.RequiredArgsConstructor;
diff --git a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java
index 781f79da..ab32f36d 100644
--- a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java
+++ b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java
@@ -12,7 +12,7 @@ import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SystemConstants;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.model.Option;
-import com.youlai.boot.core.security.manager.TokenManager;
+import com.youlai.boot.core.security.token.TokenManager;
import com.youlai.boot.core.security.service.PermissionService;
import com.youlai.boot.core.security.util.SecurityUtils;
import com.youlai.boot.shared.mail.service.MailService;
diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml
index 78318d0d..feb82393 100644
--- a/src/main/resources/application-dev.yml
+++ b/src/main/resources/application-dev.yml
@@ -72,25 +72,21 @@ mybatis-plus:
# 安全配置
security:
- # 认证策略核心配置
- auth:
- type: jwt # 认证策略 [jwt|redis-token]
- access-token-ttl: 3600 # 访问令牌 有效期(单位:秒),默认 1 小时,-1 表示永不过期
- refresh-token-ttl: 604800 # 刷新令牌有效期(单位:秒),默认 7 天,-1 表示永不过期
- # JWT 认证配置
+ session:
+ type: jwt # 会话方式 [jwt|redis-token]
+ access-token-time-to-live: 3600 # 访问令牌 有效期(单位:秒),默认 1 小时,-1 表示永不过期
+ refresh-token-time-to-live: 604800 # 刷新令牌有效期(单位:秒),默认 7 天,-1 表示永不过期
jwt:
- key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符,RS256至少2048位)
- # Redis Token 认证配置
+ secret-key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符)
redis-token:
- max-sessions: 1 # 最大允许多个设备同时登录的数量(默认为1)
- session-control: REVOKE_OLDEST # 会话超限处理策略 [REJECT|REVOKE_OLDEST|REVOKE_ALL]
- # 白名单路径
+ allow-multi-login: false # 是否允许多设备登录
+ # 安全白名单路径(完全绕过安全过滤器)
ignore-urls:
- /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录)
- /api/v1/auth/captcha # 验证码获取接口
- /api/v1/auth/refresh-token # 刷新令牌接口
- /ws/** # WebSocket接口
- # 不走 Spring Security 过滤器链的请求路径(一般是静态资源)
+ # 非安全端点路径(允许匿名访问的API)
unsecured-urls:
- ${springdoc.swagger-ui.path}
- /doc.html
diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml
index 834d55c6..a3c2e6b7 100644
--- a/src/main/resources/application-prod.yml
+++ b/src/main/resources/application-prod.yml
@@ -70,29 +70,21 @@ mybatis-plus:
# 安全配置
security:
- # 认证策略核心配置
- auth:
- # 认证策略 [jwt|redis-token]
- type: jwt
- # 访问令牌 有效期(单位:秒),默认 1 小时,-1 表示永不过期
- access-token-ttl: 3600
- # 刷新令牌有效期(单位:秒),默认 7 天,-1 表示永不过期
- refresh-token-ttl: 604800
- # JWT 认证配置
+ session:
+ type: jwt # 会话方式 [jwt|redis-token]
+ access-token-time-to-live: 3600 # 访问令牌 有效期(单位:秒),默认 1 小时,-1 表示永不过期
+ refresh-token-time-to-live: 604800 # 刷新令牌有效期(单位:秒),默认 7 天,-1 表示永不过期
jwt:
- key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符,RS256至少2048位)
- # Redis Token 认证配置
+ secret-key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符)
redis-token:
- max-sessions: 1 # 最大允许多个设备同时登录的数量(默认为1)
- session-control: REVOKE_OLDEST # 会话超限处理策略 [REJECT|REVOKE_OLDEST|REVOKE_ALL]
-
- # 无需认证的请求路径
+ allow-multi-login: true # 是否允许多设备登录
+ # 安全白名单路径(完全绕过安全过滤器)
ignore-urls:
- /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录)
- /api/v1/auth/captcha # 验证码获取接口
- /api/v1/auth/refresh-token # 刷新令牌接口
- /ws/** # WebSocket接口
- # 不走 Spring Security 过滤器链的请求路径(一般是静态资源)
+ # 非安全端点路径(允许匿名访问的API)
unsecured-urls:
- ${springdoc.swagger-ui.path}
- /doc.html