refactor: 优化 redis-token 会话管理和单一会话控制
This commit is contained in:
@@ -26,10 +26,16 @@ public interface RedisConstants {
|
|||||||
* 认证模块
|
* 认证模块
|
||||||
*/
|
*/
|
||||||
interface Auth {
|
interface Auth {
|
||||||
|
// 存储访问令牌对应的用户信息(accessToken -> OnlineUser)
|
||||||
String ACCESS_TOKEN = "auth:token:access:{}"; // 访问Token
|
String ACCESS_TOKEN_USER = "auth:token:access:{}";
|
||||||
String REFRESH_TOKEN = "auth:token:refresh:{}"; // 刷新Token
|
// 存储刷新令牌对应的用户信息(refreshToken -> OnlineUser)
|
||||||
String BLACKLIST_TOKEN = "auth:token:blacklist:{}"; // 黑名单Token
|
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:{}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.extension.wechat.WechatAuthenticationProvider;
|
||||||
import com.youlai.boot.core.security.filter.CaptchaValidationFilter;
|
import com.youlai.boot.core.security.filter.CaptchaValidationFilter;
|
||||||
import com.youlai.boot.core.security.filter.TokenAuthenticationFilter;
|
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.core.security.service.SysUserDetailsService;
|
||||||
import com.youlai.boot.system.service.ConfigService;
|
import com.youlai.boot.system.service.ConfigService;
|
||||||
import com.youlai.boot.system.service.UserService;
|
import com.youlai.boot.system.service.UserService;
|
||||||
|
|||||||
@@ -1,108 +1,111 @@
|
|||||||
package com.youlai.boot.config.property;
|
package com.youlai.boot.config.property;
|
||||||
|
|
||||||
import jakarta.validation.constraints.Min;
|
import jakarta.validation.constraints.Min;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Size;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 安全配置属性
|
* 安全模块配置属性类
|
||||||
|
*
|
||||||
|
* <p>映射 application.yml 中 security 前缀的安全相关配置</p>
|
||||||
*
|
*
|
||||||
* @author Ray.Hao
|
* @author Ray.Hao
|
||||||
* @since 2024/4/18
|
* @since 2024/4/18
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
|
@Component
|
||||||
@Validated
|
@Validated
|
||||||
@ConfigurationProperties(prefix = "security")
|
@ConfigurationProperties(prefix = "security")
|
||||||
public class SecurityProperties {
|
public class SecurityProperties {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 免认证请求路径白名单
|
* 会话管理配置
|
||||||
*/
|
*/
|
||||||
private List<String> ignoreUrls = new ArrayList<>();
|
private SessionConfig session;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 静态资源路径(不经过安全过滤器)
|
* 安全白名单路径(完全绕过安全过滤器)
|
||||||
|
* <p>示例值:/api/v1/auth/login/**, /ws/**
|
||||||
*/
|
*/
|
||||||
private List<String> unsecuredUrls = new ArrayList<>();
|
@NotEmpty
|
||||||
|
private String[] ignoreUrls;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 认证核心配置
|
* 非安全端点路径(允许匿名访问的API)
|
||||||
|
* <p>示例值:/doc.html, /v3/api-docs/**
|
||||||
*/
|
*/
|
||||||
private Auth auth = new Auth();
|
@NotEmpty
|
||||||
|
private String[] unsecuredUrls;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话配置嵌套类
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
public static class Auth {
|
public static class SessionConfig {
|
||||||
/**
|
/**
|
||||||
* 认证策略类型
|
* 认证策略类型
|
||||||
|
* <ul>
|
||||||
|
* <li>jwt - 基于JWT的无状态认证</li>
|
||||||
|
* <li>redis-token - 基于Redis的有状态认证</li>
|
||||||
|
* </ul>
|
||||||
*/
|
*/
|
||||||
@NotNull
|
@NotNull
|
||||||
private AuthType type = AuthType.JWT;
|
private String type;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 访问令牌有效期(秒)
|
* 访问令牌有效期(单位:秒)
|
||||||
|
* <p>默认值:3600(1小时)</p>
|
||||||
|
* <p>-1 表示永不过期</p>
|
||||||
*/
|
*/
|
||||||
@Min(-1)
|
@Min(-1)
|
||||||
private int accessTokenTtl = 3600;
|
private Integer accessTokenTimeToLive = 3600;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新令牌有效期(秒)
|
* 刷新令牌有效期(单位:秒)
|
||||||
|
* <p>默认值:604800(7天)</p>
|
||||||
|
* <p>-1 表示永不过期</p>
|
||||||
*/
|
*/
|
||||||
@Min(-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();
|
private RedisTokenConfig redisToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 配置嵌套类
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
public static class JwtConfig {
|
public static class JwtConfig {
|
||||||
/**
|
/**
|
||||||
* JWT 密钥
|
* JWT签名密钥
|
||||||
|
* <p>HS256算法要求至少32个字符</p>
|
||||||
|
* <p>示例:SecretKey012345678901234567890123456789</p>
|
||||||
*/
|
*/
|
||||||
@NotBlank
|
@NotNull
|
||||||
@Size(min = 32, message = "HS256算法密钥至少需要32字符")
|
private String secretKey;
|
||||||
private String key;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis令牌配置嵌套类
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
public static class RedisTokenConfig {
|
public static class RedisTokenConfig {
|
||||||
/**
|
/**
|
||||||
* 最大并发会话数
|
* 是否允许多设备同时登录
|
||||||
|
* <p>true - 允许同一账户多设备登录(默认)</p>
|
||||||
|
* <p>false - 新登录会使旧令牌失效</p>
|
||||||
*/
|
*/
|
||||||
@Min(-1)
|
private Boolean allowMultiLogin = true;
|
||||||
private int maxSessions = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 会话超限处理策略
|
|
||||||
*/
|
|
||||||
private SessionControlStrategy sessionControl = SessionControlStrategy.REVOKE_OLDEST;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 认证策略类型枚举
|
|
||||||
*/
|
|
||||||
public enum AuthType {
|
|
||||||
JWT, REDIS_TOKEN
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 会话控制策略枚举
|
|
||||||
*/
|
|
||||||
public enum SessionControlStrategy {
|
|
||||||
REVOKE_OLDEST, DENY_NEW
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import cn.hutool.core.util.StrUtil;
|
|||||||
import com.youlai.boot.common.constant.SecurityConstants;
|
import com.youlai.boot.common.constant.SecurityConstants;
|
||||||
import com.youlai.boot.common.result.ResultCode;
|
import com.youlai.boot.common.result.ResultCode;
|
||||||
import com.youlai.boot.common.util.ResponseUtils;
|
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.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|||||||
@@ -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 管理器
|
|
||||||
* <p>
|
|
||||||
* 用于生成、解析、校验、刷新 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<String, Object> redisTemplate;
|
|
||||||
|
|
||||||
public RedisTokenManager(
|
|
||||||
SecurityProperties securityProperties,
|
|
||||||
RedisTemplate<String, Object> 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<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()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析令牌
|
|
||||||
*
|
|
||||||
* @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<SimpleGrantedAuthority> 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<String> 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,45 @@
|
|||||||
package com.youlai.boot.core.security.model;
|
package com.youlai.boot.core.security.model;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* 在线用户信息对象
|
||||||
|
*
|
||||||
* @author wangtao
|
* @author wangtao
|
||||||
* @since 2025/2/27 10:31
|
* @since 2025/2/27 10:31
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class OnlineUser{
|
@NoArgsConstructor
|
||||||
private Long id;
|
@AllArgsConstructor
|
||||||
private Long deptId;
|
public class OnlineUser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户ID
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
*/
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门ID
|
||||||
|
*/
|
||||||
|
private Long deptId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据权限范围
|
||||||
|
* <p>定义用户可访问的数据范围,如全部、本部门或自定义范围</p>
|
||||||
|
*/
|
||||||
private Integer dataScope;
|
private Integer dataScope;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色权限集合
|
||||||
|
*/
|
||||||
private Set<String> authorities;
|
private Set<String> authorities;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.convert.Convert;
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
@@ -38,7 +38,7 @@ import java.util.stream.Collectors;
|
|||||||
* @author Ray.Hao
|
* @author Ray.Hao
|
||||||
* @since 2024/11/15
|
* @since 2024/11/15
|
||||||
*/
|
*/
|
||||||
@ConditionalOnProperty(value = "security.auth.type", havingValue = "jwt")
|
@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt")
|
||||||
@Service
|
@Service
|
||||||
public class JwtTokenManager implements TokenManager {
|
public class JwtTokenManager implements TokenManager {
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ public class JwtTokenManager implements TokenManager {
|
|||||||
public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate<String, Object> redisTemplate) {
|
public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate<String, Object> redisTemplate) {
|
||||||
this.securityProperties = securityProperties;
|
this.securityProperties = securityProperties;
|
||||||
this.redisTemplate = redisTemplate;
|
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
|
@Override
|
||||||
public AuthenticationToken generateToken(Authentication authentication) {
|
public AuthenticationToken generateToken(Authentication authentication) {
|
||||||
int accessTokenTimeToLive = securityProperties.getAuth().getAccessTokenTtl();
|
int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive();
|
||||||
int refreshTokenTimeToLive = securityProperties.getAuth().getRefreshTokenTtl();
|
int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive();
|
||||||
|
|
||||||
String accessToken = generateToken(authentication, accessTokenTimeToLive);
|
String accessToken = generateToken(authentication, accessTokenTimeToLive);
|
||||||
String refreshToken = generateToken(authentication, refreshTokenTimeToLive);
|
String refreshToken = generateToken(authentication, refreshTokenTimeToLive);
|
||||||
@@ -175,7 +175,7 @@ public class JwtTokenManager implements TokenManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Authentication authentication = parseToken(refreshToken);
|
Authentication authentication = parseToken(refreshToken);
|
||||||
int accessTokenExpiration = securityProperties.getAuth().getRefreshTokenTtl();
|
int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive();
|
||||||
String newAccessToken = generateToken(authentication, accessTokenExpiration);
|
String newAccessToken = generateToken(authentication, accessTokenExpiration);
|
||||||
|
|
||||||
return AuthenticationToken.builder()
|
return AuthenticationToken.builder()
|
||||||
@@ -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 管理器
|
||||||
|
* <p>
|
||||||
|
* 用于生成、解析、校验、刷新 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<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
public RedisTokenManager(SecurityProperties securityProperties,
|
||||||
|
RedisTemplate<String, Object> 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<SimpleGrantedAuthority> 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<String> roles = user.getAuthorities().stream()
|
||||||
|
.map(GrantedAuthority::getAuthority)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
return new OnlineUser(
|
||||||
|
userId,
|
||||||
|
user.getUsername(),
|
||||||
|
user.getDeptId(),
|
||||||
|
user.getDataScope(),
|
||||||
|
roles
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
import com.youlai.boot.core.security.model.AuthenticationToken;
|
||||||
@@ -25,7 +25,7 @@ public interface TokenManager {
|
|||||||
/**
|
/**
|
||||||
* 解析 Token 获取认证信息
|
* 解析 Token 获取认证信息
|
||||||
*
|
*
|
||||||
* @param token JWT Token
|
* @param token Token
|
||||||
* @return 用户认证信息
|
* @return 用户认证信息
|
||||||
*/
|
*/
|
||||||
Authentication parseToken(String token);
|
Authentication parseToken(String token);
|
||||||
@@ -17,7 +17,7 @@ import com.youlai.boot.shared.auth.enums.CaptchaTypeEnum;
|
|||||||
import com.youlai.boot.core.security.model.AuthenticationToken;
|
import com.youlai.boot.core.security.model.AuthenticationToken;
|
||||||
import com.youlai.boot.shared.auth.model.CaptchaInfo;
|
import com.youlai.boot.shared.auth.model.CaptchaInfo;
|
||||||
import com.youlai.boot.shared.auth.service.AuthService;
|
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.enums.SmsTypeEnum;
|
||||||
import com.youlai.boot.shared.sms.service.SmsService;
|
import com.youlai.boot.shared.sms.service.SmsService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import com.youlai.boot.common.constant.RedisConstants;
|
|||||||
import com.youlai.boot.common.constant.SystemConstants;
|
import com.youlai.boot.common.constant.SystemConstants;
|
||||||
import com.youlai.boot.common.exception.BusinessException;
|
import com.youlai.boot.common.exception.BusinessException;
|
||||||
import com.youlai.boot.common.model.Option;
|
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.service.PermissionService;
|
||||||
import com.youlai.boot.core.security.util.SecurityUtils;
|
import com.youlai.boot.core.security.util.SecurityUtils;
|
||||||
import com.youlai.boot.shared.mail.service.MailService;
|
import com.youlai.boot.shared.mail.service.MailService;
|
||||||
|
|||||||
@@ -72,25 +72,21 @@ mybatis-plus:
|
|||||||
|
|
||||||
# 安全配置
|
# 安全配置
|
||||||
security:
|
security:
|
||||||
# 认证策略核心配置
|
session:
|
||||||
auth:
|
type: jwt # 会话方式 [jwt|redis-token]
|
||||||
type: jwt # 认证策略 [jwt|redis-token]
|
access-token-time-to-live: 3600 # 访问令牌 有效期(单位:秒),默认 1 小时,-1 表示永不过期
|
||||||
access-token-ttl: 3600 # 访问令牌 有效期(单位:秒),默认 1 小时,-1 表示永不过期
|
refresh-token-time-to-live: 604800 # 刷新令牌有效期(单位:秒),默认 7 天,-1 表示永不过期
|
||||||
refresh-token-ttl: 604800 # 刷新令牌有效期(单位:秒),默认 7 天,-1 表示永不过期
|
|
||||||
# JWT 认证配置
|
|
||||||
jwt:
|
jwt:
|
||||||
key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符,RS256至少2048位)
|
secret-key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符)
|
||||||
# Redis Token 认证配置
|
|
||||||
redis-token:
|
redis-token:
|
||||||
max-sessions: 1 # 最大允许多个设备同时登录的数量(默认为1)
|
allow-multi-login: false # 是否允许多设备登录
|
||||||
session-control: REVOKE_OLDEST # 会话超限处理策略 [REJECT|REVOKE_OLDEST|REVOKE_ALL]
|
# 安全白名单路径(完全绕过安全过滤器)
|
||||||
# 白名单路径
|
|
||||||
ignore-urls:
|
ignore-urls:
|
||||||
- /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录)
|
- /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录)
|
||||||
- /api/v1/auth/captcha # 验证码获取接口
|
- /api/v1/auth/captcha # 验证码获取接口
|
||||||
- /api/v1/auth/refresh-token # 刷新令牌接口
|
- /api/v1/auth/refresh-token # 刷新令牌接口
|
||||||
- /ws/** # WebSocket接口
|
- /ws/** # WebSocket接口
|
||||||
# 不走 Spring Security 过滤器链的请求路径(一般是静态资源)
|
# 非安全端点路径(允许匿名访问的API)
|
||||||
unsecured-urls:
|
unsecured-urls:
|
||||||
- ${springdoc.swagger-ui.path}
|
- ${springdoc.swagger-ui.path}
|
||||||
- /doc.html
|
- /doc.html
|
||||||
|
|||||||
@@ -70,29 +70,21 @@ mybatis-plus:
|
|||||||
|
|
||||||
# 安全配置
|
# 安全配置
|
||||||
security:
|
security:
|
||||||
# 认证策略核心配置
|
session:
|
||||||
auth:
|
type: jwt # 会话方式 [jwt|redis-token]
|
||||||
# 认证策略 [jwt|redis-token]
|
access-token-time-to-live: 3600 # 访问令牌 有效期(单位:秒),默认 1 小时,-1 表示永不过期
|
||||||
type: jwt
|
refresh-token-time-to-live: 604800 # 刷新令牌有效期(单位:秒),默认 7 天,-1 表示永不过期
|
||||||
# 访问令牌 有效期(单位:秒),默认 1 小时,-1 表示永不过期
|
|
||||||
access-token-ttl: 3600
|
|
||||||
# 刷新令牌有效期(单位:秒),默认 7 天,-1 表示永不过期
|
|
||||||
refresh-token-ttl: 604800
|
|
||||||
# JWT 认证配置
|
|
||||||
jwt:
|
jwt:
|
||||||
key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符,RS256至少2048位)
|
secret-key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符)
|
||||||
# Redis Token 认证配置
|
|
||||||
redis-token:
|
redis-token:
|
||||||
max-sessions: 1 # 最大允许多个设备同时登录的数量(默认为1)
|
allow-multi-login: true # 是否允许多设备登录
|
||||||
session-control: REVOKE_OLDEST # 会话超限处理策略 [REJECT|REVOKE_OLDEST|REVOKE_ALL]
|
# 安全白名单路径(完全绕过安全过滤器)
|
||||||
|
|
||||||
# 无需认证的请求路径
|
|
||||||
ignore-urls:
|
ignore-urls:
|
||||||
- /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录)
|
- /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录)
|
||||||
- /api/v1/auth/captcha # 验证码获取接口
|
- /api/v1/auth/captcha # 验证码获取接口
|
||||||
- /api/v1/auth/refresh-token # 刷新令牌接口
|
- /api/v1/auth/refresh-token # 刷新令牌接口
|
||||||
- /ws/** # WebSocket接口
|
- /ws/** # WebSocket接口
|
||||||
# 不走 Spring Security 过滤器链的请求路径(一般是静态资源)
|
# 非安全端点路径(允许匿名访问的API)
|
||||||
unsecured-urls:
|
unsecured-urls:
|
||||||
- ${springdoc.swagger-ui.path}
|
- ${springdoc.swagger-ui.path}
|
||||||
- /doc.html
|
- /doc.html
|
||||||
|
|||||||
Reference in New Issue
Block a user