认证与会话管理
youlai-boot 提供完整的认证与会话管理方案,支持双模式切换,涵盖登录认证、Token管理、会话失效、权限缓存等核心功能。
会话模式
通过 security.session.type 配置切换:
| 模式 | 说明 | 适用场景 |
|---|---|---|
jwt | JWT无状态认证 + Redis辅助撤销 | 分布式系统、对性能要求高 |
redis-token | Redis有状态认证 | 需要在线用户管理、精细化会话控制 |
模式对比:
| 特性 | JWT模式 | Redis-Token模式 |
|---|---|---|
| Token类型 | JWT(含用户信息) | 随机UUID |
| 服务端存储 | 仅撤销状态 | 完整会话信息 |
| 扩展性 | 天然支持分布式 | 需Redis共享 |
| 在线用户 | 不支持 | 支持 |
| 会话管理 | 依赖Redis辅助 | 原生支持 |
| 性能 | 更高(无网络IO) | 需Redis查询 |
认证流程
认证接口
| 接口 | 方法 | 说明 |
|---|---|---|
/api/v1/auth/captcha | GET | 获取验证码 |
/api/v1/auth/login | POST | 用户登录 |
/api/v1/auth/refresh-token | POST | 刷新令牌 |
/api/v1/auth/logout | DELETE | 退出登录 |
请求头约定:
Authorization: Bearer <accessToken>Token结构
JWT Payload:
json
{
"sub": "admin", // 用户名
"jti": "abc123", // Token唯一标识
"iat": 1700000000, // 签发时间
"exp": 1700003600, // 过期时间
"userId": 1, // 用户ID
"deptId": 10, // 部门ID
"tokenVersion": 5, // Token版本号
"dataScopes": [ // 数据权限列表
{
"roleCode": "ADMIN",
"dataScope": 1,
"customDeptIds": null
}
],
"authorities": ["ROLE_ADMIN", "user:add"]
}| 字段 | 类型 | 说明 |
|---|---|---|
sub | String | 用户名(标准Claim) |
jti | String | Token唯一标识,用于撤销 |
iat | Long | 签发时间戳 |
exp | Long | 过期时间戳 |
userId | Long | 用户ID |
deptId | Long | 部门ID |
tokenVersion | Integer | Token版本号,用于会话失效控制 |
dataScopes | Array | 数据权限列表,支持多角色合并 |
authorities | Array | 角色权限集合 |
Redis-Token存储结构:
# 访问令牌 -> 用户会话信息
auth:access_token:{accessToken} -> UserSession
# 刷新令牌 -> 用户会话信息
auth:refresh_token:{refreshToken} -> UserSession
# 用户ID -> 访问令牌(单设备登录控制)
auth:user:access_token:{userId} -> accessToken
# 用户ID -> 刷新令牌
auth:user:refresh_token:{userId} -> refreshToken会话失效机制
两种模式都支持两类会话失效:
| 失效类型 | 说明 | 触发场景 |
|---|---|---|
| 单端退出 | 仅当前会话失效 | 用户主动退出 |
| 全端踢下线 | 用户所有会话失效 | 改密、封禁、管理员踢出 |
JWT模式失效机制:
tokenVersion机制:
# 用户禁用或修改密码后,递增版本号
Key: auth:user:token_version:{userId}
Value: 版本号(递增整数)
# 校验逻辑
redisVersion = redis.get("auth:user:token_version:{userId}") ?: 0
if (token.tokenVersion < redisVersion) {
return false; // Token版本低于Redis版本,全部失效
}版本号递增场景:
| 触发场景 | 操作 |
|---|---|
| 用户禁用 | increment(userId) |
| 用户修改密码 | increment(userId) |
| 管理员踢下线 | increment(userId) |
Redis-Token模式失效机制:
直接删除Redis中的会话数据:
# 单端退出
DELETE auth:access_token:{token}
# 全端踢下线
DELETE auth:access_token:{token}
DELETE auth:refresh_token:{refreshToken}
DELETE auth:user:access_token:{userId}
DELETE auth:user:refresh_token:{userId}Token刷新机制
前端处理:
typescript
// request.ts - Token过期自动续期
async function retryWithRefresh(config) {
const userStore = useUserStoreHook();
await userStore.refreshTokenOnce(); // 单飞控制
const token = AuthStorage.getAccessToken();
config.headers.Authorization = `Bearer ${token}`;
return http(config); // 重试原请求
}权限缓存机制
采用 Read-Through 模式,读取时自动回源:
缓存结构:
Key: system:role:perms
Type: Hash
Field: ADMIN Value: ["user:add", "user:edit", ...]
Field: USER Value: ["user:view", ...]缓存失效:权限变更时主动清除缓存
| 触发场景 | 失效方法 |
|---|---|
| 角色分配菜单 | refreshRolePermsCache(roleCode) |
| 角色删除 | refreshRolePermsCache(roleCode) |
| 菜单修改/删除 | refreshRolePermsCache() |
java
// 角色权限变更后
roleMenuService.refreshRolePermsCache(roleCode);
// 菜单修改后
roleMenuService.refreshRolePermsCache(); // 清除所有登录方式扩展
youlai-boot 基于 Spring Security 的 AuthenticationProvider 机制,支持多种登录方式扩展。
扩展原理:
认证接口:
| 接口 | 方法 | 说明 |
|---|---|---|
/api/v1/auth/captcha | GET | 获取图形验证码 |
/api/v1/auth/login | POST | 用户名密码登录 |
/api/v1/auth/login/sms | POST | 短信验证码登录 |
/api/v1/auth/sms/code | POST | 发送登录短信验证码 |
/api/v1/auth/refresh-token | POST | 刷新令牌 |
/api/v1/auth/logout | DELETE | 退出登录 |
短信验证码登录
整体流程:
核心组件:
| 组件 | 说明 |
|---|---|
SmsAuthenticationToken | 短信验证码认证 Token,包含手机号和验证码 |
SmsAuthenticationProvider | 短信验证码认证处理器,校验验证码并返回用户信息 |
SecurityConfig | 注册 SmsAuthenticationProvider 到认证管理器 |
SmsAuthenticationToken:
java
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
// 未认证:principal=手机号,credentials=验证码
public SmsAuthenticationToken(String mobile, String verifyCode) {
super(null);
this.principal = mobile;
this.credentials = verifyCode;
setAuthenticated(false);
}
// 已认证:principal=用户详情,credentials=null
public SmsAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = null;
super.setAuthenticated(true);
}
}SmsAuthenticationProvider:
java
public class SmsAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String mobile = (String) authentication.getPrincipal();
String inputVerifyCode = (String) authentication.getCredentials();
// 1. 参数校验
if (StrUtil.isBlank(mobile)) {
throw new CaptchaValidationException("手机号不能为空");
}
// 2. 查询用户
UserAuthInfo userAuthInfo = userService.getAuthInfoByMobile(mobile);
if (userAuthInfo == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 3. 校验用户状态
if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) {
throw new DisabledException("用户已被禁用");
}
// 4. 校验验证码
String cacheKey = "captcha:sms_login:" + mobile;
String cachedVerifyCode = redisTemplate.opsForValue().get(cacheKey);
if (cachedVerifyCode == null) {
throw new CaptchaValidationException("验证码已过期,请重新获取");
}
if (!StrUtil.equals(inputVerifyCode, cachedVerifyCode)) {
throw new CaptchaValidationException("验证码错误");
}
// 5. 删除验证码,防止重复使用
redisTemplate.delete(cacheKey);
// 6. 返回已认证的 Token
SysUserDetails userDetails = new SysUserDetails(userAuthInfo);
return SmsAuthenticationToken.authenticated(userDetails, userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
}Provider注册:
java
@Configuration
public class SecurityConfig {
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return provider;
}
@Bean
public SmsAuthenticationProvider smsAuthenticationProvider() {
return new SmsAuthenticationProvider(userService, redisTemplate);
}
@Bean
public AuthenticationManager authenticationManager(
DaoAuthenticationProvider daoAuthenticationProvider,
SmsAuthenticationProvider smsAuthenticationProvider
) {
return new ProviderManager(
daoAuthenticationProvider, // 用户名密码登录
smsAuthenticationProvider // 短信验证码登录
);
}
}验证码缓存:
Key: captcha:sms_login:{mobile}
Value: 验证码 (如: 1234)
TTL: 5 分钟扩展其他登录方式
参考短信验证码登录的实现,扩展其他登录方式(如微信登录、邮箱登录)只需:
创建 AuthenticationToken
javapublic class WechatAuthenticationToken extends AbstractAuthenticationToken { // principal = code, credentials = null }创建 AuthenticationProvider
javapublic class WechatAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) { // 调用微信 API 获取用户信息 // 返回已认证的 Token } @Override public boolean supports(Class<?> authentication) { return WechatAuthenticationToken.class.isAssignableFrom(authentication); } }注册 Provider
java@Bean public AuthenticationManager authenticationManager( DaoAuthenticationProvider daoProvider, SmsAuthenticationProvider smsProvider, WechatAuthenticationProvider wechatProvider ) { return new ProviderManager(daoProvider, smsProvider, wechatProvider); }
核心组件
TokenManager接口:
java
public interface TokenManager {
// 生成Token
AuthenticationToken generateToken(Authentication authentication);
// 解析Token
Authentication parseToken(String token);
// 校验Token
boolean validateToken(String token);
boolean validateRefreshToken(String refreshToken);
// 刷新Token
AuthenticationToken refreshToken(String refreshToken);
// 会话管理
void invalidateToken(String token);
void invalidateUserSessions(Long userId);
}实现类:
| 类 | 模式 | 说明 |
|---|---|---|
JwtTokenManager | jwt | JWT生成、解析、校验、撤销 |
RedisTokenManager | redis-token | Redis会话管理 |
过滤器:
| 类 | 说明 |
|---|---|
TokenAuthenticationFilter | Token校验,构建Security上下文 |
CaptchaValidationFilter | 验证码校验(登录前置) |
配置项
yaml
security:
# 白名单路径(完全绕过安全过滤器)
ignore-urls:
- /api/v1/auth/login/**
- /ws/**
# 匿名访问路径(允许未登录访问)
unsecured-urls:
- /doc.html
- /v3/api-docs/**
session:
# 会话模式: jwt | redis-token
type: jwt
# 访问令牌有效期(秒),-1永不过期
access-token-time-to-live: 3600
# 刷新令牌有效期(秒)
refresh-token-time-to-live: 604800
jwt:
# JWT签名密钥(HS256至少32字符)
secret-key: SecretKey012345678901234567890123456789
redis-token:
# 是否允许多设备同时登录
allow-multi-login: true最佳实践
模式选择
- 分布式/微服务:优先JWT模式
- 需在线用户管理:选择Redis-Token模式
安全建议
- JWT密钥至少32字符
- 访问令牌有效期建议1小时
- 刷新令牌有效期建议7天
- 生产环境禁用永不过期(TTL=-1)
性能优化
- 权限缓存使用Hash结构,支持批量查询
- tokenVersion 使用 Redis INCR 原子操作,无 TTL 过期问题
相关文件
| 文件 | 说明 |
|---|---|
security/token/TokenManager.java | Token管理器接口 |
security/token/JwtTokenManager.java | JWT实现 |
security/token/RedisTokenManager.java | Redis实现 |
security/filter/TokenAuthenticationFilter.java | 认证过滤器 |
security/service/PermissionService.java | 权限校验服务 |
security/provider/SmsAuthenticationProvider.java | 短信验证码认证处理器 |
security/model/SmsAuthenticationToken.java | 短信验证码认证 Token |
auth/controller/AuthController.java | 认证控制器 |
auth/service/AuthService.java | 认证服务接口 |
auth/service/impl/AuthServiceImpl.java | 认证服务实现 |
config/SecurityConfig.java | Security 配置类 |
config/property/SecurityProperties.java | 安全配置属性 |
