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 扩展点:AuthServiceUserInfoProviderLoginUserInfoProvider
  • 密码加密: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)。