roles = claims.get(JwtClaimConstants.ROLES);
+securityUserDetails.setRoles(roles);
+// getAuthorities() 会在运行时自动 add ROLE_ 前缀
+```
+
+同步修改 `JwtClaimConstants`:
+
+```java
+// 删除 AUTHORITIES,新增 ROLES
+String ROLES = "roles";
+```
+
+上线发布时执行登录态失效:
+
+```text
+1. 清理 Redis 中 auth:* 登录态 key
+2. 轮换 security.session.jwt.secret-key
+3. 通知前端收到 401 后跳转登录页
+```
+
+### 4.6 单模块改动清单
+
+| 文件 | 操作 | 改动 |
+|------|------|------|
+| `framework/security/port/UserAuthenticationPort.java` | **新增** | 端口接口 |
+| `framework/security/port/PermissionPort.java` | **新增** | 端口接口 |
+| `framework/security/model/SecurityUserDetails.java` | 改造 | `authorities` → `roles`,`getAuthorities()` 改为计算 |
+| `framework/security/model/UserSession.java` | **删除** | 不再需要 |
+| `framework/security/service/SecurityUserDetailsService.java` | 改造 | 注入 `UserAuthenticationPort` 替代 `UserService` |
+| `framework/security/service/PermissionService.java` | 改造 | 注入 `PermissionPort` 替代 `RoleMenuService` |
+| `framework/security/config/SecurityConfig.java` | 改造 | 移除 `UserService` 直接注入,Provider 改走端口或留在使用方 |
+| `framework/security/service/UserAuthQueryService.java` | **删除** | 空占位文件 |
+| `framework/security/service/RolePermissionService.java` | **删除** | 空占位文件 |
+| `framework/security/service/WxMaUserAuthQueryService.java` | **删除** | 空占位文件 |
+| `framework/security/model/WxMaBindInfo.java` | **删除** | 空占位文件 |
+| `framework/security/util/SecurityUtils.java` | 改造 | `getRoles()` 简化为直接取 `roles` 字段 |
+| `framework/security/token/RedisTokenManager.java` | 改造 | 存 `SecurityUserDetails`,删除 `UserSession` 引用和 `buildUserDetails` |
+| `framework/security/token/JwtTokenManager.java` | 改造 | `parseToken` 设 `roles` 替代 `authorities` |
+| `common/constant/JwtClaimConstants.java` | 改造 | 删除 `AUTHORITIES`,新增 `ROLES = "roles"` |
+| `common/enums/SocialPlatformEnum.java` | **新增/迁移** | 从 `system.enums` 下沉到 common |
+| `system/security/adapter/UserAuthenticationAdapter.java` | **新增** | 适配器实现 |
+| `system/security/adapter/PermissionAdapter.java` | **新增** | 适配器实现 |
+
+---
+
+## 五、多模块重构(youlai-boot-multi)
+
+多模块项目中,`youlai-framework` 是独立 Maven 模块,`youlai-system` 是另一个模块。核心区别:端口定义在 `youlai-framework`,适配器实现在 `youlai-system`。
+
+### 5.1 模块依赖关系
+
+```
+youlai-framework(端口定义 + 基础设施) ←──依赖── youlai-system(适配器实现 + 业务)
+ ↑ ↑
+ └──依赖── youlai-auth、youlai-application 等
+```
+
+> **关键**:`youlai-framework` 的 `pom.xml` **不依赖** `youlai-system`。`youlai-system` 依赖 `youlai-framework`,实现端口接口。
+
+### 5.2 当前多模块状态
+
+多模块项目已将 `SecurityUserDetailsService` 和 `PermissionService` 移至 `youlai-system/security/service/`,但代码仍直接 `import` system 服务,循环依赖未解决。`youlai-framework/security/service/` 目录为空。
+
+> ⚠️ **版本不一致提醒**:根 POM 声明 `4.3.1`,而 `youlai-framework` 和 `youlai-system` 的 parent 版本为 `4.3.0`。重构时应一并修正。
+
+### 5.3 端口接口定义(youlai-framework)
+
+在 `youlai-framework` 模块新建 `port` 包:
+
+```
+youlai-framework/src/main/java/com/youlai/boot/framework/security/
+├── port/
+│ ├── UserAuthenticationPort.java # 新增
+│ └── PermissionPort.java # 新增
+├── model/
+│ ├── SecurityUserDetails.java # 改造(同单模块 4.2)
+│ ├── SecurityUser.java # 重命名(原 UserAuthInfo)
+│ └── UserSession.java # 删除
+├── token/
+│ ├── TokenManager.java # 不变
+│ ├── JwtTokenManager.java # 改造(同单模块 4.5)
+│ └── RedisTokenManager.java # 改造(同单模块 4.5)
+├── util/
+│ └── SecurityUtils.java # 改造(同单模块 3.3)
+└── service/ # 保持为空(服务在 system 模块)
+```
+
+> 端口接口代码与单模块完全相同(见 4.1),仅包名一致。
+
+### 5.4 适配器实现(youlai-system)
+
+在 `youlai-system` 模块新建 `adapter` 包(当前**不存在** adapter 目录,需创建):
+
+```
+youlai-system/src/main/java/com/youlai/boot/system/security/
+├── adapter/ # ← 新建目录
+│ ├── UserAuthenticationAdapter.java # 新增
+│ └── PermissionAdapter.java # 新增
+├── service/
+│ ├── SecurityUserDetailsService.java # 改造(注入 Port)
+│ └── PermissionService.java # 改造(注入 Port)
+├── provider/
+│ ├── SmsAuthenticationProvider.java # 使用方业务 Provider,留在 system
+│ └── WxMaAuthenticationProvider.java # 使用方业务 Provider,留在 system
+└── config/
+ └── SecurityConfig.java # 检查注入对象
+```
+
+> 适配器代码与单模块完全相同(见 4.3),仅位于 `youlai-system` 模块。
+
+### 5.5 服务层改造(youlai-system)
+
+`SecurityUserDetailsService` 和 `PermissionService` 已在 `youlai-system` 模块中,改造方式与单模块相同(见 4.4),注入端口接口替代直接服务引用。
+
+`SmsAuthenticationProvider` 和 `WxMaAuthenticationProvider` 属于使用方业务认证流程,留在 `youlai-system`。它们可以直接调用 `UserService` / `UserSocialService` 完成验证码、微信绑定、session_key 更新等业务动作,但不得迁入 Starter。
+
+### 5.6 POM 依赖检查
+
+```xml
+
+
+
+ com.youlai
+ youlai-common
+
+
+
+
+
+
+
+ com.youlai
+ youlai-framework
+
+
+```
+
+### 5.7 多模块改动清单
+
+| 模块 | 文件 | 操作 |
+|------|------|------|
+| `youlai-framework` | `security/port/UserAuthenticationPort.java` | 新增 |
+| `youlai-framework` | `security/port/PermissionPort.java` | 新增 |
+| `youlai-framework` | `security/model/SecurityUserDetails.java` | 改造 |
+| `youlai-framework` | `security/model/UserSession.java` | **删除** |
+| `youlai-framework` | `security/util/SecurityUtils.java` | 改造 |
+| `youlai-framework` | `security/token/JwtTokenManager.java` | 改造 |
+| `youlai-framework` | `security/token/RedisTokenManager.java` | 改造 |
+| `youlai-framework` | `pom.xml` | 确认不含 youlai-system 依赖 |
+| `youlai-system` | `security/adapter/UserAuthenticationAdapter.java` | 新增 |
+| `youlai-system` | `security/adapter/PermissionAdapter.java` | 新增 |
+| `youlai-system` | `security/service/SecurityUserDetailsService.java` | 改造 |
+| `youlai-system` | `security/service/PermissionService.java` | 改造 |
+| `youlai-system` | `security/provider/SmsAuthenticationProvider.java` | 保留在使用方,不迁入 Starter |
+| `youlai-system` | `security/provider/WxMaAuthenticationProvider.java` | 保留在使用方,不迁入 Starter |
+| `youlai-system` | `pom.xml` | parent 版本修正为 4.3.1 |
+
+---
+
+## 六、Security Starter 抽离与发布方案
+
+> **前提**:完成第四节(单模块)或第五节(多模块)的端口/适配器解耦后,方可执行本节。
+
+### 6.1 可行性分析
+
+| 维度 | 当前状态 | Starter 要求 | 差距 |
+|------|----------|--------------|------|
+| 端口定义 | 散落在 `framework.security` | 需独立模块 | 需新建 Maven 模块 |
+| 自动装配 | 无 `spring.factories` / `AutoConfiguration.imports` | 需 SPI 注册 | 需新建自动配置类 |
+| 配置属性 | `SecurityProperties` 已有 `@ConfigurationProperties` | 需 `@EnableConfigurationProperties` | 改造 |
+| Token 实现 | 固定保留 `JwtTokenManager` 与 `RedisTokenManager` 两个实现 | 按 `security.session.type` 条件装配 | 改造 |
+| 业务耦合 | `SocialPlatformEnum` 引用 system 包 | 需下沉到 common | 改造 |
+| 依赖管理 | security 与 cache/mybatis/captcha 混在 framework | 需独立依赖树 | 新建 pom.xml |
+
+**结论**:可行。端口/适配器解耦是前置条件,解耦后 security 模块编译期零 system 依赖,符合 Starter 抽离要求。
+
+### 6.2 Starter 模块设计
+
+```
+youlai-boot-multi/(多模块项目根)
+├── youlai-common/ # 常量、枚举(SocialPlatformEnum 下沉至此)
+├── youlai-security-spring-boot-starter/ # ← 新增:Security Starter
+│ ├── src/main/java/com/youlai/boot/framework/security/
+│ │ ├── autoconfigure/ # 自动配置类
+│ │ │ ├── SecurityAutoConfiguration.java
+│ │ │ ├── JwtTokenAutoConfiguration.java
+│ │ │ └── RedisTokenAutoConfiguration.java
+│ │ ├── port/ # 端口接口(对外契约)
+│ │ │ ├── UserAuthenticationPort.java
+│ │ │ └── PermissionPort.java
+│ │ ├── model/ # 安全模型
+│ │ │ ├── SecurityUserDetails.java
+│ │ │ ├── SecurityUser.java
+│ │ │ ├── RoleDataScope.java
+│ │ │ └── ...(Token 模型等)
+│ │ ├── service/ # 框架层服务
+│ │ │ ├── SecurityUserDetailsService.java
+│ │ │ └── PermissionService.java
+│ │ ├── token/ # Token 管理器
+│ │ │ ├── TokenManager.java
+│ │ │ ├── JwtTokenManager.java
+│ │ │ └── RedisTokenManager.java
+│ │ ├── filter/ # Token 认证过滤器
+│ │ │ └── TokenAuthenticationFilter.java
+│ │ ├── exception/ # 异常
+│ │ ├── util/ # SecurityUtils
+│ │ └── config/ # SecurityProperties
+│ ├── src/main/resources/
+│ │ └── META-INF/
+│ │ └── spring/
+│ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
+│ └── pom.xml
+├── youlai-system/ # 提供 Adapter 实现
+├── youlai-framework/ # 剥离 security 后,保留 cache/mybatis 等
+└── youlai-application/ # 引用 starter
+```
+
+### 6.3 pom.xml
+
+```xml
+
+
+ 4.0.0
+
+
+ com.youlai
+ youlai-boot
+ 4.3.1
+
+
+ youlai-security-spring-boot-starter
+ youlai Security Spring Boot Starter - 认证鉴权自动装配
+
+
+
+
+ com.youlai
+ youlai-common
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+
+
+ cn.hutool
+ hutool-all
+
+
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+
+
+
+```
+
+> **关键设计**:Starter 固定携带 Security、Web、Redis、Hutool JWT 所需依赖。使用方不再拼装 JWT/Redis 依赖,降低接入复杂度。
+
+### 6.4 自动配置类
+
+```java
+// autoconfigure/SecurityAutoConfiguration.java
+package com.youlai.boot.framework.security.autoconfigure;
+
+import com.youlai.boot.framework.security.config.SecurityProperties;
+import com.youlai.boot.framework.security.filter.TokenAuthenticationFilter;
+import com.youlai.boot.framework.security.port.PermissionPort;
+import com.youlai.boot.framework.security.port.UserAuthenticationPort;
+import com.youlai.boot.framework.security.service.PermissionService;
+import com.youlai.boot.framework.security.service.SecurityUserDetailsService;
+import com.youlai.boot.framework.security.token.TokenManager;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import tools.jackson.databind.json.JsonMapper;
+
+/**
+ * Security 自动配置入口。
+ *
+ * 装配条件:classpath 存在 SecurityProperties 类。
+ * 使用方可通过 spring.autoconfigure.exclude 排除整体自动配置。
+ */
+@AutoConfiguration
+@EnableConfigurationProperties(SecurityProperties.class)
+public class SecurityAutoConfiguration {
+
+ /**
+ * 密码编码器,默认 BCrypt。
+ */
+ @Bean
+ @ConditionalOnMissingBean(PasswordEncoder.class)
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ /**
+ * JSON 映射器,供 RedisTokenManager 反序列化使用。
+ */
+ @Bean
+ @ConditionalOnMissingBean(JsonMapper.class)
+ public JsonMapper jsonMapper() {
+ return JsonMapper.builder().findAndAddModules().build();
+ }
+
+ /**
+ * 用户认证服务。
+ *
+ * 需要使用方提供 UserAuthenticationPort 实现。
+ */
+ @Bean
+ @ConditionalOnMissingBean(UserDetailsService.class)
+ public UserDetailsService userDetailsService(UserAuthenticationPort userAuthenticationPort) {
+ return new SecurityUserDetailsService(userAuthenticationPort);
+ }
+
+ /**
+ * 权限服务 Bean(SpEL @ss.hasPerm(...) 使用)。
+ *
+ * 需要 PermissionPort 实现,否则启动失败——
+ * 使用方必须提供 PermissionAdapter。
+ */
+ @Bean("ss")
+ @ConditionalOnMissingBean(name = "ss")
+ @ConditionalOnProperty(prefix = "security", name = "enabled", havingValue = "true", matchIfMissing = true)
+ public PermissionService permissionService(PermissionPort permissionPort) {
+ return new PermissionService(permissionPort);
+ }
+
+ /**
+ * Token 认证过滤器。
+ *
+ * 过滤器不写业务 JSON 响应;无效 Token 抛出 Spring Security AuthenticationException,
+ * 由使用方 SecurityConfig 中的 AuthenticationEntryPoint 统一处理。
+ */
+ @Bean
+ @ConditionalOnMissingBean(TokenAuthenticationFilter.class)
+ public TokenAuthenticationFilter tokenAuthenticationFilter(TokenManager tokenManager) {
+ return new TokenAuthenticationFilter(tokenManager);
+ }
+}
+```
+
+```java
+// autoconfigure/JwtTokenAutoConfiguration.java
+package com.youlai.boot.framework.security.autoconfigure;
+
+import com.youlai.boot.framework.security.token.JwtTokenManager;
+import com.youlai.boot.framework.security.token.TokenManager;
+import com.youlai.boot.framework.security.config.SecurityProperties;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.core.RedisTemplate;
+
+/**
+ * JWT Token 管理器自动配置。
+ *
+ * 装配条件:
+ * 1. classpath 存在 cn.hutool.jwt.JWTUtil
+ * 2. security.session.type = jwt
+ */
+@AutoConfiguration
+@ConditionalOnClass(name = "cn.hutool.jwt.JWTUtil")
+@ConditionalOnProperty(prefix = "security.session", name = "type", havingValue = "jwt", matchIfMissing = true)
+public class JwtTokenAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean(TokenManager.class)
+ public TokenManager jwtTokenManager(SecurityProperties properties,
+ RedisTemplate redisTemplate) {
+ return new JwtTokenManager(properties, redisTemplate);
+ }
+}
+```
+
+```java
+// autoconfigure/RedisTokenAutoConfiguration.java
+package com.youlai.boot.framework.security.autoconfigure;
+
+import com.youlai.boot.framework.security.token.RedisTokenManager;
+import com.youlai.boot.framework.security.token.TokenManager;
+import com.youlai.boot.framework.security.config.SecurityProperties;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.context.annotation.Bean;
+import tools.jackson.databind.json.JsonMapper;
+
+/**
+ * Redis Token 管理器自动配置。
+ *
+ * 装配条件:
+ * 1. classpath 存在 RedisTemplate
+ * 2. security.session.type = redis-token
+ */
+@AutoConfiguration
+@ConditionalOnClass(RedisTemplate.class)
+@ConditionalOnProperty(prefix = "security.session", name = "type", havingValue = "redis-token")
+public class RedisTokenAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean(TokenManager.class)
+ public TokenManager redisTokenManager(SecurityProperties properties,
+ RedisTemplate redisTemplate,
+ JsonMapper jsonMapper) {
+ return new RedisTokenManager(properties, redisTemplate, jsonMapper);
+ }
+}
+```
+
+### 6.5 SPI 注册文件
+
+```
+src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+```
+
+```
+com.youlai.boot.framework.security.autoconfigure.SecurityAutoConfiguration
+com.youlai.boot.framework.security.autoconfigure.JwtTokenAutoConfiguration
+com.youlai.boot.framework.security.autoconfigure.RedisTokenAutoConfiguration
+```
+
+> **Spring Boot 3.x+** 使用 `AutoConfiguration.imports`(非 `spring.factories`)。本项目 Spring Boot 4.0,必须使用此方式。
+
+### 6.6 配置属性
+
+```yaml
+# application.yml(使用方配置示例)
+security:
+ enabled: true
+ session:
+ type: jwt # jwt | redis-token
+ access-token-time-to-live: 7200
+ refresh-token-time-to-live: 604800
+ jwt:
+ secret-key: ${JWT_SECRET:replace-with-strong-secret}
+ redis-token:
+ allow-multi-login: true
+ ignore-urls:
+ - /api/v1/auth/login/**
+ - /api/v1/auth/refresh-token
+ unsecured-urls:
+ - /doc.html
+ - /swagger-ui/**
+ - /v3/api-docs/**
+```
+
+```java
+// config/SecurityProperties.java(Starter 内)
+@Data
+@ConfigurationProperties(prefix = "security")
+public class SecurityProperties {
+ private boolean enabled = true;
+ private SessionConfig session = new SessionConfig();
+ private String[] ignoreUrls = new String[0];
+ private String[] unsecuredUrls = new String[0];
+
+ @Data
+ public static class SessionConfig {
+ private String type = "jwt";
+ private Integer accessTokenTimeToLive = 7200;
+ private Integer refreshTokenTimeToLive = 604800;
+ private JwtConfig jwt = new JwtConfig();
+ private RedisTokenConfig redisToken = new RedisTokenConfig();
+ }
+
+ @Data
+ public static class JwtConfig {
+ private String secretKey;
+ }
+
+ @Data
+ public static class RedisTokenConfig {
+ private Boolean allowMultiLogin = true;
+ }
+}
+```
+
+### 6.7 枚举下沉处理
+
+`UserAuthenticationPort.getAuthInfoByOpenid(SocialPlatformEnum, String)` 必须引用公共枚举。定义端口前先处理:
+
+**方案**:将 `SocialPlatformEnum` 从 `youlai-system` 下沉到 `youlai-common`:
+
+```
+# 之前
+youlai-system/src/main/java/com/youlai/boot/system/enums/SocialPlatformEnum.java
+
+# 之后
+youlai-common/src/main/java/com/youlai/boot/common/enums/SocialPlatformEnum.java
+```
+
+同步修改所有引用方的 import 路径。Starter 的 `UserAuthenticationPort` 只引用 `com.youlai.boot.common.enums.SocialPlatformEnum`。
+
+### 6.8 接入契约摘要
+
+**使用方 pom.xml**:
+
+```xml
+
+ com.youlai
+ youlai-security-spring-boot-starter
+ 4.3.1
+
+```
+
+**使用方实现 Adapter**(必须):
+
+```java
+// 使用方的 system 模块必须提供两个适配器
+@Component
+public class MyUserAuthenticationAdapter implements UserAuthenticationPort { ... }
+
+@Component
+public class MyPermissionAdapter implements PermissionPort { ... }
+```
+
+> 如果使用方未提供 `UserAuthenticationPort` 或 `PermissionPort` 的 Bean,Starter 启动时会因构造器注入失败而报错——这是**预期行为**,强制使用方实现契约。
+
+### 6.9 迁移路径
+
+```
+阶段 1:端口/适配器解耦(第四节/第五节)
+ ↓
+阶段 2:枚举下沉(SocialPlatformEnum → youlai-common)
+ ↓
+阶段 3:创建 youlai-security-spring-boot-starter 模块
+ ↓
+阶段 4:将 framework/security 代码迁移至 Starter 模块
+ ↓
+阶段 5:编写自动配置类 + SPI 文件
+ ↓
+阶段 6:youlai-framework 移除 security 子包,依赖改为引用 Starter
+ ↓
+阶段 7:youlai-system 的 Adapter 确认无变化(实现同一端口接口)
+ ↓
+阶段 8:集成测试 → 发布
+```
+
+### 6.10 Starter 边界界定
+
+| 归属 Starter | 归属使用方(system 模块) | 归属 youlai-common |
+|:---:|:---:|:---:|
+| `port/` 端口接口 | `adapter/` 适配器实现 | `SecurityConstants` 常量 |
+| `model/` 安全模型 | `SecurityConfig` Web 安全配置 | `SocialPlatformEnum` 枚举 |
+| `service/` SecurityUserDetailsService、PermissionService | `provider/` 认证 Provider | `Result` 统一响应 |
+| `token/` Token 管理器 | | `enums/` 公共枚举 |
+| `filter/TokenAuthenticationFilter` | `filter/CaptchaValidationFilter` | |
+| `exception/` 安全异常 | `AuthenticationEntryPoint`、`AccessDeniedHandler` | |
+| `util/` SecurityUtils | | |
+| `autoconfigure/` 自动配置 | | |
+
+> **注意**:`SecurityConfig`(`@EnableWebSecurity` 配置类)归使用方,因为不同项目的安全规则(放行路径、CORS 等)不同,不应由 Starter 强制装配。Starter 仅提供 `SecurityProperties` 供使用方读取配置。
+
+> **异常响应边界**:Starter 不提供 `MyAuthenticationEntryPoint`、`MyAccessDeniedHandler`、`ResponseWriter`。`TokenAuthenticationFilter` 只负责解析 Token 和填充 `SecurityContext`;无效 Token 抛出 Spring Security `AuthenticationException`。最终 HTTP 状态码、JSON 结构、错误码由使用方的 `AuthenticationEntryPoint` 和 `AccessDeniedHandler` 决定。
+
+### 6.11 实操步骤(命令行操作)
+
+> 以下以多模块项目 `youlai-boot-multi` 为例,演示从零创建 Starter 模块到代码迁移的完整命令行操作。单模块项目参考第四节先完成端口解耦,再按 6.11.3 起的步骤操作。
+
+#### 6.11.1 重命名 UserAuthInfo → SecurityUser
+
+```powershell
+# Windows PowerShell
+cd d:\Project\youlai-admin\youlai-boot-multi\youlai-framework
+
+# 重命名文件
+Rename-Item -Path "src\main\java\com\youlai\boot\framework\security\model\UserAuthInfo.java" `
+ -NewName "SecurityUser.java"
+```
+
+```bash
+# Linux/macOS
+cd d:/Project/youlai-admin/youlai-boot-multi/youlai-framework
+mv src/main/java/com/youlai/boot/framework/security/model/UserAuthInfo.java \
+ src/main/java/com/youlai/boot/framework/security/model/SecurityUser.java
+```
+
+然后用 IDE 全局替换(Ctrl+Shift+R):
+- 类名 `UserAuthInfo` → `SecurityUser`
+- 检查所有 `import ...UserAuthInfo` → `import ...SecurityUser`
+- 更新 Javadoc 中的 `@author` 等引用
+
+#### 6.11.2 创建 Starter Maven 模块
+
+```powershell
+# 在多模块项目根目录下创建新模块目录
+cd d:\Project\youlai-admin\youlai-boot-multi
+
+mkdir youlai-security-spring-boot-starter
+mkdir youlai-security-spring-boot-starter\src\main\java\com\youlai\boot\framework\security
+mkdir youlai-security-spring-boot-starter\src\main\resources\META-INF\spring
+```
+
+创建 `youlai-security-spring-boot-starter/pom.xml`(内容见 6.3 节)。
+
+#### 6.11.3 在根 POM 注册新模块
+
+```xml
+
+youlai-security-spring-boot-starter
+```
+
+#### 6.11.4 迁移代码到 Starter 模块
+
+```powershell
+# 将 framework/security 下的代码移动到 Starter 模块
+$src = "youlai-framework\src\main\java\com\youlai\boot\framework\security"
+$dst = "youlai-security-spring-boot-starter\src\main\java\com\youlai\boot\framework\security"
+
+# 移动 Starter 内核子包
+Move-Item -Path "$src\port" -Destination "$dst\port" -Force
+Move-Item -Path "$src\model" -Destination "$dst\model" -Force
+Move-Item -Path "$src\service" -Destination "$dst\service" -Force
+Move-Item -Path "$src\token" -Destination "$dst\token" -Force
+Move-Item -Path "$src\exception" -Destination "$dst\exception" -Force
+Move-Item -Path "$src\util" -Destination "$dst\util" -Force
+
+# 只迁移 TokenAuthenticationFilter,CaptchaValidationFilter 留在使用方
+mkdir "$dst\filter"
+Move-Item -Path "$src\filter\TokenAuthenticationFilter.java" -Destination "$dst\filter\TokenAuthenticationFilter.java" -Force
+
+# 只迁移 SecurityProperties,PasswordEncoder 由 SecurityAutoConfiguration 提供
+mkdir "$dst\config"
+Move-Item -Path "$src\config\SecurityProperties.java" -Destination "$dst\config\SecurityProperties.java" -Force
+
+# 新建 autoconfigure 包
+mkdir "$dst\autoconfigure"
+
+# 移动 resources
+Move-Item -Path "youlai-framework\src\main\resources\..." `
+ -Destination "youlai-security-spring-boot-starter\src\main\resources\..." -Force
+```
+
+> **关键**:`provider/`、`handler/`、`CaptchaValidationFilter`、`PasswordEncoderConfig` **不迁移**。Provider 和验证码属于使用方业务流程;handler 和 `ResponseWriter` 属于使用方响应格式;PasswordEncoder Bean 由 `SecurityAutoConfiguration` 统一提供。
+
+#### 6.11.5 创建自动配置类和 SPI 文件
+
+```powershell
+# 创建 3 个自动配置类(内容见 6.4 节)
+# SecurityAutoConfiguration.java
+# JwtTokenAutoConfiguration.java
+# RedisTokenAutoConfiguration.java
+
+# 创建 SPI 注册文件
+$spiPath = "youlai-security-spring-boot-starter\src\main\resources\META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports"
+Set-Content -Path $spiPath -Encoding UTF8 -Value @"
+com.youlai.boot.framework.security.autoconfigure.SecurityAutoConfiguration
+com.youlai.boot.framework.security.autoconfigure.JwtTokenAutoConfiguration
+com.youlai.boot.framework.security.autoconfigure.RedisTokenAutoConfiguration
+"@
+```
+
+#### 6.11.6 修改 youlai-framework 的 pom.xml
+
+```xml
+
+
+
+ com.youlai
+ youlai-common
+
+
+
+
+ com.youlai
+ youlai-security-spring-boot-starter
+
+
+
+
+
+```
+
+#### 6.11.7 枚举下沉
+
+```powershell
+# 将 SocialPlatformEnum 从 youlai-system 移到 youlai-common
+$src = "youlai-system\src\main\java\com\youlai\boot\system\enums\SocialPlatformEnum.java"
+$dst = "youlai-common\src\main\java\com\youlai\boot\common\enums\SocialPlatformEnum.java"
+
+# 确保目标目录存在
+mkdir "youlai-common\src\main\java\com\youlai\boot\common\enums" -Force
+Move-Item -Path $src -Destination $dst -Force
+```
+
+然后用 IDE 全局替换包名:
+- `com.youlai.boot.system.enums.SocialPlatformEnum` → `com.youlai.boot.common.enums.SocialPlatformEnum`
+
+#### 6.11.8 验证编译
+
+```powershell
+cd d:\Project\youlai-admin\youlai-boot-multi
+
+# 编译全部模块
+mvn clean compile -pl youlai-security-spring-boot-starter,youlai-common,youlai-system,youlai-framework -am
+
+# 确认 Starter 模块零 system 依赖(搜索结果应为空)
+Select-String -Path "youlai-security-spring-boot-starter\src\**\*.java" `
+ -Pattern "import com\.youlai\.boot\.system\." -SimpleMatch
+```
+
+> 如果最后一条命令有输出,说明 Starter 仍存在 system 依赖,需排查并消除。
+
+#### 6.11.9 验证自动装配
+
+```powershell
+# 启动应用,观察日志中是否出现自动配置加载信息
+mvn spring-boot:run -pl youlai-application
+
+# 或在测试中验证
+mvn test -pl youlai-application -Dtest=SecurityAutoConfigurationTest
+```
+
+---
+
+### 6.12 发布到 Maven Central
+
+> 以下流程基于 Sonatype Central Publisher Portal。发布凭据使用 Portal User Token,不使用旧发布流程。
+
+#### 6.12.1 前置准备
+
+| 准备项 | 说明 | 获取方式 |
+|--------|------|----------|
+| **Sonatype 账号** | 发布构件的账号 | 注册 https://central.sonatype.com |
+| **GPG 密钥** | 签名构件 | `gpg --gen-key` 生成 |
+| **Namespace 所有权验证** | 证明你拥有该 groupId | 在 Central Portal 添加 namespace 并按提示完成 DNS 或 GitHub 验证 |
+| **Maven settings.xml** | 配置 Central Portal User Token | `~/.m2/settings.xml` |
+
+#### 6.12.2 生成并上传 GPG 密钥
+
+```bash
+# 生成 GPG 密钥
+gpg --gen-key
+# 按提示输入姓名、邮箱、密码
+
+# 查看密钥 ID
+gpg --list-keys
+# 输出示例:rsa3072/ABCDEF1234567890
+
+# 上传公钥到密钥服务器(Maven Central 要求)
+gpg --keyserver keyserver.ubuntu.com --send-keys ABCDEF1234567890
+# 备用服务器
+gpg --keyserver keys.openpgp.org --send-keys ABCDEF1234567890
+gpg --keyserver pgp.mit.edu --send-keys ABCDEF1234567890
+```
+
+#### 6.12.3 配置 settings.xml
+
+```xml
+
+
+
+
+ central
+ 你的 Central Portal token username
+ 你的 Central Portal token password
+
+
+
+
+
+ central
+
+ gpg
+ 你的GPG密码
+
+
+
+
+
+ central
+
+
+```
+
+#### 6.12.4 配置 Starter 的 pom.xml(发布相关)
+
+在 `youlai-security-spring-boot-starter/pom.xml` 的 `` 下追加:
+
+```xml
+
+youlai Security Spring Boot Starter
+认证鉴权 Spring Boot Starter,支持 JWT / Redis Token 双模式
+https://github.com/youlaitech/youlai-starter
+
+
+
+ The Apache Software License, Version 2.0
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+
+
+
+
+
+ youlai
+ youlai
+ youlai@example.com
+ https://github.com/youlaitech
+
+
+
+
+ https://github.com/youlaitech/youlai-starter
+ scm:git:git://github.com/youlaitech/youlai-starter.git
+ scm:git:ssh://github.com/youlaitech/youlai-starter.git
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.1
+
+
+ attach-sources
+ jar-no-fork
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.6.3
+
+
+ attach-javadocs
+ jar
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.1.0
+
+
+ sign-artifacts
+ verify
+ sign
+
+
+
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.11.0
+ true
+
+ central
+ true
+
+
+
+
+```
+
+> **注意**:Central Portal 要求发布包包含 sources、javadocs、GPG 签名和完整 POM 元信息。`central-publishing-maven-plugin` 负责上传与发布,不负责自动补齐这些元信息。
+
+#### 6.12.5 执行发布
+
+```bash
+cd d:/Project/youlai-admin/youlai-boot-multi/youlai-security-spring-boot-starter
+
+# 1. 编译 + 测试 + 签名 + 发布(一条命令)
+mvn clean deploy -P central
+
+# 2. 发布后登录 https://central.sonatype.com 检查状态
+# 状态变为 "Published" 后,约 15-30 分钟同步到 Maven Central
+```
+
+```bash
+# 验证已发布(等待同步后)
+# 浏览器访问:
+# https://repo1.maven.org/maven2/com/youlai/youlai-security-spring-boot-starter/
+```
+
+#### 6.12.6 快照版本(SNAPSHOT)发布
+
+开发阶段可发布 SNAPSHOT 版本供其他项目测试:
+
+```xml
+
+4.3.2-SNAPSHOT
+```
+
+```bash
+# SNAPSHOT 发布到 Central Portal Snapshots
+mvn clean deploy -P central
+
+# 使用方添加快照仓库
+```
+
+```xml
+
+
+
+ central-snapshots
+ https://central.sonatype.com/repository/maven-snapshots/
+ true
+
+
+```
+
+---
+
+### 6.13 使用方式(其他项目接入)
+
+#### 6.13.1 引入依赖
+
+```xml
+
+
+
+
+ com.youlai
+ youlai-security-spring-boot-starter
+ 1.0.0
+
+
+```
+
+#### 6.13.2 配置 application.yml
+
+```yaml
+security:
+ enabled: true
+ session:
+ type: jwt # jwt | redis-token
+ access-token-time-to-live: 7200
+ refresh-token-time-to-live: 604800
+ jwt:
+ secret-key: ${JWT_SECRET:your-strong-secret-key-here}
+ redis-token:
+ allow-multi-login: true
+ ignore-urls:
+ - /api/v1/auth/login/**
+ - /api/v1/auth/refresh-token
+ unsecured-urls:
+ - /doc.html
+ - /swagger-ui/**
+ - /v3/api-docs/**
+```
+
+#### 6.13.3 实现适配器(必须)
+
+```java
+// 使用方必须提供两个适配器 Bean,否则启动失败
+@Component
+public class MyUserAuthenticationAdapter implements UserAuthenticationPort {
+
+ @Autowired
+ private MyUserService myUserService; // 使用方自己的用户服务
+
+ @Override
+ public SecurityUser getAuthInfoByUsername(String username) {
+ // 调用使用方自己的用户查询逻辑
+ MyUserEntity user = myUserService.findByUsername(username);
+ if (user == null) return null;
+
+ SecurityUser securityUser = new SecurityUser();
+ securityUser.setUserId(user.getId());
+ securityUser.setUsername(user.getUsername());
+ securityUser.setPassword(user.getPassword());
+ securityUser.setStatus(user.getStatus());
+ securityUser.setDeptId(user.getDeptId());
+ securityUser.setRoles(user.getRoleCodes());
+ securityUser.setDataScopes(user.getDataScopes());
+ return securityUser;
+ }
+
+ @Override
+ public SecurityUser getAuthInfoByMobile(String mobile) {
+ // ... 实现手机号查询
+ }
+
+ @Override
+ public SecurityUser getAuthInfoByOpenid(SocialPlatformEnum platform, String openid) {
+ // ... 实现第三方登录查询(如不支持可 return null)
+ }
+}
+
+@Component
+public class MyPermissionAdapter implements PermissionPort {
+
+ @Autowired
+ private MyRoleMenuService roleMenuService;
+
+ @Override
+ public Set getRolePerms(Set roleCodes) {
+ return roleMenuService.findPermsByRoleCodes(roleCodes);
+ }
+}
+```
+
+#### 6.13.4 编写 SecurityConfig(使用方自定义安全规则)
+
+```java
+@Configuration
+@EnableWebSecurity
+@EnableMethodSecurity
+public class SecurityConfig {
+
+ @Autowired
+ private TokenAuthenticationFilter tokenAuthenticationFilter; // Starter 提供
+
+ @Autowired
+ private AuthenticationEntryPoint authenticationEntryPoint; // 使用方提供
+
+ @Autowired
+ private AccessDeniedHandler accessDeniedHandler; // 使用方提供
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ http
+ .csrf(csrf -> csrf.disable())
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .authorizeHttpRequests(auth -> auth
+ .requestMatchers("/api/v1/auth/login").permitAll()
+ .requestMatchers("/api/v1/auth/captcha").permitAll()
+ .anyRequest().authenticated()
+ )
+ .exceptionHandling(ex -> ex
+ .authenticationEntryPoint(authenticationEntryPoint)
+ .accessDeniedHandler(accessDeniedHandler)
+ )
+ // 必须位于 ExceptionTranslationFilter 之后、AuthorizationFilter 之前,
+ // 这样 TokenAuthenticationFilter 抛出的 AuthenticationException 会由
+ // AuthenticationEntryPoint 统一处理。
+ .addFilterBefore(tokenAuthenticationFilter, AuthorizationFilter.class);
+ return http.build();
+ }
+}
+```
+
+使用方需要提供 `AuthenticationEntryPoint` 和 `AccessDeniedHandler` Bean。youlai 项目可以在这两个 Bean 内继续使用自己的 `ResponseWriter` 输出统一 `Result`;其他项目可以返回空 401/403、Problem Details 或自有 JSON。Starter 不关心响应格式。
+
+#### 6.13.5 验证接入成功
+
+```java
+// 启动应用,日志应显示自动配置已加载:
+// ... SecurityAutoConfiguration matched
+// ... JwtTokenAutoConfiguration matched(或 RedisTokenAutoConfiguration)
+
+// 调用登录接口测试
+// POST /api/v1/auth/login → 返回 token
+// GET /api/v1/sys/users(带 token + @PreAuthorize("@ss.hasPerm('sys:user:list')"))→ 200 或 403
+```
+
+---
+
+### 6.14 迁移路径总览
+
+```
+阶段 1:P1-P3 端口/适配器解耦(第四节/第五节)
+ │
+ ├── UserAuthInfo 重命名为 SecurityUser
+ ├── 枚举下沉 SocialPlatformEnum → youlai-common
+ └── 删除空占位文件、UserSession
+ │
+ ↓
+阶段 2:创建 Starter 模块(6.11 实操步骤)
+ │
+ ├── 6.11.1 重命名 UserAuthInfo → SecurityUser
+ ├── 6.11.2 创建 Starter 模块 + pom.xml
+ ├── 6.11.3 根 POM 注册模块
+ ├── 6.11.4 迁移代码到 Starter
+ ├── 6.11.5 创建自动配置类 + SPI 文件
+ ├── 6.11.6 youlai-framework pom.xml 改为引用 Starter
+ ├── 6.11.7 枚举下沉
+ ├── 6.11.8 验证编译 + 零 system 依赖
+ └── 6.11.9 验证自动装配
+ │
+ ↓
+阶段 3:发布到 Maven Central(6.12)
+ │
+ ├── 6.12.1 Sonatype 账号 + GPG 密钥
+ ├── 6.12.2 上传 GPG 公钥
+ ├── 6.12.3 配置 settings.xml
+ ├── 6.12.4 配置 pom.xml 发布插件
+ ├── 6.12.5 mvn clean deploy
+ └── 6.12.6 验证 Maven Central
+ │
+ ↓
+阶段 4:其他项目接入(6.13)
+ │
+ ├── 6.13.1 引入依赖
+ ├── 6.13.2 配置 application.yml
+ ├── 6.13.3 实现适配器
+ ├── 6.13.4 编写 SecurityConfig
+ └── 6.13.5 验证接入
+```
+
+---
+
+## 七、测试策略
+
+### 7.1 单元测试
+
+| 测试目标 | Mock 对象 | 验证点 |
+|----------|----------|--------|
+| `SecurityUserDetailsService` | `UserAuthenticationPort` | 返回正确 `SecurityUserDetails`;用户不存在抛 `UsernameNotFoundException` |
+| `PermissionService` | `PermissionPort` | 有权限返回 true;无权限返回 false;超管放行 |
+| `UserAuthenticationAdapter` | `UserService`、`UserSocialService` | 委托调用正确;返回值转换正确 |
+| `PermissionAdapter` | `RoleMenuService` | 委托调用正确 |
+| `RedisTokenManager` | `RedisTemplate`、`JsonMapper` | 存取 `SecurityUserDetails` 正确;`password` 为 null |
+| `JwtTokenManager` | — | JWT 签发/解析 `roles` 正确;`getAuthorities()` 带 `ROLE_` 前缀 |
+
+### 7.2 集成测试
+
+- 登录流程:用户名密码 → 生成 token → 解析 token → 权限校验
+- Redis-Token 模式:登录 → Redis 存储 → 解析还原 → 踢人
+- JWT 模式:登录 → JWT 签发 → 解析 → tokenVersion 失效
+- SpEL 权限:`@PreAuthorize("@ss.hasPerm('sys:user:create')")` 通过/拒绝
+
+### 7.3 回归测试用例
+
+- **用户认证**:正确用户名/密码登录成功;错误用户名或密码登录失败
+- **用户禁用**:用户状态为禁用时,登录应被拒绝(`isEnabled()` 返回 `false`)
+- **角色加载**:登录成功后,`SecurityUserDetails.getAuthorities()` 获得正确的角色列表(前缀 `ROLE_`)
+- **权限校验**:`@PreAuthorize` 或 `ss.hasPerm` 表达式,有权限放行、无权限拒绝
+- **微信登录**:通过 `UserAuthenticationPort.getAuthInfoByOpenid` 测试社交登录
+- **边界情况**:角色集合为空、权限集合为空、端口实现抛异常
+- **事务和并发**:高并发登录、权限检查,确保端口实现线程安全
+- **登录态失效**:发布后旧 JWT / Redis Token 均返回 401,前端跳转登录页
+
+---
+
+## 八、实施阶段
+
+| 阶段 | 内容 | 风险 | 可独立发布 |
+|------|------|------|:---:|
+| **P0** | 多模块 parent 版本统一;确认 `SocialPlatformEnum` 下沉;确认旧 Token 发布失效策略 | 低(前置整理) | ✓ |
+| **P1** | `SecurityUserDetails` 改 `roles` + 删 `UserSession` + `RedisTokenManager` 简化 + `SecurityUtils` 简化 + 删除 4 个空占位文件 | 低(内部改造,接口不变) | ✓ |
+| **P2** | `JwtTokenManager` 适配 `roles` | 中(需回归 JWT 登录流程) | ✓ |
+| **P3** | 端口/适配器解耦 + 枚举下沉 | 中(影响面大,需全量回归) | ✓ |
+| **P4** | Security Starter 抽离(6.11 实操) | 高(模块拆分,需完整回归) | ✓ |
+| **P5** | 发布到 Maven Central(6.12) | 中(发布流程,不影响代码) | ✓ |
+
+> P0 必须先完成。P1、P2 可合并发布(模型改造一起做),P3 单独发布,P4 在 P3 稳定后执行,P5 在 P4 验证通过后执行。
+
+---
+
+## 九、AI 协作指南
+
+> **本节为 AI 修改 security 代码的操作规范。任何 AI 在修改前必须阅读。**
+
+### 9.1 修改前必读检查
+
+```
+□ 已阅读第〇节「文档使用指南」
+□ 已阅读第二节「设计决策」,确认不违背 D1-D8
+□ 已确认当前代码处于哪个实施阶段(P0/P1/P2/P3/P4/P5)
+□ 已确认是单模块还是多模块项目
+```
+
+### 9.2 代码修改检查清单
+
+```
+□ framework/security 下的代码不含 import com.youlai.boot.system.*
+□ 端口接口在 framework.security.port 包,无 system 依赖
+□ 适配器在 system.security.adapter 包,标注 @Component
+□ 端口返回模型使用 SecurityUser(由 UserAuthInfo 重命名)
+□ SecurityUserDetails.roles 为 Set,getAuthorities() 实时计算
+□ UserSession.java 已删除,无残留引用
+□ RedisTokenManager 存取 SecurityUserDetails,password 置 null
+□ JwtTokenManager parseToken 设 roles,JWT claims 格式不变
+□ SecurityUtils.getRoles() 直接取 roles 字段,无 ROLE_ strip
+□ 所有新增接口和类有 Javadoc 注释
+□ 端口接口 @see 注释引用适配器类(方便定位实现)
+□ 多模块:youlai-framework/pom.xml 不依赖 youlai-system
+```
+
+### 9.3 新增端口/适配器决策树
+
+```
+需要安全模块调用 system 模块的新功能?
+│
+├─ 是 → 是否能通过现有端口完成?
+│ │
+│ ├─ 能 → 在现有端口接口添加方法,适配器实现
+│ └─ 不能 → 需要新端口
+│ │
+│ ├─ 功能是否属于认证鉴权最小内核?
+│ │ ├─ 是 → 新增核心端口(framework.security.port)
+│ │ └─ 否 → 不加入 Security Starter,由业务模块自行实现
+│ │
+│ └─ 命名:<领域意图>Port,如 AuditLogPort
+│
+└─ 否 → 不需要端口,直接在 framework 内实现
+```
+
+### 9.4 常见问题
+
+**Q: 为什么不用 `@Autowired` 而用构造器注入?**
+A: 构造器注入(`@RequiredArgsConstructor` + `final`)保证依赖不可变、非空,符合 Spring 官方推荐。
+
+**Q: 端口接口需要标注 `@Component` 吗?**
+A: 不需要。端口是接口,由适配器实现类标注 `@Component`,Spring 自动按类型注入。
+
+**Q: `SocialPlatformEnum` 为什么必须下沉到 common?**
+A: 端口接口 `UserAuthenticationPort` 引用了它。如果端口在 Starter 模块而枚举在 system 模块,Starter 编译期就会依赖 system——违背解耦目标。
+
+**Q: 可以在端口接口返回 system 实体类吗?**
+A: **禁止**。端口返回类型必须为 `SecurityUser`(纯 POJO)或 JDK 基础类型(`Set` 等)。适配器负责实体 → POJO 转换。
+
+**Q: 为什么 `UserAuthInfo` 要重命名为 `SecurityUser`?**
+A: 两点原因:① 语义——`UserAuthInfo` 偏向"认证信息",但该模型实际承载用户安全数据(角色、权限范围、部门等),`SecurityUser` 更准确;② 作为 Starter 公开 API,`SecurityUser` 比 `UserAuthInfo` 更中性、更规范,其他项目接入时更直观。重命名而非新建,改动最小。
+
+**Q: `SecurityUser` 和 `SecurityUserDetails` 有什么区别?**
+A: `SecurityUser` 是端口返回的安全数据 POJO(含 status、nickname 等原始字段);`SecurityUserDetails` 是 Spring Security `UserDetails` 实现(含 enabled、roles 等认证字段)。`SecurityUserDetails` 由 `SecurityUser` 构造,数据流:`Adapter 查 DB → SecurityUser → SecurityUserDetails`。
+
+**Q: 新增了一个安全过滤器,放在哪?**
+A: 放在 `framework/security/filter/`(单模块)或 Starter 的 `filter/` 包。过滤器属于框架基础设施,不依赖 system。
+
+**Q: `SecurityConfig` 应该放在哪?**
+A: 放在使用方(system 模块或 application 模块)。不同项目安全规则不同,不应由 Starter 强制装配。Starter 仅提供 `SecurityProperties`。
+
+**Q: 如何确认重构后 framework 层零 system 依赖?**
+A: 在 `framework/security` 目录执行全局搜索 `import com.youlai.boot.system`,结果应为空(`SocialPlatformEnum` 下沉后)。
+
+### 9.5 文档修改规范
+
+1. **修改本文档时**:更新顶部「最后更新」日期,并在附录 B 变更日志追加记录
+2. **新增设计决策时**:在第〇节 0.3 关键决策表追加 `D7`、`D8`... 编号,不得修改已有决策编号
+3. **新增端口时**:更新第二节 2.2 端口定义表、第六节 6.10 边界界定表
+4. **完成实施阶段时**:在第八节对应阶段标注「✅ 已完成」及日期
+5. **代码示例更新时**:保持与实际代码同步,注释标注 `// 改动点:` 说明差异
+
+### 9.6 AI 提示词模板
+
+当需要让 AI 执行 security 重构时,可使用以下提示词:
+
+```
+请按照 docs/security-refactor-plan.md 执行 security 模块重构。
+
+当前阶段:P3(端口/适配器解耦)
+项目类型:单模块(youlai-boot)
+
+要求:
+1. 先阅读第〇节「文档使用指南」和第二节「设计决策」
+2. 按第四节「单模块重构」执行
+3. 完成后对照第十节「代码审查清单」自查
+4. 不违背 D1-D8 任何决策
+5. 删除 4 个空占位文件(见 0.4 节)
+```
+
+---
+
+## 十、代码审查清单
+
+### 10.1 模型改造(P1/P2)
+
+- [ ] `SecurityUserDetails` 的 `roles` 字段为 `Set`,`getAuthorities()` 实时计算
+- [ ] `UserSession.java` 已删除,无残留引用
+- [ ] `RedisTokenManager` 存取 `SecurityUserDetails`,`password` 置 null
+- [ ] `JwtTokenManager` 的 `parseToken` 设 `roles`,JWT claims 格式不变
+- [ ] `SecurityUtils.getRoles()` 直接取 `roles` 字段,无 `ROLE_` strip 逻辑
+- [ ] 4 个空占位文件已删除(`UserAuthQueryService`、`RolePermissionService`、`WxMaUserAuthQueryService`、`WxMaBindInfo`)
+
+### 10.2 端口/适配器解耦(P3)
+
+- [ ] `SecurityUserDetailsService` 不含 `import com.youlai.boot.system.*`
+- [ ] `PermissionService` 不含 `import com.youlai.boot.system.*`
+- [ ] 端口接口在 `framework.security.port` 包下,无 system 依赖
+- [ ] 适配器在 `system.security.adapter` 包下,标注 `@Component`
+- [ ] 端口接口有 `@see` 注释引用适配器类
+- [ ] `SocialPlatformEnum` 已下沉到 `youlai-common`(P3/Starter 阶段)
+- [ ] `SmsAuthenticationProvider`、`WxMaAuthenticationProvider` 留在使用方,未迁入 Starter
+
+### 10.3 Starter 抽离(P4)
+
+- [ ] `UserAuthInfo` 已重命名为 `SecurityUser`,所有引用已更新
+- [ ] `youlai-security-spring-boot-starter` 模块已创建
+- [ ] `pom.xml` 固定包含 Security、Web、Redis、Hutool JWT 依赖
+- [ ] `AutoConfiguration.imports` 文件已创建,注册 3 个自动配置类
+- [ ] `JwtTokenAutoConfiguration` 使用 `@ConditionalOnClass` + `@ConditionalOnProperty`
+- [ ] `RedisTokenAutoConfiguration` 使用 `@ConditionalOnClass` + `@ConditionalOnProperty`
+- [ ] `SecurityConfig` 留在使用方,不在 Starter 中强制装配
+- [ ] `youlai-framework` 移除 security 子包,pom.xml 改为引用 Starter
+- [ ] `SocialPlatformEnum` 已下沉到 `youlai-common`
+- [ ] Starter 模块零 `import com.youlai.boot.system.*`(已验证)
+
+### 10.4 Starter 发布
+
+- [ ] Sonatype Central Portal 账号已注册,namespace/GroupId 已验证
+- [ ] GPG 密钥已生成并上传到 3 个密钥服务器
+- [ ] `settings.xml` 已配置 `central` server 凭据
+- [ ] `pom.xml` 已配置 `maven-source-plugin`、`maven-javadoc-plugin`、`maven-gpg-plugin`
+- [ ] `pom.xml` 已配置 `central-publishing-maven-plugin`(新方式)
+- [ ] `pom.xml` 已配置 ``、``、`` 元信息
+- [ ] `mvn clean deploy -P central` 执行成功
+- [ ] Maven Central 可搜索到构件(https://central.sonatype.com)
+
+### 10.5 通用规范
+
+- [ ] 所有新增接口和类有 Javadoc 注释(中文)
+- [ ] 端口方法签名包含 `@param`、`@return`、`@throws` 注释
+- [ ] 使用构造器注入(`@RequiredArgsConstructor` + `final`)
+- [ ] 日志不输出密码、密钥等敏感信息
+- [ ] 单元测试覆盖端口调用和适配器委托
+- [ ] 多模块:`youlai-framework/pom.xml` 不依赖 `youlai-system`
+- [ ] 多模块:parent 版本与根 POM 一致(4.3.1)
+
+---
+
+## 附录 A:术语表
+
+| 术语 | 含义 |
+|------|------|
+| **Port(端口)** | 安全模块定义的抽象接口,描述需要外部提供的功能。放在 `framework.security.port` 包。 |
+| **Adapter(适配器)** | system 模块中对 Port 接口的具体实现,委托 system Service 完成实际操作。放在 `system.security.adapter` 包。 |
+| **Ports & Adapters** | 六边形架构模式,核心业务通过端口与外部通信,外部实现通过适配器连入。 |
+| **SecurityUser** | 安全模块的用户安全数据 POJO,纯 JDK 类型,无 system 依赖。端口接口的返回类型。由原 `UserAuthInfo` 重命名而来(D2 决策)。 |
+| **SecurityUserDetails** | Spring Security `UserDetails` 实现,封装认证后的用户信息。`roles` 字段为 `Set`。 |
+| **UserSession** | (待删除)Redis 存储的会话中间模型,字段是 `SecurityUserDetails` 的子集。 |
+| **TokenManager** | Token 管理器接口,有 `JwtTokenManager` 和 `RedisTokenManager` 两个实现。 |
+| **SecurityProperties** | 安全配置属性类,`@ConfigurationProperties(prefix = "security")`。 |
+| **Starter** | Spring Boot 自动装配模块,引入依赖即自动配置。本项目命名为 `youlai-security-spring-boot-starter`。 |
+| **AutoConfiguration.imports** | Spring Boot 3.x+ 的自动配置注册文件,位于 `META-INF/spring/` 目录。 |
+| **D1-D8** | 本文档第〇节定义的 8 条关键设计决策,不可违背。 |
+
+---
+
+## 附录 B:变更日志
+
+| 日期 | 版本 | 变更内容 |
+|------|------|----------|
+| 2026-06-29 | v1.0 | 初版 security-refactor-plan.md(模型消除 + 单/多模块教程) |
+| 2026-06-29 | v1.0 | deep-research-report.md 归档(Ports & Adapters 研究) |
+| 2026-07-04 | v2.0 | **合并两文档**:消除重复逻辑(统一模型为 SecurityUser、端口收敛、重复的模式介绍);新增第六节 Security Starter 抽离方案;新增第九节 AI 协作指南;补充空占位文件说明(0.4 节);补充枚举下沉方案(6.7 节);补充多模块版本不一致提醒(5.2 节) |
+| 2026-07-04 | v2.1 | **SecurityUser 重命名 + Starter 实操发布指南**:D2 决策更新(`UserAuthInfo` → `SecurityUser` 重命名,非新建);全文 `UserAuthInfo` 替换为 `SecurityUser`;新增第六节 6.11 实操步骤(命令行);新增 Starter 发布与使用方式 |
+| 2026-07-04 | v2.2 | **方案收敛为唯一施工路线**:删除 OnlineUserPort 与额外仓库分支;明确不接受旧 Token;JWT claims 统一为 `roles`;Starter 不依赖 `ResponseWriter`,异常响应交给使用方 `AuthenticationEntryPoint` / `AccessDeniedHandler`;配置项统一使用 `security.session.*` |
+
+---
+
+> **参考资料**:本方案基于 youlai-boot 项目 `framework/security` 与 `system` 模块的实际代码分析,结合 Ports & Adapters(六边形架构)模式和 Spring Boot Starter 自动装配规范设计。所有代码示例中的包名、类名、方法签名均与实际代码库对齐。
diff --git a/src/main/java/com/youlai/boot/framework/security/config/SecurityConfig.java b/src/main/java/com/youlai/boot/auth/security/config/SecurityConfig.java
similarity index 59%
rename from src/main/java/com/youlai/boot/framework/security/config/SecurityConfig.java
rename to src/main/java/com/youlai/boot/auth/security/config/SecurityConfig.java
index fa541460..597afd12 100644
--- a/src/main/java/com/youlai/boot/framework/security/config/SecurityConfig.java
+++ b/src/main/java/com/youlai/boot/auth/security/config/SecurityConfig.java
@@ -1,17 +1,19 @@
-package com.youlai.boot.framework.security.config;
+package com.youlai.boot.auth.security.config;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.hutool.core.util.ArrayUtil;
import com.youlai.boot.framework.captcha.service.CaptchaService;
-import com.youlai.boot.framework.security.filter.CaptchaValidationFilter;
+import com.youlai.boot.framework.security.config.SecurityProperties;
import com.youlai.boot.framework.security.filter.TokenAuthenticationFilter;
-import com.youlai.boot.framework.security.handler.MyAccessDeniedHandler;
-import com.youlai.boot.framework.security.handler.MyAuthenticationEntryPoint;
-import com.youlai.boot.framework.security.provider.SmsAuthenticationProvider;
-import com.youlai.boot.framework.security.provider.WxMaAuthenticationProvider;
+import com.youlai.boot.framework.security.port.UserAuthenticationPort;
+import com.youlai.boot.framework.security.service.SecurityUserDetailsService;
import com.youlai.boot.framework.security.token.TokenManager;
-import com.youlai.boot.framework.security.service.SysUserDetailsService;
-import com.youlai.boot.system.service.UserService;
+import com.youlai.boot.auth.security.filter.CaptchaValidationFilter;
+import com.youlai.boot.auth.security.handler.JsonAccessDeniedHandler;
+import com.youlai.boot.auth.security.handler.JsonAuthenticationEntryPoint;
+import com.youlai.boot.auth.security.provider.SmsAuthenticationProvider;
+import com.youlai.boot.auth.security.provider.WxMaAuthenticationProvider;
+import com.youlai.boot.system.service.UserSocialService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -29,9 +31,13 @@ import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.access.intercept.AuthorizationFilter;
/**
- * Spring Security 配置类
+ * Spring Security 配置类。
+ *
+ * 归使用方(auth 模块),安全规则(放行路径、CORS、Provider 装配、响应格式)
+ * 因项目而异,不应由框架层强制装配。
*
* @author Ray.Hao
* @since 4.3.1
@@ -44,57 +50,41 @@ public class SecurityConfig {
private final RedisTemplate redisTemplate;
private final PasswordEncoder passwordEncoder;
-
private final TokenManager tokenManager;
- private final UserService userService;
- private final SysUserDetailsService userDetailsService;
-
+ private final SecurityUserDetailsService userDetailsService;
private final CaptchaService captchaService;
private final SecurityProperties securityProperties;
- /**
- * 配置安全过滤链 SecurityFilterChain
- */
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(requestMatcherRegistry -> {
- // 配置无需登录即可访问的公开接口(配置文件方式)
String[] ignoreUrls = securityProperties.getIgnoreUrls();
if (ArrayUtil.isNotEmpty(ignoreUrls)) {
requestMatcherRegistry.requestMatchers(ignoreUrls).permitAll();
}
- // 其他所有请求需登录后访问
requestMatcherRegistry.anyRequest().authenticated();
}
)
.exceptionHandling(configurer ->
configurer
- .authenticationEntryPoint(new MyAuthenticationEntryPoint()) // 未认证异常处理器
- .accessDeniedHandler(new MyAccessDeniedHandler()) // 无权限访问异常处理器
+ .authenticationEntryPoint(new JsonAuthenticationEntryPoint())
+ .accessDeniedHandler(new JsonAccessDeniedHandler())
)
-
- // 禁用默认的 Spring Security 特性,适用于前后端分离架构
.sessionManagement(configurer ->
- configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态认证,不使用 Session
+ configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
- .csrf(AbstractHttpConfigurer::disable) // 禁用 CSRF 防护,前后端分离无需此防护机制
- .formLogin(AbstractHttpConfigurer::disable) // 禁用默认的表单登录功能,前后端分离采用 Token 认证方式
- .httpBasic(AbstractHttpConfigurer::disable) // 禁用 HTTP Basic 认证,避免弹窗式登录
- // 禁用 X-Frame-Options 响应头,允许页面被嵌套到 iframe 中
+ .csrf(AbstractHttpConfigurer::disable)
+ .formLogin(AbstractHttpConfigurer::disable)
+ .httpBasic(AbstractHttpConfigurer::disable)
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
- // 验证码校验过滤器
+ // 验证码校验(使用方过滤器,直接写 JSON 响应)
.addFilterBefore(new CaptchaValidationFilter(captchaService), UsernamePasswordAuthenticationFilter.class)
- // 验证和解析过滤器
- .addFilterBefore(new TokenAuthenticationFilter(tokenManager), UsernamePasswordAuthenticationFilter.class)
+ // Token 认证(Starter 过滤器,抛 AuthenticationException 交给 ExceptionTranslationFilter 处理)
+ .addFilterBefore(new TokenAuthenticationFilter(tokenManager), AuthorizationFilter.class)
.build();
}
- /**
- * 配置Web安全自定义器,以忽略特定请求路径的安全性检查。
- *
- * 该配置用于指定哪些请求路径不经过Spring Security过滤器链。通常用于静态资源文件。
- */
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> {
@@ -105,38 +95,27 @@ public class SecurityConfig {
};
}
- /**
- * 默认密码认证的 Provider
- */
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
- DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(userDetailsService);
- daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
- return daoAuthenticationProvider;
+ DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
+ provider.setPasswordEncoder(passwordEncoder);
+ return provider;
}
- /**
- * 短信验证码认证 Provider
- */
@Bean
- public SmsAuthenticationProvider smsAuthenticationProvider() {
- return new SmsAuthenticationProvider(userService, redisTemplate);
+ public SmsAuthenticationProvider smsAuthenticationProvider(UserAuthenticationPort userAuthPort) {
+ return new SmsAuthenticationProvider(userAuthPort, redisTemplate);
}
- /**
- * 微信小程序认证 Provider
- */
@Bean
public WxMaAuthenticationProvider wechatMiniAuthenticationProvider(
WxMaService wxMaService,
- SysUserDetailsService sysUserDetailsService
+ UserAuthenticationPort userAuthenticationPort,
+ UserSocialService userSocialService
) {
- return new WxMaAuthenticationProvider(wxMaService, sysUserDetailsService);
+ return new WxMaAuthenticationProvider(wxMaService, userAuthenticationPort, userSocialService);
}
- /**
- * 认证管理器
- */
@Bean
public AuthenticationManager authenticationManager(
DaoAuthenticationProvider daoAuthenticationProvider,
@@ -149,5 +128,4 @@ public class SecurityConfig {
wxMaAuthenticationProvider
);
}
-
}
diff --git a/src/main/java/com/youlai/boot/framework/security/exception/NeedBindMobileException.java b/src/main/java/com/youlai/boot/auth/security/exception/MobileNotBoundException.java
similarity index 60%
rename from src/main/java/com/youlai/boot/framework/security/exception/NeedBindMobileException.java
rename to src/main/java/com/youlai/boot/auth/security/exception/MobileNotBoundException.java
index 706b6179..099b9ee3 100644
--- a/src/main/java/com/youlai/boot/framework/security/exception/NeedBindMobileException.java
+++ b/src/main/java/com/youlai/boot/auth/security/exception/MobileNotBoundException.java
@@ -1,17 +1,16 @@
-package com.youlai.boot.framework.security.exception;
+package com.youlai.boot.auth.security.exception;
import org.springframework.security.core.AuthenticationException;
/**
- * 需要绑定手机号异常
+ * 需要绑定手机号异常(微信小程序登录未绑定手机号时抛出)。
*/
-public class NeedBindMobileException extends AuthenticationException {
+public class MobileNotBoundException extends AuthenticationException {
private final String openid;
-
private final String sessionKey;
- public NeedBindMobileException(String openid, String sessionKey) {
+ public MobileNotBoundException(String openid, String sessionKey) {
super("需要绑定手机号");
this.openid = openid;
this.sessionKey = sessionKey;
@@ -24,5 +23,4 @@ public class NeedBindMobileException extends AuthenticationException {
public String getSessionKey() {
return sessionKey;
}
-
}
diff --git a/src/main/java/com/youlai/boot/framework/security/exception/SmsCaptchaException.java b/src/main/java/com/youlai/boot/auth/security/exception/SmsCaptchaException.java
similarity index 83%
rename from src/main/java/com/youlai/boot/framework/security/exception/SmsCaptchaException.java
rename to src/main/java/com/youlai/boot/auth/security/exception/SmsCaptchaException.java
index 8fca2cd9..d5bfd0ea 100644
--- a/src/main/java/com/youlai/boot/framework/security/exception/SmsCaptchaException.java
+++ b/src/main/java/com/youlai/boot/auth/security/exception/SmsCaptchaException.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.security.exception;
+package com.youlai.boot.auth.security.exception;
import org.springframework.security.core.AuthenticationException;
@@ -9,6 +9,7 @@ import org.springframework.security.core.AuthenticationException;
* @since 2025/3/1
*/
public class SmsCaptchaException extends AuthenticationException {
+
public SmsCaptchaException(String msg) {
super(msg);
}
diff --git a/src/main/java/com/youlai/boot/framework/security/filter/CaptchaValidationFilter.java b/src/main/java/com/youlai/boot/auth/security/filter/CaptchaValidationFilter.java
similarity index 93%
rename from src/main/java/com/youlai/boot/framework/security/filter/CaptchaValidationFilter.java
rename to src/main/java/com/youlai/boot/auth/security/filter/CaptchaValidationFilter.java
index 30504bc2..db2b4973 100644
--- a/src/main/java/com/youlai/boot/framework/security/filter/CaptchaValidationFilter.java
+++ b/src/main/java/com/youlai/boot/auth/security/filter/CaptchaValidationFilter.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.security.filter;
+package com.youlai.boot.auth.security.filter;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
@@ -29,7 +29,9 @@ import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
/**
- * 图形验证码校验过滤器
+ * 图形验证码校验过滤器。
+ *
+ * 归使用方,因为验证码规则(哪些接口需要验证码、验证码类型)因项目而异。
*/
public class CaptchaValidationFilter extends OncePerRequestFilter {
@@ -49,20 +51,17 @@ public class CaptchaValidationFilter extends OncePerRequestFilter {
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
- // 非登录接口直接放行
if (!LOGIN_PATH_REQUEST_MATCHER.matches(request)) {
chain.doFilter(request, response);
return;
}
- // 仅支持 JSON 登录
String contentType = request.getContentType();
if (contentType == null || !contentType.contains(MediaType.APPLICATION_JSON_VALUE)) {
ResponseWriter.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR);
return;
}
- // 包装请求,确保下游还能读取 body
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request, -1);
byte[] bodyBytes = StreamUtils.copyToByteArray(requestWrapper.getInputStream());
@@ -85,9 +84,6 @@ public class CaptchaValidationFilter extends OncePerRequestFilter {
}
}
- /**
- * Simple wrapper to allow repeated reads of the request body after we've parsed it here.
- */
private static class RepeatableReadRequestWrapper extends HttpServletRequestWrapper {
private final byte[] cachedBody;
@@ -139,5 +135,3 @@ public class CaptchaValidationFilter extends OncePerRequestFilter {
}
}
}
-
-
diff --git a/src/main/java/com/youlai/boot/framework/security/handler/MyAccessDeniedHandler.java b/src/main/java/com/youlai/boot/auth/security/handler/JsonAccessDeniedHandler.java
similarity index 64%
rename from src/main/java/com/youlai/boot/framework/security/handler/MyAccessDeniedHandler.java
rename to src/main/java/com/youlai/boot/auth/security/handler/JsonAccessDeniedHandler.java
index b4b28ab4..3124829f 100644
--- a/src/main/java/com/youlai/boot/framework/security/handler/MyAccessDeniedHandler.java
+++ b/src/main/java/com/youlai/boot/auth/security/handler/JsonAccessDeniedHandler.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.security.handler;
+package com.youlai.boot.auth.security.handler;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.framework.web.util.ResponseWriter;
@@ -9,17 +9,18 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
- * 无权限访问处理器
+ * 无权限访问处理器。
+ *
+ * 归使用方,因为 JSON 响应格式因项目而异。
*
* @author Ray.Hao
* @since 2.0.0
*/
-public class MyAccessDeniedHandler implements AccessDeniedHandler {
+public class JsonAccessDeniedHandler implements AccessDeniedHandler {
@Override
- public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
- // 权限不足返回 403 Forbidden
+ public void handle(HttpServletRequest request, HttpServletResponse response,
+ AccessDeniedException accessDeniedException) {
ResponseWriter.writeError(response, ResultCode.ACCESS_PERMISSION_EXCEPTION);
}
-
}
diff --git a/src/main/java/com/youlai/boot/framework/security/handler/MyAuthenticationEntryPoint.java b/src/main/java/com/youlai/boot/auth/security/handler/JsonAuthenticationEntryPoint.java
similarity index 54%
rename from src/main/java/com/youlai/boot/framework/security/handler/MyAuthenticationEntryPoint.java
rename to src/main/java/com/youlai/boot/auth/security/handler/JsonAuthenticationEntryPoint.java
index 4f4257c1..0a265125 100644
--- a/src/main/java/com/youlai/boot/framework/security/handler/MyAuthenticationEntryPoint.java
+++ b/src/main/java/com/youlai/boot/auth/security/handler/JsonAuthenticationEntryPoint.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.security.handler;
+package com.youlai.boot.auth.security.handler;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.framework.web.util.ResponseWriter;
@@ -14,35 +14,24 @@ import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
- * 统一处理 Spring Security 认证失败响应
+ * 统一处理 Spring Security 认证失败响应。
+ *
+ * 归使用方,因为 JSON 响应格式(Result 结构、错误码)因项目而异。
*
* @author Ray.Hao
* @since 2.0.0
*/
-public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
+public class JsonAuthenticationEntryPoint implements AuthenticationEntryPoint {
- /**
- * 认证失败处理入口方法
- *
- * @param request 触发异常的请求对象(可用于获取请求头、参数等)
- * @param response 响应对象(用于写入错误信息)
- * @param authException 认证异常对象(包含具体失败原因)
- */
@Override
- public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
+ public void commence(HttpServletRequest request, HttpServletResponse response,
+ AuthenticationException authException) throws IOException, ServletException {
if (authException instanceof BadCredentialsException) {
- // 用户名或密码错误
ResponseWriter.writeError(response, ResultCode.USER_PASSWORD_ERROR);
- } else if(authException instanceof InsufficientAuthenticationException){
- // 请求头缺失Authorization、Token格式错误、Token过期、签名验证失败
+ } else if (authException instanceof InsufficientAuthenticationException) {
ResponseWriter.writeError(response, ResultCode.ACCESS_TOKEN_INVALID);
} else {
- // 其他未明确处理的认证异常(如账户被锁定、账户禁用等)
ResponseWriter.writeError(response, ResultCode.USER_LOGIN_EXCEPTION, authException.getMessage());
}
}
}
-
-
-
-
diff --git a/src/main/java/com/youlai/boot/framework/security/model/SmsAuthenticationToken.java b/src/main/java/com/youlai/boot/auth/security/model/SmsAuthenticationToken.java
similarity index 54%
rename from src/main/java/com/youlai/boot/framework/security/model/SmsAuthenticationToken.java
rename to src/main/java/com/youlai/boot/auth/security/model/SmsAuthenticationToken.java
index bdcb60fe..064e277b 100644
--- a/src/main/java/com/youlai/boot/framework/security/model/SmsAuthenticationToken.java
+++ b/src/main/java/com/youlai/boot/auth/security/model/SmsAuthenticationToken.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.security.model;
+package com.youlai.boot.auth.security.model;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
@@ -8,13 +8,9 @@ import java.io.Serial;
import java.util.Collection;
/**
- * 短信验证码认证 Token
+ * 短信验证码认证 Token。
*
- * 用于短信验证码登录场景,遵循 Spring Security 认证模型:
- *
- * - 未认证状态:principal 为手机号,credentials 为验证码
- * - 已认证状态:principal 为用户详情,credentials 为 null
- *
+ * 未认证:principal=手机号,credentials=验证码;已认证:principal=SecurityUserDetails,credentials=null。
*
* @author Ray.Hao
* @since 2.20.0
@@ -24,30 +20,9 @@ public class SmsAuthenticationToken extends AbstractAuthenticationToken {
@Serial
private static final long serialVersionUID = 621L;
- /**
- * 认证信息
- *
- * - 未认证时:手机号
- * - 已认证时:SysUserDetails 用户详情
- *
- */
private final Object principal;
-
- /**
- * 凭证信息
- *
- * - 未认证时:短信验证码
- * - 已认证时:null
- *
- */
private final Object credentials;
- /**
- * 创建未认证的 Token
- *
- * @param mobile 手机号
- * @param verifyCode 短信验证码
- */
public SmsAuthenticationToken(String mobile, String verifyCode) {
super(AuthorityUtils.NO_AUTHORITIES);
this.principal = mobile;
@@ -55,12 +30,6 @@ public class SmsAuthenticationToken extends AbstractAuthenticationToken {
setAuthenticated(false);
}
- /**
- * 创建已认证的 Token
- *
- * @param principal 用户详情(SysUserDetails)
- * @param authorities 授权信息
- */
public SmsAuthenticationToken(Object principal, Collection extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
@@ -68,13 +37,6 @@ public class SmsAuthenticationToken extends AbstractAuthenticationToken {
super.setAuthenticated(true);
}
- /**
- * 创建已认证的 Token(静态工厂方法)
- *
- * @param principal 用户详情(SysUserDetails)
- * @param authorities 授权信息
- * @return 已认证的 SmsAuthenticationToken
- */
public static SmsAuthenticationToken authenticated(Object principal, Collection extends GrantedAuthority> authorities) {
return new SmsAuthenticationToken(principal, authorities);
}
diff --git a/src/main/java/com/youlai/boot/framework/security/model/WxMaAuthenticationToken.java b/src/main/java/com/youlai/boot/auth/security/model/WxMaAuthenticationToken.java
similarity index 68%
rename from src/main/java/com/youlai/boot/framework/security/model/WxMaAuthenticationToken.java
rename to src/main/java/com/youlai/boot/auth/security/model/WxMaAuthenticationToken.java
index 584929ef..3f012894 100644
--- a/src/main/java/com/youlai/boot/framework/security/model/WxMaAuthenticationToken.java
+++ b/src/main/java/com/youlai/boot/auth/security/model/WxMaAuthenticationToken.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.security.model;
+package com.youlai.boot.auth.security.model;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
@@ -8,7 +8,9 @@ import java.io.Serial;
import java.util.Collection;
/**
- * 微信小程序认证 Token
+ * 微信小程序认证 Token。
+ *
+ * 未认证:principal=微信code;已认证:principal=SecurityUserDetails。
*
* @author Ray.Hao
* @since 4.0.0
@@ -18,25 +20,9 @@ public class WxMaAuthenticationToken extends AbstractAuthenticationToken {
@Serial
private static final long serialVersionUID = 622L;
- /**
- * 认证信息
- * 未认证时:微信code
- * 已认证时:SysUserDetails 用户详情
- */
private final Object principal;
-
- /**
- * 凭证信息
- * 未认证时:null
- * 已认证时:null
- */
private final Object credentials;
- /**
- * 创建未认证的 Token
- *
- * @param code 微信小程序code
- */
public WxMaAuthenticationToken(String code) {
super(AuthorityUtils.NO_AUTHORITIES);
this.principal = code;
@@ -44,12 +30,6 @@ public class WxMaAuthenticationToken extends AbstractAuthenticationToken {
setAuthenticated(false);
}
- /**
- * 创建已认证的 Token
- *
- * @param principal 用户详情(SysUserDetails)
- * @param authorities 授权信息
- */
public WxMaAuthenticationToken(Object principal, Collection extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
@@ -57,9 +37,6 @@ public class WxMaAuthenticationToken extends AbstractAuthenticationToken {
super.setAuthenticated(true);
}
- /**
- * 创建已认证的 Token(静态工厂方法)
- */
public static WxMaAuthenticationToken authenticated(Object principal, Collection extends GrantedAuthority> authorities) {
return new WxMaAuthenticationToken(principal, authorities);
}
diff --git a/src/main/java/com/youlai/boot/framework/security/provider/SmsAuthenticationProvider.java b/src/main/java/com/youlai/boot/auth/security/provider/SmsAuthenticationProvider.java
similarity index 61%
rename from src/main/java/com/youlai/boot/framework/security/provider/SmsAuthenticationProvider.java
rename to src/main/java/com/youlai/boot/auth/security/provider/SmsAuthenticationProvider.java
index 3450bd7a..68626416 100644
--- a/src/main/java/com/youlai/boot/framework/security/provider/SmsAuthenticationProvider.java
+++ b/src/main/java/com/youlai/boot/auth/security/provider/SmsAuthenticationProvider.java
@@ -1,13 +1,13 @@
-package com.youlai.boot.framework.security.provider;
+package com.youlai.boot.auth.security.provider;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.RedisConstants;
-import com.youlai.boot.framework.security.exception.SmsCaptchaException;
-import com.youlai.boot.framework.security.model.SmsAuthenticationToken;
-import com.youlai.boot.framework.security.model.SysUserDetails;
-import com.youlai.boot.framework.security.model.UserAuthInfo;
-import com.youlai.boot.system.service.UserService;
+import com.youlai.boot.framework.security.model.SecurityUser;
+import com.youlai.boot.framework.security.model.SecurityUserDetails;
+import com.youlai.boot.framework.security.port.UserAuthenticationPort;
+import com.youlai.boot.auth.security.exception.SmsCaptchaException;
+import com.youlai.boot.auth.security.model.SmsAuthenticationToken;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationProvider;
@@ -19,47 +19,34 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
/**
* 短信验证码认证 Provider
*
- * 实现 Spring Security 的 {@link AuthenticationProvider} 接口,处理短信验证码登录认证。
- *
* 认证流程:
*
* - 根据手机号查询用户信息
- * - 校验用户状态(是否禁用)
+ * - 校验用户状态
* - 校验短信验证码(与 Redis 缓存比对)
- * - 验证成功后删除验证码,防止重复使用
+ * - 验证成功后删除验证码
* - 返回已认证的 Authentication
*
*
* @author Ray.Hao
* @since 2.17.0
- * @see SmsAuthenticationToken
- * @see AuthenticationProvider
*/
@Slf4j
public class SmsAuthenticationProvider implements AuthenticationProvider {
- private final UserService userService;
-
+ private final UserAuthenticationPort userAuthPort;
private final RedisTemplate redisTemplate;
- public SmsAuthenticationProvider(UserService userService, RedisTemplate redisTemplate) {
- this.userService = userService;
+ public SmsAuthenticationProvider(UserAuthenticationPort userAuthPort, RedisTemplate redisTemplate) {
+ this.userAuthPort = userAuthPort;
this.redisTemplate = redisTemplate;
}
- /**
- * 执行短信验证码认证
- *
- * @param authentication 未认证的 {@link SmsAuthenticationToken}
- * @return 已认证的 {@link SmsAuthenticationToken}
- * @throws AuthenticationException 认证失败异常
- */
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String mobile = (String) authentication.getPrincipal();
String inputVerifyCode = (String) authentication.getCredentials();
- // 参数校验
if (StrUtil.isBlank(mobile)) {
log.warn("短信验证码登录失败:手机号为空");
throw new SmsCaptchaException("手机号不能为空");
@@ -69,21 +56,18 @@ public class SmsAuthenticationProvider implements AuthenticationProvider {
throw new SmsCaptchaException("验证码不能为空");
}
- // 根据手机号获取用户信息
- UserAuthInfo userAuthInfo = userService.getAuthInfoByMobile(mobile);
+ SecurityUser securityUser = userAuthPort.getAuthInfoByMobile(mobile);
- if (userAuthInfo == null) {
+ if (securityUser == null) {
log.warn("短信验证码登录失败:用户不存在,手机号={}", mobile);
throw new UsernameNotFoundException("用户不存在");
}
- // 检查用户状态是否有效
- if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) {
- log.warn("短信验证码登录失败:用户已禁用,用户名={}", userAuthInfo.getUsername());
+ if (ObjectUtil.notEqual(securityUser.getStatus(), 1)) {
+ log.warn("短信验证码登录失败:用户已禁用,用户名={}", securityUser.getUsername());
throw new DisabledException("用户已被禁用");
}
- // 校验短信验证码
String cacheKey = StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile);
String cachedVerifyCode = (String) redisTemplate.opsForValue().get(cacheKey);
@@ -97,24 +81,13 @@ public class SmsAuthenticationProvider implements AuthenticationProvider {
throw new SmsCaptchaException("验证码错误");
}
- // 验证成功后删除验证码,防止重复使用
redisTemplate.delete(cacheKey);
- // 构建认证后的用户详情信息
- SysUserDetails userDetails = new SysUserDetails(userAuthInfo);
-
- log.info("短信验证码登录成功:用户名={},手机号={}", userAuthInfo.getUsername(), mobile);
-
- // 创建已认证的 SmsAuthenticationToken
+ SecurityUserDetails userDetails = new SecurityUserDetails(securityUser);
+ log.info("短信验证码登录成功:用户名={},手机号={}", securityUser.getUsername(), mobile);
return SmsAuthenticationToken.authenticated(userDetails, userDetails.getAuthorities());
}
- /**
- * 支持的认证类型
- *
- * @param authentication 认证类型
- * @return 是否支持该认证类型
- */
@Override
public boolean supports(Class> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
diff --git a/src/main/java/com/youlai/boot/framework/security/provider/WxMaAuthenticationProvider.java b/src/main/java/com/youlai/boot/auth/security/provider/WxMaAuthenticationProvider.java
similarity index 64%
rename from src/main/java/com/youlai/boot/framework/security/provider/WxMaAuthenticationProvider.java
rename to src/main/java/com/youlai/boot/auth/security/provider/WxMaAuthenticationProvider.java
index d30bfd60..3b40f2f5 100644
--- a/src/main/java/com/youlai/boot/framework/security/provider/WxMaAuthenticationProvider.java
+++ b/src/main/java/com/youlai/boot/auth/security/provider/WxMaAuthenticationProvider.java
@@ -1,14 +1,16 @@
-package com.youlai.boot.framework.security.provider;
+package com.youlai.boot.auth.security.provider;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
import cn.hutool.core.util.ObjectUtil;
-import com.youlai.boot.framework.security.exception.NeedBindMobileException;
-import com.youlai.boot.framework.security.model.SysUserDetails;
-import com.youlai.boot.framework.security.model.UserAuthInfo;
-import com.youlai.boot.framework.security.model.WxMaAuthenticationToken;
-import com.youlai.boot.framework.security.service.SysUserDetailsService;
+import com.youlai.boot.common.enums.SocialPlatformEnum;
+import com.youlai.boot.framework.security.model.SecurityUser;
+import com.youlai.boot.framework.security.model.SecurityUserDetails;
+import com.youlai.boot.framework.security.port.UserAuthenticationPort;
+import com.youlai.boot.auth.security.exception.MobileNotBoundException;
+import com.youlai.boot.auth.security.model.WxMaAuthenticationToken;
import com.youlai.boot.system.model.entity.UserSocial;
+import com.youlai.boot.system.service.UserSocialService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
@@ -26,7 +28,8 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
public class WxMaAuthenticationProvider implements AuthenticationProvider {
private final WxMaService wxMaService;
- private final SysUserDetailsService sysUserDetailsService;
+ private final UserAuthenticationPort userAuthPort;
+ private final UserSocialService userSocialService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
@@ -38,44 +41,35 @@ public class WxMaAuthenticationProvider implements AuthenticationProvider {
}
try {
- // 1. 用 code 换取 openid
WxMaJscode2SessionResult session = wxMaService.jsCode2SessionInfo(code);
String openid = session.getOpenid();
String sessionKey = session.getSessionKey();
log.info("微信小程序登录:openid={}", openid);
- // 2. 根据 openid 查询绑定信息
- UserSocial userSocial = sysUserDetailsService.getWechatMiniBindInfo(openid);
+ UserSocial userSocial = userSocialService.getByPlatformAndOpenid(SocialPlatformEnum.WECHAT_MINI, openid);
if (userSocial == null) {
- // 未绑定,抛出异常提示需要绑定手机号
log.info("微信小程序登录:用户未绑定手机号,openid={}", openid);
- throw new NeedBindMobileException(openid, sessionKey);
+ throw new MobileNotBoundException(openid, sessionKey);
}
- // 3. 获取用户认证信息
- UserAuthInfo userAuthInfo = sysUserDetailsService.getAuthInfoByWechatOpenid(openid);
+ SecurityUser securityUser = userAuthPort.getAuthInfoByOpenid(SocialPlatformEnum.WECHAT_MINI, openid);
- if (userAuthInfo == null) {
+ if (securityUser == null) {
log.warn("微信小程序登录失败:用户不存在,openid={}", openid);
throw new UsernameNotFoundException("用户不存在");
}
- // 4. 检查用户状态
- if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) {
- log.warn("微信小程序登录失败:用户已禁用,username={}", userAuthInfo.getUsername());
+ if (ObjectUtil.notEqual(securityUser.getStatus(), 1)) {
+ log.warn("微信小程序登录失败:用户已禁用,username={}", securityUser.getUsername());
throw new DisabledException("用户已被禁用");
}
- // 5. 更新 session_key
- sysUserDetailsService.updateWechatSessionKey(userSocial.getId(), sessionKey);
-
- // 6. 构建已认证 Token
- SysUserDetails userDetails = new SysUserDetails(userAuthInfo);
-
- log.info("微信小程序登录成功:username={}, openid={}", userAuthInfo.getUsername(), openid);
+ userSocialService.updateSessionKey(userSocial.getId(), sessionKey);
+ SecurityUserDetails userDetails = new SecurityUserDetails(securityUser);
+ log.info("微信小程序登录成功:username={}, openid={}", securityUser.getUsername(), openid);
return WxMaAuthenticationToken.authenticated(userDetails, userDetails.getAuthorities());
} catch (WxErrorException e) {
@@ -88,5 +82,4 @@ public class WxMaAuthenticationProvider implements AuthenticationProvider {
public boolean supports(Class> authentication) {
return WxMaAuthenticationToken.class.isAssignableFrom(authentication);
}
-
}
diff --git a/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java b/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java
index 086caa17..a2bc3c44 100644
--- a/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java
+++ b/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java
@@ -6,8 +6,8 @@ import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.framework.captcha.model.CaptchaInfo;
import com.youlai.boot.framework.captcha.service.CaptchaService;
import com.youlai.boot.framework.security.model.AuthenticationToken;
-import com.youlai.boot.framework.security.model.SmsAuthenticationToken;
import com.youlai.boot.framework.security.token.TokenManager;
+import com.youlai.boot.auth.security.model.SmsAuthenticationToken;
import com.youlai.boot.framework.security.util.SecurityUtils;
import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum;
import com.youlai.boot.framework.integration.sms.service.SmsService;
@@ -58,9 +58,9 @@ public class AuthServiceImpl implements AuthService {
// 2. 执行认证(认证中)
// 说明:这里的认证流程由 Spring Security 提供的 AuthenticationManager 执行。
// 默认情况下会委托给 DaoAuthenticationProvider:
- // 1) retrieveUser(...):内部通过 UserDetailsService.loadUserByUsername(...) 获取用户信息(本项目为 SysUserDetailsService 实现)
+ // 1) retrieveUser(...):内部通过 UserDetailsService.loadUserByUsername(...) 获取用户信息(本项目为 SecurityUserDetailsService 实现)
// 2) additionalAuthenticationChecks(...):对比请求密码与用户存储密码(由 PasswordEncoder 完成匹配)
- // 认证通过后返回已认证的 Authentication(principal 为 SysUserDetails,authorities 为角色/权限集合)。
+ // 认证通过后返回已认证的 Authentication(principal 为 SecurityUserDetails,authorities 为角色/权限集合)。
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
diff --git a/src/main/java/com/youlai/boot/auth/service/impl/WxMaAuthServiceImpl.java b/src/main/java/com/youlai/boot/auth/service/impl/WxMaAuthServiceImpl.java
index 9a6e0aa6..439cb329 100644
--- a/src/main/java/com/youlai/boot/auth/service/impl/WxMaAuthServiceImpl.java
+++ b/src/main/java/com/youlai/boot/auth/service/impl/WxMaAuthServiceImpl.java
@@ -8,12 +8,12 @@ import cn.hutool.core.util.StrUtil;
import com.youlai.boot.auth.model.vo.WxMaLoginVO;
import com.youlai.boot.auth.service.WxMaAuthService;
import com.youlai.boot.common.constant.RedisConstants;
-import com.youlai.boot.framework.security.exception.NeedBindMobileException;
import com.youlai.boot.framework.security.model.AuthenticationToken;
-import com.youlai.boot.framework.security.model.SysUserDetails;
-import com.youlai.boot.framework.security.model.WxMaAuthenticationToken;
+import com.youlai.boot.framework.security.model.SecurityUserDetails;
import com.youlai.boot.framework.security.token.TokenManager;
-import com.youlai.boot.system.enums.SocialPlatformEnum;
+import com.youlai.boot.auth.security.exception.MobileNotBoundException;
+import com.youlai.boot.auth.security.model.WxMaAuthenticationToken;
+import com.youlai.boot.common.enums.SocialPlatformEnum;
import com.youlai.boot.system.model.entity.SysUser;
import com.youlai.boot.system.service.UserSocialService;
import com.youlai.boot.system.service.UserService;
@@ -69,7 +69,7 @@ public class WxMaAuthServiceImpl implements WxMaAuthService {
.tokenType(authToken.getTokenType())
.expiresIn(authToken.getExpiresIn())
.build();
- } catch (NeedBindMobileException e) {
+ } catch (MobileNotBoundException e) {
return WxMaLoginVO.builder()
.isNewUser(true)
.needBindMobile(true)
@@ -234,7 +234,7 @@ public class WxMaAuthServiceImpl implements WxMaAuthService {
* 生成认证令牌
*/
private AuthenticationToken generateAuthToken(String mobile) {
- SysUserDetails userDetails = new SysUserDetails(userService.getAuthInfoByMobile(mobile));
+ SecurityUserDetails userDetails = new SecurityUserDetails(userService.getAuthInfoByMobile(mobile));
Authentication authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
diff --git a/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java b/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java
index 9aaadcda..61be1d98 100644
--- a/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java
+++ b/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java
@@ -33,9 +33,9 @@ public interface JwtClaimConstants {
String DATA_SCOPES = "dataScopes";
/**
- * 权限(角色Code)集合
+ * 角色编码集合(不带 ROLE_ 前缀)
*/
- String AUTHORITIES = "authorities";
+ String ROLES = "roles";
/**
* Token 版本号
diff --git a/src/main/java/com/youlai/boot/system/enums/SocialPlatformEnum.java b/src/main/java/com/youlai/boot/common/enums/SocialPlatformEnum.java
similarity index 94%
rename from src/main/java/com/youlai/boot/system/enums/SocialPlatformEnum.java
rename to src/main/java/com/youlai/boot/common/enums/SocialPlatformEnum.java
index 0779f594..c6e85c13 100644
--- a/src/main/java/com/youlai/boot/system/enums/SocialPlatformEnum.java
+++ b/src/main/java/com/youlai/boot/common/enums/SocialPlatformEnum.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.system.enums;
+package com.youlai.boot.common.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.youlai.boot.common.base.IBaseEnum;
diff --git a/src/main/java/com/youlai/boot/framework/mybatis/interceptor/MyDataPermissionHandler.java b/src/main/java/com/youlai/boot/framework/mybatis/interceptor/MyDataPermissionHandler.java
index 7dd4e003..bc2a48b9 100644
--- a/src/main/java/com/youlai/boot/framework/mybatis/interceptor/MyDataPermissionHandler.java
+++ b/src/main/java/com/youlai/boot/framework/mybatis/interceptor/MyDataPermissionHandler.java
@@ -7,7 +7,7 @@ import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
import com.youlai.boot.common.annotation.DataPermission;
import com.youlai.boot.common.enums.DataScopeEnum;
import com.youlai.boot.framework.security.model.RoleDataScope;
-import com.youlai.boot.framework.security.model.SysUserDetails;
+import com.youlai.boot.framework.security.model.SecurityUserDetails;
import com.youlai.boot.framework.security.util.SecurityUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@@ -58,7 +58,7 @@ public class MyDataPermissionHandler implements DataPermissionHandler {
// 获取当前用户的数据权限列表
List dataScopes = SecurityUtils.getUser()
- .map(SysUserDetails::getDataScopes)
+ .map(SecurityUserDetails::getDataScopes)
.orElse(List.of());
// 如果任一角色是 ALL,则跳过数据权限过滤(并集策略)
diff --git a/src/main/java/com/youlai/boot/framework/security/filter/TokenAuthenticationFilter.java b/src/main/java/com/youlai/boot/framework/security/filter/TokenAuthenticationFilter.java
index 4aad3db0..c4298f35 100644
--- a/src/main/java/com/youlai/boot/framework/security/filter/TokenAuthenticationFilter.java
+++ b/src/main/java/com/youlai/boot/framework/security/filter/TokenAuthenticationFilter.java
@@ -2,78 +2,72 @@ package com.youlai.boot.framework.security.filter;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.SecurityConstants;
-import com.youlai.boot.common.result.ResultCode;
-import com.youlai.boot.framework.web.util.ResponseWriter;
import com.youlai.boot.framework.security.token.TokenManager;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
+import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
- * Token 认证校验过滤器
+ * Token 认证过滤器。
+ *
+ * 仅负责解析 Token 和填充 {@link SecurityContextHolder}。
+ * 无效 Token 抛出 {@link AuthenticationException},由使用方的
+ * {@code AuthenticationEntryPoint} 统一处理响应格式。
+ *
+ * 必须注册在 {@code ExceptionTranslationFilter} 之后(即 {@code AuthorizationFilter} 之前),
+ * 这样抛出的异常才能被 {@code ExceptionTranslationFilter} 捕获。
*
* @author wangtao
- * @since 2025/3/6 16:50
+ * @since 2025/3/6
*/
public class TokenAuthenticationFilter extends OncePerRequestFilter {
- /**
- * Token 管理器
- */
private final TokenManager tokenManager;
public TokenAuthenticationFilter(TokenManager tokenManager) {
this.tokenManager = tokenManager;
}
- /**
- * 校验 Token ,包括验签和是否过期
- * 如果 Token 有效,将 Token 解析为 Authentication 对象,并设置到 Spring Security 上下文中
- */
@Override
- protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
String rawToken = resolveToken(request);
- try {
- if (StrUtil.isNotBlank(rawToken)) {
- // 执行令牌有效性检查(包含密码学验签和过期时间验证)
- boolean isValidToken = tokenManager.validateToken(rawToken);
- if (!isValidToken) {
- ResponseWriter.writeError(response, ResultCode.ACCESS_TOKEN_INVALID);
- return;
+ if (StrUtil.isNotBlank(rawToken)) {
+ try {
+ boolean isValid = tokenManager.validateToken(rawToken);
+ if (!isValid) {
+ SecurityContextHolder.clearContext();
+ throw new InsufficientAuthenticationException("Token 无效或已过期");
}
-
- // 将令牌解析为 Spring Security 上下文认证对象
Authentication authentication = tokenManager.parseToken(rawToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
+ } catch (AuthenticationException ex) {
+ SecurityContextHolder.clearContext();
+ throw ex;
+ } catch (Exception ex) {
+ SecurityContextHolder.clearContext();
+ throw new InsufficientAuthenticationException("Token 认证失败", ex);
}
- } catch (Exception ex) {
- // 安全上下文清除保障(防止上下文残留)
- SecurityContextHolder.clearContext();
- ResponseWriter.writeError(response, ResultCode.ACCESS_TOKEN_INVALID);
- return;
}
- // 继续后续过滤器链执行
filterChain.doFilter(request, response);
}
- /**
- * 从请求中解析 Token(仅支持 Authorization Header)
- */
private String resolveToken(HttpServletRequest request) {
- String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
- if (StrUtil.isNotBlank(authorizationHeader)
- && authorizationHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
- return authorizationHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
+ String header = request.getHeader(HttpHeaders.AUTHORIZATION);
+ if (StrUtil.isNotBlank(header) && header.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
+ return header.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
}
return null;
}
diff --git a/src/main/java/com/youlai/boot/framework/security/model/UserAuthInfo.java b/src/main/java/com/youlai/boot/framework/security/model/SecurityUser.java
similarity index 77%
rename from src/main/java/com/youlai/boot/framework/security/model/UserAuthInfo.java
rename to src/main/java/com/youlai/boot/framework/security/model/SecurityUser.java
index e6b17d21..d9673e7f 100644
--- a/src/main/java/com/youlai/boot/framework/security/model/UserAuthInfo.java
+++ b/src/main/java/com/youlai/boot/framework/security/model/SecurityUser.java
@@ -6,16 +6,16 @@ import java.util.List;
import java.util.Set;
/**
- * 用户认证信息
+ * 安全模块用户数据 POJO。
*
- * 用于登录认证过程中的用户信息承载,包含用户名、密码、状态、角色等与认证/授权相关的数据。
- *
+ * 作为端口接口的返回类型,承载用户认证所需的全部数据。
+ * 纯 JDK 类型,无 system 模块依赖,可直接序列化。
*
* @author Ray.Hao
* @since 2025/12/16
*/
@Data
-public class UserAuthInfo {
+public class SecurityUser {
/**
* 用户ID
@@ -48,7 +48,7 @@ public class UserAuthInfo {
private Integer status;
/**
- * 角色集合
+ * 角色编码集合
*/
private Set roles;
diff --git a/src/main/java/com/youlai/boot/framework/security/model/SysUserDetails.java b/src/main/java/com/youlai/boot/framework/security/model/SecurityUserDetails.java
similarity index 65%
rename from src/main/java/com/youlai/boot/framework/security/model/SysUserDetails.java
rename to src/main/java/com/youlai/boot/framework/security/model/SecurityUserDetails.java
index 2e733b11..eeecd5f3 100644
--- a/src/main/java/com/youlai/boot/framework/security/model/SysUserDetails.java
+++ b/src/main/java/com/youlai/boot/framework/security/model/SecurityUserDetails.java
@@ -13,17 +13,18 @@ import java.util.*;
import java.util.stream.Collectors;
/**
- * Spring Security 用户认证对象
+ * Spring Security 用户认证对象。
*
- * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。
- * 实现了 {@link UserDetails} 接口,提供用户的核心信息。
+ * 实现 {@link UserDetails},封装用户标识、角色、数据权限等认证信息。
+ * {@link #roles} 字段存储角色编码(不带 ROLE_ 前缀),
+ * {@link #getAuthorities()} 运行时补前缀并转为 {@link SimpleGrantedAuthority}。
*
* @author Ray.Hao
* @version 3.0.0
*/
@Data
@NoArgsConstructor
-public class SysUserDetails implements UserDetails {
+public class SecurityUserDetails implements UserDetails {
/**
* 用户ID
@@ -52,42 +53,35 @@ public class SysUserDetails implements UserDetails {
/**
* 数据权限列表
- *
- * 存储用户所有角色的数据权限范围,用于实现多角色权限合并(并集策略)
*/
private List dataScopes;
/**
- * 用户角色权限集合
+ * 角色编码集合(不带 ROLE_ 前缀)
*/
- private Collection authorities;
+ private Set roles;
/**
- * 构造函数:根据用户认证信息初始化用户详情对象
- *
- * @param user 用户认证信息对象 {@link UserAuthInfo}
+ * 构造函数:根据 {@link SecurityUser} 初始化。
*/
- public SysUserDetails(UserAuthInfo user) {
+ public SecurityUserDetails(SecurityUser user) {
this.userId = user.getUserId();
this.username = user.getUsername();
this.password = user.getPassword();
this.enabled = ObjectUtil.equal(user.getStatus(), 1);
this.deptId = user.getDeptId();
this.dataScopes = user.getDataScopes();
-
- // 初始化角色权限集合
- this.authorities = CollectionUtil.isNotEmpty(user.getRoles())
- ? user.getRoles().stream()
- // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add)
- .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role))
- .collect(Collectors.toSet())
- : Collections.emptySet();
+ this.roles = user.getRoles();
}
-
@Override
public Collection extends GrantedAuthority> getAuthorities() {
- return this.authorities;
+ if (CollectionUtil.isEmpty(roles)) {
+ return Collections.emptySet();
+ }
+ return roles.stream()
+ .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role))
+ .collect(Collectors.toSet());
}
@Override
@@ -107,8 +101,6 @@ public class SysUserDetails implements UserDetails {
/**
* 判断是否包含"全部数据"权限
- *
- * @return 是否有全部数据权限
*/
public boolean hasAllDataScope() {
if (CollectionUtil.isEmpty(dataScopes)) {
@@ -119,9 +111,7 @@ public class SysUserDetails implements UserDetails {
}
/**
- * 获取数据权限列表
- *
- * @return 数据权限列表,永不为null
+ * 获取数据权限列表,永不为 null
*/
public List getDataScopes() {
return dataScopes != null ? dataScopes : Collections.emptyList();
diff --git a/src/main/java/com/youlai/boot/framework/security/model/UserSession.java b/src/main/java/com/youlai/boot/framework/security/model/UserSession.java
deleted file mode 100644
index a798a4c5..00000000
--- a/src/main/java/com/youlai/boot/framework/security/model/UserSession.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package com.youlai.boot.framework.security.model;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.util.List;
-import java.util.Set;
-
-/**
- * 用户会话信息
- *
- * 存储在Token中的用户会话快照,包含用户身份、数据权限和角色权限信息。
- * 用于Redis-Token模式下的会话管理,支持在线用户查询和会话控制。
- *
- * @author wangtao
- * @since 2025/2/27 10:31
- */
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-public class UserSession {
-
- /**
- * 用户ID
- */
- private Long userId;
-
- /**
- * 用户名
- */
- private String username;
-
- /**
- * 部门ID
- */
- private Long deptId;
-
- /**
- * 数据权限列表
- */
- private List dataScopes;
-
- /**
- * 角色权限集合
- */
- private Set roles;
-
-}
diff --git a/src/main/java/com/youlai/boot/framework/security/model/WxMaBindInfo.java b/src/main/java/com/youlai/boot/framework/security/model/WxMaBindInfo.java
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/main/java/com/youlai/boot/framework/security/port/PermissionPort.java b/src/main/java/com/youlai/boot/framework/security/port/PermissionPort.java
new file mode 100644
index 00000000..9417eded
--- /dev/null
+++ b/src/main/java/com/youlai/boot/framework/security/port/PermissionPort.java
@@ -0,0 +1,22 @@
+package com.youlai.boot.framework.security.port;
+
+import java.util.Set;
+
+/**
+ * 权限查询端口。
+ *
+ * 由 system 模块提供适配器实现,framework 层通过此接口获取角色权限集合,
+ * 不直接依赖 system 模块的 {@code RoleMenuService}。
+ *
+ * @see com.youlai.boot.system.security.adapter.PermissionAdapter
+ */
+public interface PermissionPort {
+
+ /**
+ * 根据角色编码集合查询权限标识集合。
+ *
+ * @param roleCodes 角色编码集合
+ * @return 权限标识集合,如 "sys:user:create"
+ */
+ Set getRolePerms(Set roleCodes);
+}
diff --git a/src/main/java/com/youlai/boot/framework/security/port/UserAuthenticationPort.java b/src/main/java/com/youlai/boot/framework/security/port/UserAuthenticationPort.java
new file mode 100644
index 00000000..82acea2c
--- /dev/null
+++ b/src/main/java/com/youlai/boot/framework/security/port/UserAuthenticationPort.java
@@ -0,0 +1,40 @@
+package com.youlai.boot.framework.security.port;
+
+import com.youlai.boot.common.enums.SocialPlatformEnum;
+import com.youlai.boot.framework.security.model.SecurityUser;
+
+/**
+ * 用户认证信息查询端口。
+ *
+ * 由 system 模块提供适配器实现,framework 层通过此接口获取认证数据,
+ * 不直接依赖 system 模块的 {@code UserService} / {@code UserSocialService}。
+ *
+ * @see com.youlai.boot.system.security.adapter.UserAuthenticationAdapter
+ */
+public interface UserAuthenticationPort {
+
+ /**
+ * 根据用户名查询认证信息。
+ *
+ * @param username 用户名
+ * @return 认证信息,不存在返回 null
+ */
+ SecurityUser getAuthInfoByUsername(String username);
+
+ /**
+ * 根据手机号查询认证信息。
+ *
+ * @param mobile 手机号
+ * @return 认证信息,不存在返回 null
+ */
+ SecurityUser getAuthInfoByMobile(String mobile);
+
+ /**
+ * 根据第三方平台 openid 查询认证信息。
+ *
+ * @param platform 第三方平台
+ * @param openid openid
+ * @return 认证信息,未绑定返回 null
+ */
+ SecurityUser getAuthInfoByOpenid(SocialPlatformEnum platform, String openid);
+}
diff --git a/src/main/java/com/youlai/boot/framework/security/service/PermissionService.java b/src/main/java/com/youlai/boot/framework/security/service/PermissionService.java
index 46516f3c..ef7cc446 100644
--- a/src/main/java/com/youlai/boot/framework/security/service/PermissionService.java
+++ b/src/main/java/com/youlai/boot/framework/security/service/PermissionService.java
@@ -2,8 +2,8 @@ package com.youlai.boot.framework.security.service;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
+import com.youlai.boot.framework.security.port.PermissionPort;
import com.youlai.boot.framework.security.util.SecurityUtils;
-import com.youlai.boot.system.service.RoleMenuService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@@ -12,11 +12,10 @@ import org.springframework.util.PatternMatchUtils;
import java.util.Set;
/**
- * Spring Security 权限校验组件
+ * Spring Security 权限校验组件。
*
- * 用于 SpEL 表达式权限校验,如:@PreAuthorize("@ss.hasPerm('sys:user:create')")
- *
- * 权限数据来源:{@link RoleMenuService#getRolePermsByRoleCodes}(带 Redis 缓存)
+ * 用于 SpEL 表达式:{@code @PreAuthorize("@ss.hasPerm('sys:user:create')")}。
+ * 通过 {@link PermissionPort} 查询角色权限,不直接依赖 system 模块。
*
* @author Ray.Hao
* @since 0.0.1
@@ -26,39 +25,30 @@ import java.util.Set;
@Slf4j
public class PermissionService {
- private final RoleMenuService roleMenuService;
+ private final PermissionPort permissionPort;
/**
- * 判断当前登录用户是否拥有操作权限
- *
- * 支持通配符匹配,如:权限码 "sys:user:*" 可匹配 "sys:user:create"、"sys:user:delete" 等
- *
- * @param requiredPerm 所需权限
- * @return 是否有权限
+ * 判断当前用户是否拥有操作权限,支持通配符匹配。
*/
public boolean hasPerm(String requiredPerm) {
if (StrUtil.isBlank(requiredPerm)) {
return false;
}
- // 超级管理员放行
if (SecurityUtils.isRoot()) {
return true;
}
- // 获取当前登录用户的角色编码集合
Set roleCodes = SecurityUtils.getRoles();
if (CollectionUtil.isEmpty(roleCodes)) {
return false;
}
- // 获取当前登录用户的所有角色的权限列表(从缓存读取)
- Set rolePerms = roleMenuService.getRolePermsByRoleCodes(roleCodes);
+ Set rolePerms = permissionPort.getRolePerms(roleCodes);
if (CollectionUtil.isEmpty(rolePerms)) {
return false;
}
- // 判断权限列表中是否包含所需权限(支持通配符)
boolean hasPermission = rolePerms.stream()
.anyMatch(rolePerm -> PatternMatchUtils.simpleMatch(rolePerm, requiredPerm));
diff --git a/src/main/java/com/youlai/boot/framework/security/service/RolePermissionService.java b/src/main/java/com/youlai/boot/framework/security/service/RolePermissionService.java
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/main/java/com/youlai/boot/framework/security/service/SecurityUserDetailsService.java b/src/main/java/com/youlai/boot/framework/security/service/SecurityUserDetailsService.java
new file mode 100644
index 00000000..9b998c22
--- /dev/null
+++ b/src/main/java/com/youlai/boot/framework/security/service/SecurityUserDetailsService.java
@@ -0,0 +1,41 @@
+package com.youlai.boot.framework.security.service;
+
+import com.youlai.boot.framework.security.model.SecurityUser;
+import com.youlai.boot.framework.security.model.SecurityUserDetails;
+import com.youlai.boot.framework.security.port.UserAuthenticationPort;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+/**
+ * 系统用户认证 DetailsService。
+ *
+ * 通过 {@link UserAuthenticationPort} 获取认证信息,不直接依赖 system 模块。
+ *
+ * @author Ray.Hao
+ * @since 2021/10/19
+ */
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class SecurityUserDetailsService implements UserDetailsService {
+
+ private final UserAuthenticationPort userAuthPort;
+
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ try {
+ SecurityUser securityUser = userAuthPort.getAuthInfoByUsername(username);
+ if (securityUser == null) {
+ throw new UsernameNotFoundException(username);
+ }
+ return new SecurityUserDetails(securityUser);
+ } catch (Exception e) {
+ log.error("认证异常:{}", e.getMessage());
+ throw e;
+ }
+ }
+}
diff --git a/src/main/java/com/youlai/boot/framework/security/service/SysUserDetailsService.java b/src/main/java/com/youlai/boot/framework/security/service/SysUserDetailsService.java
deleted file mode 100644
index 2bbe035b..00000000
--- a/src/main/java/com/youlai/boot/framework/security/service/SysUserDetailsService.java
+++ /dev/null
@@ -1,82 +0,0 @@
-package com.youlai.boot.framework.security.service;
-
-import com.youlai.boot.framework.security.model.SysUserDetails;
-import com.youlai.boot.framework.security.model.UserAuthInfo;
-import com.youlai.boot.system.enums.SocialPlatformEnum;
-import com.youlai.boot.system.model.entity.UserSocial;
-import com.youlai.boot.system.service.UserSocialService;
-import com.youlai.boot.system.service.UserService;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.security.core.userdetails.UserDetails;
-import org.springframework.security.core.userdetails.UserDetailsService;
-import org.springframework.security.core.userdetails.UsernameNotFoundException;
-import org.springframework.stereotype.Service;
-
-/**
- * 系统用户认证 DetailsService
- *
- * @author Ray.Hao
- * @since 2021/10/19
- */
-@Service
-@RequiredArgsConstructor
-@Slf4j
-public class SysUserDetailsService implements UserDetailsService {
-
- private final UserService userService;
- private final UserSocialService userSocialService;
-
- /**
- * 根据用户名获取用户信息
- *
- * @param username 用户名
- * @return 用户信息
- * @throws UsernameNotFoundException 用户名未找到异常
- */
- @Override
- public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
- try {
- UserAuthInfo userAuthInfo = userService.getAuthInfoByUsername(username);
- if (userAuthInfo == null) {
- throw new UsernameNotFoundException(username);
- }
- return new SysUserDetails(userAuthInfo);
- } catch (Exception e) {
- // 记录异常日志
- log.error("认证异常:{}", e.getMessage());
- // 抛出异常
- throw e;
- }
- }
-
- /**
- * 根据微信小程序openid查询绑定信息
- *
- * @param openid 微信小程序openid
- * @return 绑定信息,未绑定返回null
- */
- public UserSocial getWechatMiniBindInfo(String openid) {
- return userSocialService.getByPlatformAndOpenid(SocialPlatformEnum.WECHAT_MINI, openid);
- }
-
- /**
- * 根据微信小程序openid获取用户认证信息
- *
- * @param openid 微信小程序openid
- * @return 用户认证信息,用户不存在返回null
- */
- public UserAuthInfo getAuthInfoByWechatOpenid(String openid) {
- return userSocialService.getAuthInfoByOpenid(SocialPlatformEnum.WECHAT_MINI, openid);
- }
-
- /**
- * 更新微信小程序session_key
- *
- * @param bindId 绑定记录ID
- * @param sessionKey session_key
- */
- public void updateWechatSessionKey(Long bindId, String sessionKey) {
- userSocialService.updateSessionKey(bindId, sessionKey);
- }
-}
diff --git a/src/main/java/com/youlai/boot/framework/security/service/UserAuthQueryService.java b/src/main/java/com/youlai/boot/framework/security/service/UserAuthQueryService.java
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/main/java/com/youlai/boot/framework/security/service/WxMaUserAuthQueryService.java b/src/main/java/com/youlai/boot/framework/security/service/WxMaUserAuthQueryService.java
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java b/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java
index 1b153f64..bdd44dab 100644
--- a/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java
+++ b/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java
@@ -17,30 +17,25 @@ import com.youlai.boot.framework.security.config.SecurityProperties;
import com.youlai.boot.framework.security.exception.TokenInvalidException;
import com.youlai.boot.framework.security.model.AuthenticationToken;
import com.youlai.boot.framework.security.model.RoleDataScope;
+import com.youlai.boot.framework.security.model.SecurityUserDetails;
import org.apache.commons.lang3.StringUtils;
-import com.youlai.boot.framework.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.*;
-import java.util.concurrent.TimeUnit; // Import TimeUnit
+import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
- * JWT Token 管理器
+ * JWT Token 管理器。
*
- * 实现基于JWT的无状态认证,支持:
- *
- * - Access Token + Refresh Token 双令牌机制
- * - Token 撤销(jti黑名单)
- * - 用户级会话失效(tokenVersion)
- * - 多角色数据权限存储
- *
+ * 基于 JWT 的无状态认证,支持 Access + Refresh 双令牌、Token 撤销(jti 黑名单)、
+ * 用户级会话失效(tokenVersion)、多角色数据权限存储。
+ *
+ * JWT claims 中存储角色编码(不带 ROLE_ 前缀),解析后由 {@link SecurityUserDetails#getAuthorities()} 运行时补前缀。
*
* @author Ray.Hao
* @since 2024/11/15
@@ -59,12 +54,6 @@ public class JwtTokenManager implements TokenManager {
this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes();
}
- /**
- * 生成令牌
- *
- * @param authentication 认证信息
- * @return 令牌响应对象
- */
@Override
public AuthenticationToken generateToken(Authentication authentication) {
int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive();
@@ -81,22 +70,15 @@ public class JwtTokenManager implements TokenManager {
.build();
}
- /**
- * 解析令牌
- *
- * @param token JWT Token
- * @return Authentication 对象
- */
@Override
public Authentication parseToken(String token) {
-
JWT jwt = JWTUtil.parseToken(token);
JSONObject payloads = jwt.getPayloads();
- SysUserDetails userDetails = new SysUserDetails();
- userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID
- userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID
+ SecurityUserDetails userDetails = new SecurityUserDetails();
+ userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID));
+ userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID));
- // 解析数据权限列表
+ // 数据权限
JSONArray dataScopesArray = payloads.getJSONArray(JwtClaimConstants.DATA_SCOPES);
if (dataScopesArray != null && !dataScopesArray.isEmpty()) {
List dataScopes = dataScopesArray.stream()
@@ -115,88 +97,60 @@ public class JwtTokenManager implements TokenManager {
userDetails.setDataScopes(dataScopes);
}
- userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名
- // 角色集合
- Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES)
- .stream()
- .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority)))
- .collect(Collectors.toSet());
+ userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT));
- return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
+ // 角色编码(不带 ROLE_ 前缀)
+ JSONArray rolesArray = payloads.getJSONArray(JwtClaimConstants.ROLES);
+ if (rolesArray != null && !rolesArray.isEmpty()) {
+ Set roles = rolesArray.stream()
+ .map(Convert::toStr)
+ .collect(Collectors.toSet());
+ userDetails.setRoles(roles);
+ }
+
+ return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
- /**
- * 校验令牌
- *
- * @param token JWT Token
- * @return 是否有效
- */
@Override
public boolean validateToken(String token) {
return validateToken(token, false);
}
- /**
- * 校验刷新令牌
- *
- * @param refreshToken JWT Token
- * @return 验证结果
- */
@Override
public boolean validateRefreshToken(String refreshToken) {
return validateToken(refreshToken, true);
}
- /**
- * 校验令牌
- *
- * 校验流程(按顺序执行):
- *
- * - 签名验证 + 过期时间检查
- * - 刷新令牌类型校验(仅刷新场景)
- * - tokenVersion 校验(用户级会话失效)
- * - jti 黑名单校验(单Token撤销)
- *
- *
- * @param token JWT Token
- * @param validateRefreshToken 是否校验刷新令牌类型
- * @return 是否有效
- */
private boolean validateToken(String token, boolean validateRefreshToken) {
JWT jwt = JWTUtil.parseToken(token);
- // 检查 Token 是否有效(验签 + 是否过期)
boolean isValid = jwt.setKey(secretKey).validate(0);
if (isValid) {
JSONObject payloads = jwt.getPayloads();
- // 1. 校验刷新令牌类型(仅在校验刷新令牌场景启用)
+ // 刷新令牌类型校验
String jti = payloads.getStr(JWTPayload.JWT_ID);
if (validateRefreshToken) {
- //刷新token需要校验token类别
boolean isRefreshToken = payloads.getBool(JwtClaimConstants.TOKEN_TYPE);
if (!isRefreshToken) {
return false;
}
}
- // 2. 校验 tokenVersion(用于按用户维度失效历史 Token)
- // 场景示例:用户修改密码、被管理员强制下线、手动"踢所有端"后,递增 tokenVersion,
- // 之前签发的 Token 因版本号不匹配而失效
+
+ // tokenVersion 校验(用户维度 Token 失效)
Long userId = payloads.getLong(JwtClaimConstants.USER_ID);
if (userId != null) {
Integer tokenVersion = payloads.getInt(JwtClaimConstants.TOKEN_VERSION);
-
+
String versionKey = StrUtil.format(RedisConstants.Auth.USER_TOKEN_VERSION, userId);
Object currentVersionObj = redisTemplate.opsForValue().get(versionKey);
int currentVersion = currentVersionObj != null ? Convert.toInt(currentVersionObj) : 0;
- // 版本号不匹配则 Token 无效(新签发的 Token 版本号必须 >= Redis 中的版本号)
if (tokenVersion == null || tokenVersion < currentVersion) {
return false;
}
}
- // 3. 判断 Token 是否已被撤销(单端退出/会话注销)
- // 场景示例:单点退出登录、后台手动注销某个会话、封禁账号后立即阻断当前 Token 等
+ // jti 黑名单校验
if (isTokenRevoked(jti)) {
return false;
}
@@ -204,17 +158,11 @@ public class JwtTokenManager implements TokenManager {
return isValid;
}
- /**
- * 将令牌加入黑名单
- *
- * @param token JWT Token
- */
@Override
public void invalidateToken(String token) {
if (StringUtils.isBlank(token)) {
return;
}
-
if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
}
@@ -225,77 +173,15 @@ public class JwtTokenManager implements TokenManager {
revokeTokenByJti(jti, expirationAt);
}
- /**
- * 检查Token是否已被撤销
- *
- * @param jti Token唯一标识
- * @return true-已撤销,false-未撤销
- */
- private boolean isTokenRevoked(String jti) {
- if (StringUtils.isBlank(jti)) {
- return false;
- }
- return Boolean.TRUE.equals(redisTemplate.hasKey(StrUtil.format(RedisConstants.Auth.REVOKED_JTI, jti)));
- }
-
- /**
- * 将Token加入撤销黑名单
- *
- * 黑名单有效期与Token剩余有效期一致,避免永久存储
- *
- * @param jti Token唯一标识
- * @param expirationAt Token过期时间戳
- */
- private void revokeTokenByJti(String jti, Integer expirationAt) {
- if (StringUtils.isBlank(jti)) {
- return;
- }
-
- String revokedJtiKey = StrUtil.format(RedisConstants.Auth.REVOKED_JTI, jti);
- if (expirationAt != null) {
- int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000);
- if (expirationAt < currentTimeSeconds) {
- return;
- }
- int expirationIn = expirationAt - currentTimeSeconds;
- redisTemplate.opsForValue().set(revokedJtiKey, Boolean.TRUE, expirationIn, TimeUnit.SECONDS);
- } else {
- redisTemplate.opsForValue().set(revokedJtiKey, Boolean.TRUE);
- }
- }
-
- /**
- * 失效指定用户的所有会话
- *
- * 通过递增用户 tokenVersion,使该用户之前签发的所有 Token 因版本号不匹配而失效。
- *
- * 适用场景:
- *
- * - 用户修改密码
- * - 管理员强制下线用户
- * - 用户主动踢出所有设备
- * - 用户被禁用
- *
- *
- * @param userId 用户ID
- */
@Override
public void invalidateUserSessions(Long userId) {
if (userId == null) {
return;
}
-
String versionKey = StrUtil.format(RedisConstants.Auth.USER_TOKEN_VERSION, userId);
- // 递增版本号,无需设置 TTL(版本号永久有效,避免 TTL 过期导致的安全问题)
redisTemplate.opsForValue().increment(versionKey);
}
- /**
- * 刷新令牌
- *
- * @param refreshToken 刷新令牌
- * @return 令牌响应对象
- */
@Override
public AuthenticationToken refreshToken(String refreshToken) {
boolean isValid = validateRefreshToken(refreshToken);
@@ -313,45 +199,19 @@ public class JwtTokenManager implements TokenManager {
.build();
}
- /**
- * 生成 JWT Token
- *
- * @param authentication 认证信息
- * @param ttl 过期时间(秒),-1表示永不过期
- * @return JWT Token字符串
- */
+ // ======================== private ========================
+
private String generateToken(Authentication authentication, int ttl) {
return generateToken(authentication, ttl, false);
}
-
- /**
- * 生成 JWT Token
- *
- * Payload包含:
- *
- * - userId - 用户ID
- * - deptId - 部门ID
- * - dataScopes - 数据权限列表
- * - authorities - 角色权限集合
- * - tokenType - 是否为刷新令牌
- * - tokenVersion - Token版本号(用于会话失效控制)
- * - iat/exp - 签发/过期时间
- * - jti - Token唯一标识(用于撤销)
- *
- *
- * @param authentication 认证信息
- * @param ttl 过期时间(秒)
- * @param isRefreshToken 是否为刷新令牌
- * @return JWT Token字符串
- */
private String generateToken(Authentication authentication, int ttl, boolean isRefreshToken) {
- SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
+ SecurityUserDetails userDetails = (SecurityUserDetails) authentication.getPrincipal();
Map payload = new HashMap<>();
- payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID
- payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID
+ payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId());
+ payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId());
- // 存储数据权限列表
+ // 数据权限
List dataScopes = userDetails.getDataScopes();
if (dataScopes != null && !dataScopes.isEmpty()) {
List