scaffold-security -- 安全认证模块
模块概述
scaffold-security 是基于 Spring Security + JJWT 的 JWT 认证模块,提供登录/登出/刷新Token/获取用户信息的完整认证流程。模块采用无状态(STATELESS)会话策略,通过 JwtAuthenticationFilter 对每个请求进行 Token 校验。支持智能感知 Redis 模块:有 Redis 时使用 Redis 存储 Token(支持单点登录控制),无 Redis 时自动降级为内存存储。
功能列表
- JWT AccessToken + RefreshToken 双令牌机制
- 登录/登出/刷新Token/获取用户信息 REST API
JwtAuthenticationFilter请求拦截与认证- Token 存储:Redis 优先(
RedisTokenStoreDelegate),内存兜底(MemoryTokenStore) - SPI 扩展点:
AuthService、UserInfoProvider、LoginUserInfoProvider - 密码加密:BCryptPasswordEncoder
- 匿名路径配置
- CORS 跨域支持
- 认证/授权失败处理器
安全流程
1. 客户端 POST /v1/auth/login { username, password }
-> AuthService.login() 校验凭证
-> JwtUtils 生成 AccessToken + RefreshToken
-> TokenStore 保存 Token
-> 返回 { accessToken, refreshToken, expiresIn, userId, ... }
2. 后续请求携带 Header: Authorization: Bearer <accessToken>
-> JwtAuthenticationFilter 提取并验证 Token
-> TokenStore 校验 Token 一致性
-> 创建 LoginUser 放入 SecurityContext
-> 请求继续到 Controller
3. AccessToken 过期(前端收到 code=2001)
-> 客户端 POST /v1/auth/refresh { refreshToken }
-> AuthService.refreshToken() 验证 RefreshToken
-> 生成新的 AccessToken
-> TokenStore 更新 Token
-> 返回新的 Token 对
4. 登出
-> POST /v1/auth/logout
-> TokenStore 删除 Token
配置参数
在 application.yml 中以 scaffold.security 为前缀配置:
scaffold:
security:
enabled: true
jwt-secret: "your-production-secret-key-at-least-32-chars"
access-token-expire-hours: 2
refresh-token-expire-days: 7
jwt-issuer: "scaffold"
token-store-enabled: true
multi-tenant-enabled: false
token-header: "Authorization"
token-prefix: "Bearer "
login-path: "/v1/auth/login"
logout-path: "/v1/auth/logout"
refresh-path: "/v1/auth/refresh"
anonymous-enabled: false
anonymous-paths:
- "/v1/auth/login"
- "/v1/auth/refresh"
- "/doc.html"
- "/v3/api-docs/**"
cors-enabled: false
cors-allowed-origins: "*"
session-enabled: false
maximum-sessions: 1
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enabled |
Boolean | true | 是否启用安全模块 |
jwt-secret |
String | (内置默认值) | JWT 签名密钥,生产环境必须更换 |
access-token-expire-hours |
Long | 2 | AccessToken 过期时间(小时) |
refresh-token-expire-days |
Long | 7 | RefreshToken 过期时间(天) |
jwt-issuer |
String | "scaffold" | Token 签发者 |
token-store-enabled |
Boolean | true | 是否启用 Token 存储 |
multi-tenant-enabled |
Boolean | false | 是否启用多租户支持 |
token-header |
String | "Authorization" | Token 请求头名称 |
token-prefix |
String | "Bearer " | Token 前缀 |
login-path |
String | "/v1/auth/login" | 登录路径 |
logout-path |
String | "/v1/auth/logout" | 登出路径 |
refresh-path |
String | "/v1/auth/refresh" | 刷新 Token 路径 |
anonymous-enabled |
Boolean | false | 是否启用匿名路径 |
anonymous-paths |
String[] | [] | 匿名访问路径列表 |
cors-enabled |
Boolean | false | 是否启用 CORS |
cors-allowed-origins |
String | "*" | 允许的跨域来源(逗号分隔) |
session-enabled |
Boolean | false | 是否启用会话管理 |
maximum-sessions |
Integer | 1 | 同一账号最大登录数 |
max-sessions-prevents-login |
Boolean | true | 是否阻止已登录账号再次登录 |
controller.enabled |
Boolean | true | 是否启用内置 AuthController |
API 接口列表
POST /v1/auth/login -- 登录
请求体:
{
"username": "admin",
"password": "admin123",
"loginType": "password",
"tenantId": 1,
"clientType": "web"
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
username |
String | 是 | 用户名/手机号/邮箱 |
password |
String | 是 | 密码 |
captcha |
String | 否 | 验证码 |
captchaKey |
String | 否 | 验证码 Key |
loginType |
String | 否 | 登录类型:password/sms/email,默认 password |
tenantId |
Long | 否 | 租户 ID(多租户场景) |
clientType |
String | 否 | 客户端类型:web/app/miniapp,默认 web |
deviceId |
String | 否 | 设备 ID |
响应体(Result<LoginResponse>):
{
"code": 0,
"data": {
"accessToken": "eyJhbGciOiJI...",
"refreshToken": "eyJhbGciOiJI...",
"tokenType": "Bearer",
"expiresIn": 7200,
"userId": 10001,
"username": "admin",
"tenantId": 1,
"userType": "user"
}
}
POST /v1/auth/logout -- 登出
请求头:Authorization: Bearer <accessToken>
响应:Result<Void>(code: 0)
POST /v1/auth/refresh -- 刷新 Token
请求体:
{
"refreshToken": "eyJhbGciOiJI..."
}
响应体与登录接口相同(Result<LoginResponse>)。
GET /v1/auth/userInfo -- 获取当前用户信息
请求头:Authorization: Bearer <accessToken>
响应体(Result<UserInfoResponse>):
{
"code": 0,
"data": {
"userId": 10001,
"username": "admin",
"nickname": "管理员",
"avatar": "/files/avatar/10001.jpg",
"email": "admin@example.com",
"phone": "13800138000",
"roles": ["admin", "user"],
"permissions": ["system:user:list", "system:user:add"]
}
}
JWT 配置
JWT Token 的 Claims 结构:
| 字段 | 类型 | 说明 |
|---|---|---|
userId |
Long | 用户 ID |
username |
String | 用户名 |
tenantId |
Long | 租户 ID(可选) |
userType |
String | 用户类型(user/admin) |
iss |
String | 签发者 |
iat |
Date | 签发时间 |
exp |
Date | 过期时间 |
RefreshToken 额外包含 type: "refresh" 字段,用于与 AccessToken 区分。
Token 存储
模块根据 classpath 是否存在 scaffold-redis 自动选择存储实现:
Redis Token 存储(RedisTokenStoreDelegate)
当 classpath 存在 com.scaffold.redis.util.RedisCache 且 Bean 可用时自动激活。Token 存储在 Redis 中,支持:
- 单点登录控制(同一用户只保留最新 Token)
- Token 主动吊销(登出后旧 Token 立即失效)
- 分布式环境共享 Token 状态
Key 格式:auth:access:{userId} / auth:refresh:{userId}
内存 Token 存储(MemoryTokenStore)
当 Redis 不可用时自动降级。使用 ConcurrentHashMap 存储,仅适用于单机开发和测试环境。不支持跨实例 Token 共享。
TokenStore 接口方法
| 方法 | 说明 |
|---|---|
saveToken(userId, accessToken, refreshToken) |
保存 Token 对 |
getAccessToken(userId) |
获取 AccessToken |
getRefreshToken(userId) |
获取 RefreshToken |
removeToken(userId) |
删除 Token(登出) |
isTokenValid(userId, token) |
校验 Token 一致性 |
exists(userId) |
判断用户是否已登录 |
refreshToken(userId, oldToken, newToken) |
刷新 Token |
getUserIdByRefreshToken(refreshToken) |
根据 RefreshToken 反查 userId |
SPI 扩展点
AuthService
认证服务接口,业务模块(通常是 scaffold-system)必须提供实现。默认实现为 DefaultAuthService。
登录状态
登录成功后返回的状态码:
| 状态值 | 含义 |
|---|---|
| 1 | 成功 |
| 0 | 失败 |
注意:登录日志的 status 字段遵循布尔语义规范,1 = 成功,0 = 失败。
public interface AuthService {
LoginResponse login(LoginRequest request);
void logout(Long userId);
LoginResponse refreshToken(RefreshTokenRequest request);
default LoginUserInfo authenticate(String username, String password) { ... }
}
自定义实现示例:
@Service
public class CustomAuthService implements AuthService {
@Override
public LoginResponse login(LoginRequest request) {
// 1. 校验验证码(可选)
// 2. 查询用户
// 3. 校验密码
// 4. 生成 Token
// 5. 保存 Token
// 6. 返回 LoginResponse
}
@Override
public void logout(Long userId) {
tokenStore.removeToken(userId);
}
@Override
public LoginResponse refreshToken(RefreshTokenRequest request) {
// 1. 解析 RefreshToken
// 2. 校验有效性
// 3. 生成新 AccessToken
// 4. 更新 TokenStore
// 5. 返回新 Token 对
}
}
UserInfoProvider
用户信息提供者,用于 /v1/auth/userInfo 接口返回完整用户信息(含角色、权限)。由 scaffold-system 实现。
@Service
public class SystemUserInfoProvider implements UserInfoProvider {
@Override
public UserInfoResponse getUserInfo(Long userId, String username) {
User user = userRepository.getById(userId);
List<String> roles = roleService.getRoleCodes(userId);
List<String> permissions = menuService.getPermissions(userId);
return UserInfoResponse.builder()
.userId(user.getId())
.username(user.getUsername())
.nickname(user.getNickname())
.avatar(user.getAvatar())
.email(user.getEmail())
.phone(user.getPhone())
.roles(roles)
.permissions(permissions)
.build();
}
}
LoginUserInfoProvider
登录用户信息提供者,用于补充登录时需要的身份信息。由 scaffold-system 实现。
@Service
public class SystemLoginUserInfoProvider implements LoginUserInfoProvider {
@Override
public LoginUserInfo loadByUsername(String username) {
User user = userRepository.getByUsername(username);
return LoginUserInfo.builder()
.userId(user.getId())
.username(user.getUsername())
.tenantId(user.getTenantId())
.userType("user")
.nickname(user.getNickname())
.avatar(user.getAvatar())
.build();
}
}
匿名路径配置
以下路径默认无需认证即可访问:
/v1/auth/login-- 登录/v1/auth/refresh-- 刷新 Token/doc.html,/webjars/**,/v3/api-docs/**,/swagger-resources/**-- API 文档/favicon.ico
通过 scaffold.security.anonymous-paths 可追加自定义匿名路径:
scaffold:
security:
anonymous-paths:
- "/v1/public/**"
- "/v1/i18n/public/**"
SecurityContextHolderUtils 工具类
在业务代码中获取当前登录用户信息:
// 获取当前用户 ID
Long userId = SecurityContextHolderUtils.getCurrentUserId();
// 获取当前用户名
String username = SecurityContextHolderUtils.getCurrentUsername();
// 获取当前租户 ID
Long tenantId = SecurityContextHolderUtils.getCurrentTenantId();
// 获取完整用户信息
LoginUser loginUser = SecurityContextHolderUtils.getCurrentUser();
// 判断是否已登录
boolean loggedIn = SecurityContextHolderUtils.isAuthenticated();
// 获取用户类型
String userType = SecurityContextHolderUtils.getCurrentUserType();
// 清除认证信息(一般不需要手动调用)
SecurityContextHolderUtils.clear();
自动配置说明
本模块通过 SecurityAutoConfiguration 注册以下 Bean(均支持 @ConditionalOnMissingBean 覆盖):
| Bean | 条件 | 说明 |
|---|---|---|
JwtUtils |
未自定义 | JWT 工具类 |
TokenStore (Redis) |
classpath 有 RedisCache | Redis Token 存储 |
TokenStore (Memory) |
classpath 无 RedisCache | 内存 Token 存储 |
JwtAuthenticationFilter |
未自定义 | JWT 认证过滤器 |
SecurityAuthenticationEntryPoint |
未禁用 | 认证失败处理器 |
SecurityAccessDeniedHandler |
未禁用 | 授权失败处理器 |
SecurityLogoutSuccessHandler |
未禁用 | 登出成功处理器 |
PasswordEncoder |
未自定义 | BCrypt 密码编码器 |
AuthenticationManager |
未自定义 | 认证管理器 |
CorsConfigurationSource |
corsEnabled=true | CORS 配置 |
SecurityFilterChain |
未自定义 | Security 过滤器链 |
AuthService |
未自定义 | 默认认证服务 |
LoginUserInfoProvider |
未自定义 | 默认登录信息提供者 |
UserInfoProvider |
未自定义 | 默认用户信息提供者 |
注意事项
- 生产环境务必更换
jwt-secret,且密钥长度须满足 HMAC-SHA256 要求(至少 32 字节)。 - AccessToken 默认 2 小时过期,前端应在收到
code=2001时自动调用/v1/auth/refresh刷新。 - 内存 Token 存储仅用于开发和测试,生产环境应引入
scaffold-redis模块以使用 Redis 存储。 - 自定义
AuthService实现后,默认实现自动失效,无需额外配置。 LoginUser对象中userId为 Long 类型,前端显示时应转为 String,避免 JS 精度丢失(Jackson 已全局配置 Long 序列化为 String)。