diff --git a/README.en.md b/README.en.md new file mode 100644 index 00000000..a58091c6 --- /dev/null +++ b/README.en.md @@ -0,0 +1,192 @@ +
+ +# youlai-boot youlai-boot + +[English](./README.en.md) · [简体中文](./README.md) + +**Enterprise-grade permission management backend based on Spring Boot 4** + +[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-4.0.5-6DB33F?logo=spring-boot)](https://spring.io/projects/spring-boot) +[![JDK](https://img.shields.io/badge/JDK-17%2B-007396?logo=openjdk)](https://openjdk.org/) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue?logo=apache)](LICENSE) +[![Gitee Star](https://gitee.com/youlaiorg/youlai-boot/badge/star.svg)](https://gitee.com/youlaiorg/youlai-boot/stargazers) +[![GitHub Star](https://img.shields.io/github/stars/youlaitech/youlai-boot?style=social)](https://github.com/youlaitech/youlai-boot) +[![GitCode Star](https://gitcode.com/youlai/youlai-boot/star/badge.svg)](https://gitcode.com/youlai/youlai-boot/stargazers) + +
+ +![](https://foruda.gitee.com/images/1708618984641188532/a7cca095_716974.png "rainbow.png") + +
+ +[🖥️ Live Preview](https://vue.youlai.tech) | [📱 Mobile Preview](https://app.youlai.tech) | [📖 Documentation](https://www.youlai.tech/docs/server/spring-boot/) + +
+ +## Introduction + +**youlai-boot** is an enterprise-grade permission management backend built on Spring Boot 4. It ships with the frontend [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) and the mobile app [youlai-app](https://gitee.com/youlaiorg/youlai-app), and is one of **7 language implementations** (Java / Node.js / Go / Python / PHP / C# / Rust) that share the same API specification and database schema. It is suitable for learning, reference, and secondary development of enterprise admin systems. + +## Core Features + +- 🔐 **Security** — Spring Security + JWT/Redis dual-session model, token renewal, multi-device mutual exclusion +- 🛡️ **Fine-grained permissions** — 5-level RBAC: data → menu → button → API → field +- ⚡ **Code generator** — one-click generation of full-stack CRUD code +- 📦 **Complete modules** — users, roles, menus, departments, dictionaries, files, scheduled tasks, message center, operation logs +- 🌐 **Multi-tenant SaaS** — data isolation + tenant config, with a standalone [youlai-boot-tenant](https://gitee.com/youlaiorg/youlai-boot-tenant) edition +- 🔌 **Real-time communication** — SSE push: online user count, dictionary sync, notification broadcast + +## System Preview + +**PC** + + + + + + + + + + + + + + +
PC Preview 1PC Preview 2
PC Preview 3PC Preview 4
PC Preview 5PC Preview 6
+ +**Mobile** + + + + + + + + +
App Preview 1App Preview 2App Preview 3App Preview 4
+ +## Quick Start + +**Requirements**: JDK 17+ · MySQL 8.0+ · Redis 6.0+ + +1. Clone: `git clone https://gitee.com/youlaiorg/youlai-boot.git` +2. Import database: `sql/youlai-admin.sql` +3. Adjust config (optional, a read-only online data source is configured by default): `src/main/resources/application-dev.yml` +4. Start and visit http://localhost:8000/doc.html + +Default credentials: `admin` / `123456` + +**Docker**: `cd deploy/docker`, then `docker-compose up -d` + +Detailed guide: [Deployment Docs](https://www.youlai.tech/docs/server/spring-boot/deploy) · [Dev Standards](https://www.youlai.tech/docs/server/spring-boot/dev-standards) + +## Tech Stack + +| Tech | Version | Description | +|:-----|:--------|:------------| +| Spring Boot | 4.0.5 | Core framework | +| Spring Security | 6.x | Auth & authorization | +| MyBatis-Plus | 3.5.15 | ORM | +| Druid | 1.2.24 | Connection pool | +| Redis + Redisson | 6.0+ / 4.1.0 | Cache · Session · Distributed lock | +| Caffeine | 2.9.3 | Local cache | +| XXL-Job | 3.2.0 | Distributed scheduled tasks | +| Knife4j | 4.5.0 | API docs | +| MapStruct | 1.6.3 | Object mapping | +| MinIO | 8.5.10 | Object storage | + +## Directory Structure + +``` +youlai-boot/ +├── deploy/ +│ └── docker/ # Docker orchestration +├── docs/ # Docs and image assets +├── sql/ # Database init scripts +├── src/main/java/com/youlai/boot/ +│ ├── YouLaiBootApplication.java # Bootstrap class +│ ├── auth/ # Auth (login/logout/token) +│ ├── codegen/ # Code generator +│ ├── common/ # Common module (constants/enums/response) +│ ├── file/ # File service (MinIO/local/OSS) +│ ├── framework/ # Technical framework layer +│ │ ├── apidoc/ # OpenAPI / Knife4j +│ │ ├── cache/ # Redis / Caffeine cache +│ │ ├── captcha/ # Graphic captcha +│ │ ├── integration/ # SMS / Email / WeChat +│ │ ├── job/ # XXL-Job scheduled tasks +│ │ ├── mybatis/ # MyBatis-Plus config +│ │ ├── security/ # Security / JWT / Token +│ │ └── web/ # Global exception / CORS / rate limit +│ ├── message/ # SSE push +│ └── system/ # Business (user/role/menu/dept) +└── pom.xml # Maven dependency management +``` + +## Ecosystem + +**Frontend** + +| Project | Stack | Description | +|:-----|:------|:------------| +| [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) | Vue 3 + Element Plus | PC admin frontend (recommended) | +| [youlai-app](https://gitee.com/youlaiorg/youlai-app) | Vue 3 + UniApp | Mobile App | + +**Backend** + +| Project | Stack | Description | +|:-----|:------|:------------| +| [youlai-nest](https://gitee.com/youlaiorg/youlai-nest) | NestJS + TypeORM | Node.js | +| [youlai-gin](https://gitee.com/youlaiorg/youlai-gin) | Go + Gorm | Go | +| [youlai-django](https://gitee.com/youlaiorg/youlai-django) | Django + DRF | Python | +| [youlai-thinkphp](https://gitee.com/youlaiorg/youlai-thinkphp) | ThinkPHP 8 | PHP | +| [youlai-aspnet](https://gitee.com/youlaiorg/youlai-aspnet) | ASP.NET Core | C# | +| [youlai-rust](https://gitee.com/youlaiorg/youlai-rust) | Axum + SeaORM | Rust | + +> **youlai-boot** also provides the following variants and branches: [Multi-Tenant](https://gitee.com/youlaiorg/youlai-boot-tenant) (Spring Boot 4) · [MyBatis-Flex](https://gitee.com/youlaiorg/youlai-boot-flex) (Spring Boot 4) · [Spring Boot 3](https://gitee.com/youlaiorg/youlai-boot/tree/spring-boot-3) · [PostgreSQL](https://gitee.com/youlaiorg/youlai-boot/tree/db-pg) · [Multi-Module](https://gitee.com/youlaiorg/youlai-boot/tree/multi-module) +> +> The seven backends share the same **RESTful API specification** and **database schema**, so the frontend can switch seamlessly. + +## Documentation + +| Resource | Link | +|:-----|:-----| +| 📖 Full docs site | [www.youlai.tech](https://www.youlai.tech/) | +| 🖥️ PC live preview | [vue.youlai.tech](https://vue.youlai.tech) | +| 📱 Mobile live preview | [app.youlai.tech](https://app.youlai.tech) | +| 🔗 Apifox API docs | [apifox.com](https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5) | +| 🔗 Local API docs | [localhost:8000/doc.html](http://localhost:8000/doc.html) | + +## Contributing + +Issues and Pull Requests are welcome! See the [Contribution Guide](https://www.youlai.tech/faq/help). + +[![Contributors](https://contrib.rocks/image?repo=haoxianrui/youlai-boot)](https://github.com/haoxianrui/youlai-boot/graphs/contributors) + +## License + +Released under the [Apache License 2.0](LICENSE); free for commercial use. + +--- + + + + + + + + + +
+ Official WeChat Account
+ Official WeChat Account +
     + Mini Program
+ Mini Program +
     + Add author on WeChat
+ Add author on WeChat +
+ +

Technical discussion · Feedback · Business cooperation

diff --git a/README.md b/README.md index 1d498c4c..ed6b9c57 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@
-youlai-boot +# youlai-boot youlai-boot -# youlai-boot +[English](./README.en.md) · [简体中文](./README.md) **Spring Boot 4 企业级权限管理系统后端** @@ -25,7 +25,7 @@ ## 简介 -**youlai-boot** 是一套基于 Spring Boot 4 的企业级权限管理系统后端,配套前端 [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) 和移动端 [youlai-app](https://gitee.com/youlaiorg/youlai-app),并提供 **6 种语言实现**(Java / Node.js / Go / Python / PHP / C#),共享同一套 API 规范与数据库结构。适用于企业中后台管理系统的学习参考与二次开发。 +**youlai-boot** 是一套基于 Spring Boot 4 的企业级权限管理系统后端,配套前端 [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) 和移动端 [youlai-app](https://gitee.com/youlaiorg/youlai-app),并提供 **7 种语言实现**(Java / Node.js / Go / Python / PHP / C# / Rust),共享同一套 API 规范与数据库结构。适用于企业中后台管理系统的学习参考与二次开发。 ## 核心特性 @@ -36,12 +36,6 @@ - 🌐 **多租户 SaaS** — 数据隔离 + 租户配置,独立 [youlai-boot-tenant](https://gitee.com/youlaiorg/youlai-boot-tenant) 版本 - 🔌 **实时通信** — SSE 推送:在线用户数、字典同步、通知广播 -## 技术架构 - -

- youlai-boot 技术架构图 -

- ## 系统预览 **PC 端** @@ -148,10 +142,11 @@ youlai-boot/ | [youlai-django](https://gitee.com/youlaiorg/youlai-django) | Django + DRF | Python | | [youlai-thinkphp](https://gitee.com/youlaiorg/youlai-thinkphp) | ThinkPHP 8 | PHP | | [youlai-aspnet](https://gitee.com/youlaiorg/youlai-aspnet) | ASP.NET Core | C# | +| [youlai-rust](https://gitee.com/youlaiorg/youlai-rust) | Axum + SeaORM | Rust | > **youlai-boot** 还提供以下变种和分支版本:[多租户](https://gitee.com/youlaiorg/youlai-boot-tenant)(Spring Boot 4)· [MyBatis-Flex](https://gitee.com/youlaiorg/youlai-boot-flex)(Spring Boot 4)· [Spring Boot 3](https://gitee.com/youlaiorg/youlai-boot/tree/spring-boot-3) · [PostgreSQL](https://gitee.com/youlaiorg/youlai-boot/tree/db-pg) · [多模块](https://gitee.com/youlaiorg/youlai-boot/tree/multi-module) > -> 六种后端共享同一套 **RESTful API 规范** 和 **数据库结构**,前端可无缝切换。 +> 七种后端共享同一套 **RESTful API 规范** 和 **数据库结构**,前端可无缝切换。 ## 文档资源 @@ -178,17 +173,17 @@ youlai-boot/ diff --git a/docs/deep-research-report.md b/docs/deep-research-report.md deleted file mode 100644 index 842edae6..00000000 --- a/docs/deep-research-report.md +++ /dev/null @@ -1,452 +0,0 @@ - - -# 概要(已归档) - -> **本文档已于 2026-07-04 完整合并至 `security-refactor-plan.md`(v2.1)。** -> -> 合并时进行的去重处理: -> - 虚构模型 `SecurityUser` → 统一使用实际存在的 `UserAuthInfo` -> - 4 端口方案 → 精简为 2 核心端口(`UserAuthenticationPort`、`PermissionPort`)+ 1 可选扩展(`OnlineUserPort`,移至附录) -> - `RolePort` 删除(roles 已包含在 `UserAuthInfo` 中,单独端口冗余) -> - 重复的 Ports & Adapters 模式介绍 → 精简为第二节 2.1 -> - 重复的迁移步骤/代码审查清单 → 合并至统一章节 -> -> 以下为原始内容,仅供历史参考。 - ---- - -当前 `framework/security` 模块直接依赖 `system` 模块的用户、角色等服务,实现登录认证和权限校验。比如 `SecurityUserDetailsService` 中通过 `UserService.getAuthInfoByUsername(...)` 获取用户认证信息,并使用 `SecurityUserDetails` 包装返回;还引入了 `UserSocialService` 等系统服务。这种实现导致安全模块与系统模块耦合度极高,不利于后续模块化或替换。按照 Ports & Adapters(六边形架构)原则,**应将安全模块对系统模块的调用抽象为接口(Port)**,在系统模块提供对应的实现(Adapter)。这样安全模块只依赖接口,而不直接引用系统实体或服务,实现内核业务与外部实现的解耦。下文给出全面的重构设计:定义清晰的包结构、接口(Port)命名规范、适配器实现示例、事件设计、自动装配等,并通过对比表、迁移步骤和测试/审查清单详细说明改动原因与风险。 - -**主要改进**:将原来直接使用 `UserService`/`UserSocialService` 等系统服务的逻辑,迁移到新的接口调用上。以 `UserAuthenticationPort`、`PermissionPort`、`RolePort`、`OnlineUserPort` 等抽象端口替代 `SecurityUserDetailsService` 中的调用;在系统模块中提供适配器实现;修改原认证/权限代码使用这些端口接口,从而使安全模块与系统模块仅通过抽象接口交互,实现松耦合和更高的可测试性。 - -# 现状 vs 建议(关键点比较) - -| 对比维度 | **当前实现(耦合严重)** | **建议实现(使用 Ports & Adapters)** | -|-----------|-------------------------|---------------------------------------| -| **依赖关系** | `framework/security` 直接依赖 `system` 模块的实体和服务。
代码中大量 `import com.youlai.boot.system.*`。 | 安全模块仅引用自身定义的接口(Port),不再直接 `import system`。系统模块实现这些接口并注入。
安全模块不直接依赖系统实体,只通过抽象调用。 | -| **用户认证** | 在 `SecurityUserDetailsService` 中通过 `UserService` 查询用户认证数据。 | 定义 `UserAuthenticationPort` 接口(`loadUserByUsername`),由系统侧适配器实现。`SecurityUserDetailsService` 调用该接口获得用户信息。 | -| **角色权限** | `SecurityUserDetails` 构造时直接使用 `UserAuthInfo.getRoles()`等;权限校验使用 `PermissionService`(注入 `RoleMenuService`)动态获取权限集合。 | 定义 `RolePort`、`PermissionPort` 接口,分别提供用户角色、用户权限查询方法。系统模块通过查询角色表、角色-权限表等实现。安全模块使用端口获取角色/权限。 | -| **在线用户** | 无专门接口;若记录在线用户,需要手动在登录逻辑中调用系统服务。 | 定义 `OnlineUserPort` 接口,在登录成功/退出时由安全模块发布事件调用该接口(示例:`online(SecurityUser user)`,`offline(Long userId)`)。系统侧实现可记录用户登录状态或统计。 | -| **命名规范** | 普通 Service 命名(`XxxService`、`XxxServiceImpl`);未区分 Ports/Adapters 概念。 | 端口接口统一以 `Port` 结尾,如 `UserAuthenticationPort`、`PermissionPort` 等;适配器类可以 `Adapter` 或 `ServiceImpl` 后缀。符合常见 *hexagonal* 实践。 | -| **包结构** | 安全模块代码散落在 `security.service`、`security.model` 等包中,与业务服务混合。 | 建议在安全模块新增 `security.port` 包放置接口,或置于 `security.service` 目录下;系统模块新增 `adapter` 包放置实现。逻辑清晰分层。 | -| **测试可控** | 安全代码测试需要引入系统服务(耦合高)。 | 安全逻辑可针对 Port 接口单元测试(使用模拟实现),系统实现也可单独测试。解耦后更易覆盖测试。 | -| **演进灵活** | 更换系统或提取模块成本高。 | 通过实现不同的 Port 适配器,可灵活替换系统实现;后续拆分为 Starter 模式时,只需发布 Port 接口和依赖注入方案。 | - -# Ports & Adapters 模式简介 - -**六边形架构(Ports & Adapters)**模式提出应用核心业务逻辑通过抽象的"端口(Port)"与外部通信,实现对外界依赖的隔离。外部实现(如数据库、Web 服务、其他模块)通过具体的适配器(Adapter)连入端口。通俗地说,将原本直接调用的外部组件封装为接口(Port),核心业务只依赖接口而非具体实现;在外围提供实现类(Adapter)注入接口。此模式不是 GOF 中的"23 种设计模式"之一,而是一种架构模式,常被称为**六边形架构**或**Hexagonal Architecture**。 - -在 Spring Boot 项目中,可将安全模块核心逻辑看作"内部六边形",定义各类服务所需的接口(Ports),而系统模块提供对应实现。例如: - -- **端口(Port)接口**:定义安全模块需要的功能,如加载用户信息、查询权限、角色等。接口可使用 `Port` 后缀命名以示区分(`UserAuthenticationPort`、`PermissionPort`、`RolePort`、`OnlineUserPort`)。 -- **适配器(Adapter)实现**:在系统模块中实现上述接口,内部调用 `UserService`、`RoleService`、`RoleMenuService` 等具体服务,将数据转换成安全模块期望的格式。使用 `@Component` 等注解注入 Spring 容器。 -- **解耦效果**:安全模块只在编译期看到接口,不持有系统包的任何引用,大幅降低耦合。系统模块可自行修改实现细节或拆分发布,只要契合接口契约即可。 - -例如,下述接口定义了登录认证所需的端口: - -```java -package com.youlai.boot.security.port; - -/** - * 用户认证接口 (Port). - * 由系统模块提供实现,用于根据用户名加载用户安全信息。 - */ -public interface UserAuthenticationPort { - - /** - * 根据用户名加载用户认证信息(包含用户名、密码、启用状态、角色列表等)。 - * - * @param username 用户名 - * @return 包含用户认证信息的SecurityUser对象,如果用户不存在可返回null或抛出异常 - * @throws UsernameNotFoundException 如果未找到用户 - */ - SecurityUser loadUserByUsername(String username) throws UsernameNotFoundException; -} -``` - -同理,可定义获取权限和角色的接口(示例): - -```java -package com.youlai.boot.security.port; - -/** - * 权限查询接口 (Port). - * 提供获取指定用户权限集合的方法。 - */ -public interface PermissionPort { - /** - * 获取指定用户 ID 的权限字符串集合。 - * - * @param userId 用户ID - * @return 权限字符串集合,如"user:list", "order:detail"等 - */ - Set getPermissions(Long userId); -} - -/** - * 角色查询接口 (Port). - * 提供获取指定用户角色集合的方法。 - */ -public interface RolePort { - /** - * 获取指定用户 ID 的角色标识集合。 - * - * @param userId 用户ID - * @return 角色编码集合,如"ADMIN", "USER"等 - */ - Set getRoles(Long userId); -} - -/** - * 在线用户通知接口 (Port). - * 用户登录登出时用于通知系统模块记录在线状态。 - */ -public interface OnlineUserPort { - /** - * 用户上线通知,登录认证成功后调用。 - * @param user 登录成功的安全用户信息 - */ - void online(SecurityUser user); - - /** - * 用户下线通知,可在登出或 token 失效时调用。 - * @param userId 用户ID - */ - void offline(Long userId); -} -``` - -以上接口(Ports)只属于安全模块,无系统包引用。接口命名使用了 `Port` 后缀,这是一种常见约定,用于区分业务接口与基础设施实现。**命名规范**上,接口名应精确描述业务功能,遵循驼峰式命名,端口名称以英文名词加上"Port"后缀,如 `UserAuthenticationPort`;实现类可名为 `SystemUserAuthenticationAdapter` 或 `UserAuthenticationAdapterImpl` 等,并标记为 Spring 的组件(`@Component`)。 - -# 包结构与端口定义 - -重构后的建议包结构示例如下(省略部分不相关包): - -``` -com.youlai.boot.security -├── config/ # 安全模块配置(如 WebSecurity 配置等) -├── port/ # 定义端口接口(Ports) -│ ├── UserAuthenticationPort.java -│ ├── PermissionPort.java -│ ├── RolePort.java -│ └── OnlineUserPort.java -├── service/ # 安全模块内部服务实现(仅业务逻辑,无系统依赖) -│ ├── SecurityUserDetailsService.java # 实现 UserDetailsService,调用 Port 接口 -│ └── ... # 其他认证服务 -├── handler/ # 异常、结果处理器等 -├── filter/ # 安全过滤器,如 JWT 过滤等 -├── model/ # 安全模块领域模型,如 SecurityUser、UserSession 等 -└── ... # 其他安全相关包 -com.youlai.boot.system -└── adapter/ # 系统模块中的适配器实现 - ├── UserAuthenticationAdapter.java # 实现 UserAuthenticationPort - ├── PermissionAdapter.java # 实现 PermissionPort - ├── RoleAdapter.java # 实现 RolePort - ├── OnlineUserAdapter.java # 实现 OnlineUserPort - └── ... -``` - -其中,`com.youlai.boot.security.model.SecurityUser` 是我们自定义的安全用户模型,包含用户ID、用户名、密码、启用状态、角色、权限等字段,用于端口接口返回(可参考 `UserAuthInfo`)。安全核心逻辑不直接使用 `com.youlai.boot.system.model.entity.*`,而是使用自己的 `SecurityUser` 或相似 DTO。SecurityUserDetails 的构造可改为接受 `SecurityUser`。 - -### 接口(Port)定义 - -以登录认证为例,`UserAuthenticationPort` 接口定义如下: - -```java -package com.youlai.boot.security.port; - -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -/** - * 用户认证端口(Port)。 - * Security 模块通过此接口加载用户认证信息,由系统模块提供实现。 - */ -public interface UserAuthenticationPort { - - /** - * 根据用户名加载用户安全信息,包括用户名、密码、启用状态、角色列表等。 - * - * @param username 用户名 - * @return 包含用户认证数据的 SecurityUser 对象 - * @throws UsernameNotFoundException 用户不存在时抛出 - */ - SecurityUser loadUserByUsername(String username) throws UsernameNotFoundException; -} -``` - -类似地,`PermissionPort`、`RolePort`、`OnlineUserPort` 接口如前所示。所有接口方法应带有清晰的 Javadoc 注释,说明参数和返回值(或异常)。方法命名使用动词开头,如 `getPermissions`、`getRoles`、`online`、`offline`,与团队命名规范一致。返回类型避免使用系统实体类,而是安全模块自己的模型或基础类型。 - -# 适配器实现示例 - -在系统模块中实现上述接口。适配器需标注为 Spring 组件(如 `@Component`),以便自动注入。示例: - -```java -package com.youlai.boot.system.adapter; - -import com.youlai.boot.security.model.SecurityUser; -import com.youlai.boot.security.port.UserAuthenticationPort; -import com.youlai.boot.system.service.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Component; - -/** - * 系统用户适配器,实现安全模块的 UserAuthenticationPort。 - */ -@Component -public class UserAuthenticationAdapter implements UserAuthenticationPort { - - @Autowired - private UserService userService; - - @Override - public SecurityUser loadUserByUsername(String username) throws UsernameNotFoundException { - // 调用系统服务获取用户认证信息(可能包括用户实体、角色列表等) - var userAuthInfo = userService.getAuthInfoByUsername(username); - if (userAuthInfo == null) { - throw new UsernameNotFoundException("用户名不存在: " + username); - } - // 将系统返回的数据转换为安全模块的 SecurityUser - SecurityUser user = new SecurityUser(); - user.setUserId(userAuthInfo.getUserId()); - user.setUsername(userAuthInfo.getUsername()); - user.setPassword(userAuthInfo.getPassword()); - user.setEnabled(userAuthInfo.getStatus() == 1); - user.setDeptId(userAuthInfo.getDeptId()); - user.setRoles(userAuthInfo.getRoles()); - // 如果需要,也可以在此填充用户权限 - return user; - } -} -``` - -在上例中,**`UserAuthenticationAdapter` 实现了 `UserAuthenticationPort`**,在调用时系统模块可直接注入此类。它内部调用原来的 `UserService.getAuthInfoByUsername`,然后将数据封装为安全模块领域的 `SecurityUser`。如此,安全模块再也无需直接引用 `com.youlai.boot.system` 包,只关心接口和 `SecurityUser` 类型。 - -权限、角色适配器示例(系统模块): - -```java -package com.youlai.boot.system.adapter; - -import com.youlai.boot.security.port.PermissionPort; -import com.youlai.boot.security.port.RolePort; -import com.youlai.boot.system.service.RoleMenuService; -import com.youlai.boot.system.service.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import java.util.Set; - -/** - * 系统权限/角色适配器。 - */ -@Component -public class PermissionAndRoleAdapter implements PermissionPort, RolePort { - - @Autowired - private RoleMenuService roleMenuService; // 系统服务,用于获取角色权限 - - @Autowired - private UserService userService; // 系统服务,用于获取用户角色 - - @Override - public Set getRoles(Long userId) { - // 调用系统服务查询用户角色标识列表(需实现此方法或自行查询用户角色关联表) - return userService.getUserRoleCodes(userId); - } - - @Override - public Set getPermissions(Long userId) { - // 根据用户角色获取权限列表(类似原有 PermissionService 逻辑) - Set roles = getRoles(userId); - if (roles == null || roles.isEmpty()) { - return Set.of(); - } - // roleMenuService.getRolePermsByRoleCodes 返回角色对应的菜单/权限标识 - return roleMenuService.getRolePermsByRoleCodes(roles); - } -} -``` - -注意:上述代码假定 `UserService` 提供 `getUserRoleCodes(userId)` 方法来返回用户拥有的角色编码集合;如果没有,需自行查询 `sys_user_role` 表并结合 `SysRole` 表获取。`RoleMenuService.getRolePermsByRoleCodes(roles)` 方法则返回对应角色的所有权限字符串。这些实现细节可根据系统模块已有功能调整,但**接口契约应保证返回安全模块期望的格式**。 - -`OnlineUserPort` 的实现示例:当用户登录或退出时,安全模块可发布事件(详见下一节)由适配器捕获并执行,比如记录在线用户或更新登录时间: - -```java -package com.youlai.boot.system.adapter; - -import com.youlai.boot.security.model.SecurityUser; -import com.youlai.boot.security.port.OnlineUserPort; -import com.youlai.boot.system.service.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -/** - * 在线用户适配器,实现上线/下线通知。 - */ -@Component -public class OnlineUserAdapter implements OnlineUserPort { - - @Autowired - private UserService userService; - - @Override - public void online(SecurityUser user) { - // 系统记录用户上线事件(例如保存用户最后登录时间、标记在线状态) - userService.markUserOnline(user.getUserId()); - } - - @Override - public void offline(Long userId) { - // 系统记录用户下线事件 - userService.markUserOffline(userId); - } -} -``` - -其中 `markUserOnline/markUserOffline` 需在系统模块实现,可简单更新用户实体字段或写入登录日志。 - -# 事件设计 - -为了在用户登录成功或退出时自动调用 `OnlineUserPort`,可使用 Spring 事件机制或 Spring Security 提供的监听器。例如,定义监听 Spring Security 的 `AuthenticationSuccessEvent` 和自定义登出事件: - -```java -@Component -public class AuthenticationEventListener { - - @Autowired - private OnlineUserPort onlineUserPort; - - // 登录成功事件 - @EventListener - public void onAuthenticationSuccess(AuthenticationSuccessEvent event) { - Object principal = event.getAuthentication().getPrincipal(); - if (principal instanceof SecurityUser) { - SecurityUser user = (SecurityUser) principal; - onlineUserPort.online(user); - } - } - - // 注销事件(可用 ServletRequestListener 或自定义 Token 过期事件触发) - @EventListener - public void onLogoutSuccess(CustomLogoutEvent event) { - Long userId = event.getUserId(); - onlineUserPort.offline(userId); - } -} -``` - -在上例中,当用户认证成功时,将触发 `online(SecurityUser)` 调用;当用户登出或 token 过期时,可触发 `offline` 调用。这样无须在业务逻辑手工嵌入标记在线/离线逻辑,符合观察者模式设计。 - -**事件时序示例**(Mermaid 时序图): - -```mermaid -sequenceDiagram - participant Client 客户端 - participant Security 安全模块 - participant Port 用户认证端口 - participant System 系统模块 - Client->>Security: 发起登录请求 - Security->>Port: loadUserByUsername(username) - Port->>System: 调用 UserService.getAuthInfoByUsername(...) - System-->>Port: 返回用户数据 - Port-->>Security: 返回 SecurityUser - Security->>Port: online(SecurityUser) // 通过事件或直接调用 - Port->>System: 调用 UserService.markUserOnline - System-->>Port: 确认 -``` - -# 自动装配与配置 - -由于还未拆分为独立 Starter,可借助 Spring Boot 自动扫描和条件注入来保证端口与适配器正确关联: - -- 在安全模块中无需特殊配置,只要扫描到 `com.youlai.boot.security.port` 下的接口即可(Spring 会自动注入匹配类型的 Bean)。接口注解可以省略。 -- 在系统模块适配器类上加 `@Component`(或其他 stereotype),保证其 Bean 名称默认即类名首字母小写,可用 `@Qualifier` 指定(非必需)。Spring 容器会自动将实现注入到安全模块需要的接口引用处。 -- 如果未来拆分为独立的 `security-starter`,可编写一个自动配置类,`@ConditionalOnMissingBean(UserAuthenticationPort.class)` 等方式提供默认实现或抛出警告。但当前单体工程中可省略。 - -# 单元/集成测试要点 - -1. **安全模块单元测试**:对 `SecurityUserDetailsService` 等类进行单元测试时,模拟 `UserAuthenticationPort` 等依赖接口(使用 Mock 对象)。验证当接口返回合法的 `SecurityUser` 时,`loadUserByUsername` 正确返回 `UserDetails`;当返回 null 或抛异常时,抛出 `UsernameNotFoundException`。 -2. **适配器单元测试**:对系统模块的适配器类进行测试。可以使用内存数据库或模拟 `UserService`、`RoleMenuService` 以测试 `UserAuthenticationAdapter`、`PermissionAdapter` 等正确调用系统服务并转换结果。 -3. **集成测试**:启动 Spring 容器,加载真实的 `UserService` 与端口实现,测试完整的登录认证流程。例如插入一个测试用户,模拟登录请求,验证能通过认证并获得正确角色权限。还可测试登录事件触发 `OnlineUserPort` 的调用。 -4. **兼容性测试**:迁移期间同时保留旧有和新实现路径,在新的接口实现加入时,可通过配置文件开关或条件注册方式,确保临时并存(如果需要灰度)。测试现有系统接口不受影响。 - -# 迁移步骤(逐步变更清单) - -以下是重构安全模块的推荐步骤,可按次序逐步实施,每步均编写并运行相关测试,确认无误后继续下一步。示例以 git 操作为准,可调整为团队实际流程。 - -1. **新建端口接口**:在 `security` 模块新建 `com.youlai.boot.security.port` 包,添加 `UserAuthenticationPort`, `PermissionPort`, `RolePort`, `OnlineUserPort` 接口(如上定义)。 - ```shell - # 新建包和文件(示例路径): - mkdir -p src/main/java/com/youlai/boot/security/port - ``` - 在 `UserAuthenticationPort.java` 中写入接口定义和注释。其余接口同理。 -2. **系统模块实现适配器**:在 `system` 模块中新建 `adapter` 包,实现上述接口。例如: - - `UserAuthenticationAdapter implements UserAuthenticationPort`(`@Component`),内部调用原有 `UserService.getAuthInfoByUsername(...)` 并转换为 `SecurityUser`。 - - `PermissionAdapter implements PermissionPort`,使用 `RoleMenuService` 获取权限集合。 - - `RoleAdapter implements RolePort`,使用 `UserService` 或 `SysUserRoleMapper` 获取角色集合。 - - `OnlineUserAdapter implements OnlineUserPort`,调用系统服务记录在线状态。 - 编写完成后,启动 Spring,检查这些 Bean 是否被扫描注入。 -3. **修改认证服务使用 Port**:修改 `security/service/SecurityUserDetailsService.java`: - - 去掉对 `UserService`、`UserSocialService` 的注入。 - - 增加对 `UserAuthenticationPort` 的注入。例如: - ```diff - @RequiredArgsConstructor - @Service -- public class SecurityUserDetailsService implements UserDetailsService { -+ public class SecurityUserDetailsService implements UserDetailsService { - // 删除:private final UserService userService; - // 删除:private final UserSocialService userSocialService; - private final UserAuthenticationPort userAuthPort; - ``` - - 修改 `loadUserByUsername` 方法,调用 `userAuthPort.loadUserByUsername(username)` 取代原有的 `userService.getAuthInfoByUsername`。如: - ```diff - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { -- UserAuthInfo userAuthInfo = userService.getAuthInfoByUsername(username); -+ SecurityUser user = userAuthPort.loadUserByUsername(username); -+ if (user == null) { throw new UsernameNotFoundException(username); } -- return new SecurityUserDetails(userAuthInfo); -+ return new SecurityUserDetails(user); - } - ``` - - 由于 `SecurityUserDetails` 构造函数接收的类型可能变更为 `SecurityUser`(或兼容的DTO),需要相应调整 `SecurityUserDetails`。例如新增一个构造函数或修改现有构造逻辑。 -4. **移除系统引用**:清理安全模块中对 `com.youlai.boot.system.*` 的直接引用。确认没有遗留的导入。例如删除 `import com.youlai.boot.system.service.UserService;` 等。如果项目中还存在其他安全相关的系统依赖,同样替换为调用端口。 -5. **调整其他安全逻辑**:如果有自定义 `PermissionService`、`SecurityUtils` 等,也可考虑改用新 Port。否则只需确保系统角色/权限从端口获取。例如将某些静态调用改为注入 `PermissionPort` 使用。 -6. **更新配置或常量**:如果 `SecurityUser` 类中新加入了字段,应更新相关配置(如 Jackson 序列化时忽略密码)和常量。保证新旧字段兼容。 -7. **回归测试**:执行所有单元测试和集成测试,验证登录、权限校验、在线用户事件等功能正常。特别测试无系统用户调用失败情况。 -8. **代码审查、文档更新**:按照开发规范检查命名、注释完整性等(参见下文清单)。完善 Javadoc、异常处理后提交合并。 - -每步变更完成后,可用临时配置(如 Spring 配置文件)切换旧实现(如保留一个旧的 `UserDetailsService` Bean)与新实现进行对比测试,确保新代码无功能回归。如发现问题可回滚到上一步完成修复后再继续。 - -# 回归测试用例清单 - -- **用户认证**:正确用户名/密码登录成功;错误用户名或密码登录失败并抛 `UsernameNotFoundException` 或认证错误。 -- **用户禁用**:用户状态为禁用时,登录应被拒绝(`isEnabled()` 返回 `false`)。 -- **角色加载**:登录成功后,在 `SecurityUserDetails` 中可通过 `getAuthorities()` 获得正确的角色列表(前缀为 `ROLE_`)。 -- **权限校验**:使用带有 `@PreAuthorize` 或 `ss.hasPerm` 表达式的接口,用户具备权限时允许访问,不具备时禁止访问。 -- **微信登录(如适用)**:若支持小程序或其他社交登录,测试通过 `UserSocialService` 注入的相关功能是否正常(可暂按旧逻辑保留)。 -- **在线用户事件**:模拟登录成功后,验证系统模块已执行 `online(userId)` 操作(如在线表中插入记录、更新最后登录时间);登出后执行 `offline(userId)` 清理。 -- **边界情况**:极限测试用户角色集合为空、权限集合为空、端口实现抛异常等,确保安全模块正确处理并记录日志(不漏调用)。 -- **事务和并发**:在用户登录、权限检查时模拟高并发,确保端口实现线程安全(无共享可变状态)。 -- **版本兼容**:如有旧前端调用 API,确保接口行为未因内部重构发生变化。 - -# 代码审查清单 - -- **接口与实现匹配**:所有 Port 接口的实现类均以 `implements` 关键词明确标注,并使用 `@Component`(或注解使之纳入 Spring 容器)。接口方法签名与实现一致(参数、异常声明)。 -- **命名规范**:接口名以 Port 结尾,使用混合大小写词首大写;实现类名可以 "Adapter" 或 "Impl" 结尾。包名统一小写(参见开发规范)。例如 `UserAuthenticationPort` 对应 `UserAuthenticationAdapter`。 -- **注释完善**:所有公共接口、实现类、接口方法应有清晰的 Javadoc(中文)。描述业务功能、参数、返回及异常。举例:`UserAuthenticationPort.loadUserByUsername` 应说明抛出 `UsernameNotFoundException` 的情况。 -- **依赖注入**:使用构造器注入或 `@Autowired` 注入端口和系统服务;避免在业务代码中手动 `new` 实现类。优先使用 `final` 属性和构造函数注入保证必需依赖。 -- **日志与异常**:捕获系统服务可能抛出的异常时,应记录日志。使用 `log.error(...)` 并传递参数而非拼字符串(参见改前日志写法)。用户找不到时抛出 `UsernameNotFoundException`;权限不足时抛出 `AccessDeniedException` 或让 Spring Security 处理。 -- **安全性**:不要在日志中明文输出密码、密钥等敏感信息。对外接口返回的用户信息只包含必要字段(可设置 JSON 忽略字段)。遵循《阿里巴巴 Java 开发手册》对异常和日志的要求。 -- **测试覆盖**:对于新增 Port 接口和实现类,应编写单元测试;关键业务逻辑(如角色-权限映射)也要测试。请确认 CI 报告无遗漏。 -- **依赖关系正确**:检查修改后安全模块的 `pom.xml` 中对 system 模块的依赖是否为编译时(compile)且非传递;无多余依赖。逆向检查系统模块应添加对 security 模块的依赖,以访问接口定义(分包引用)。 -- **兼容性**:若需要在过渡期同时支持旧逻辑,确保原有 `UserService` 功能在适配器中仍被调用。避免出现切换时功能丢失。 -- **文档更新**:更新系统架构文档(如有)以反映新的接口和依赖关系。尤其对外部接入的配置点(如果有变)做说明。 - ---- - -**参考资料:** 以上方案参考了当前有来开源 `youlai-boot` 项目中 `framework/security` 与 `system` 模块的实现。具体关键代码位置如 `SecurityUserDetailsService`(参见)展示了现有耦合;AWS 官方文档对 Ports & Adapters 模式的介绍说明了设计思想。方案设计兼顾中文社区通用命名规范和企业级开发规范,并提供示例代码片段、迁移步骤及测试清单,以确保方案可执行性和安全稳定性。 diff --git a/docs/images/architecture/architecture.png b/docs/images/architecture/architecture.png deleted file mode 100644 index fbd6a09f..00000000 Binary files a/docs/images/architecture/architecture.png and /dev/null differ diff --git a/docs/images/logo/favicon.ico b/docs/images/logo/favicon.ico new file mode 100644 index 00000000..0a6744a4 Binary files /dev/null and b/docs/images/logo/favicon.ico differ diff --git a/docs/images/logo/logo.png b/docs/images/logo/logo.png index 5a0435f3..ebfa2af6 100644 Binary files a/docs/images/logo/logo.png and b/docs/images/logo/logo.png differ diff --git a/docs/images/qr/wechat-app.jpg b/docs/images/qrcode/wechat-app.jpg similarity index 100% rename from docs/images/qr/wechat-app.jpg rename to docs/images/qrcode/wechat-app.jpg diff --git a/docs/images/qrcode/wechat-mp.jpg b/docs/images/qrcode/wechat-mp.jpg new file mode 100644 index 00000000..34852141 Binary files /dev/null and b/docs/images/qrcode/wechat-mp.jpg differ diff --git a/docs/images/qr/wechat-offical.png b/docs/images/qrcode/wechat-official.png similarity index 97% rename from docs/images/qr/wechat-offical.png rename to docs/images/qrcode/wechat-official.png index 406447c8..54f1e17f 100644 Binary files a/docs/images/qr/wechat-offical.png and b/docs/images/qrcode/wechat-official.png differ diff --git a/docs/images/qr/wechat-personal.png b/docs/images/qrcode/wechat-personal.png similarity index 100% rename from docs/images/qr/wechat-personal.png rename to docs/images/qrcode/wechat-personal.png diff --git a/docs/security-refactor-plan.md b/docs/security-refactor-plan.md deleted file mode 100644 index e82031d9..00000000 --- a/docs/security-refactor-plan.md +++ /dev/null @@ -1,1953 +0,0 @@ -# Security 框架重构与 Starter 抽离方案 - -> **状态**:待实施 -> **范围**:`framework/security` 模块 -> **目标**:消除模型冗余 → 端口/适配器解耦 → 抽离为独立 Spring Boot Starter -> **适用**:youlai-boot(单模块)和 youlai-boot-multi(多模块) -> **最后更新**:2026-07-04 -> **合并自**:`deep-research-report.md`(Ports & Adapters 研究)+ 原 `security-refactor-plan.md`(模型消除教程) -> **实施策略**:不保留历史登录态;重构发布后所有旧 Token 失效,用户重新登录 - ---- - -## 目录 - -- [〇、文档使用指南(AI 协作必读)](#〇文档使用指南ai-协作必读) -- [一、问题诊断](#一问题诊断) -- [二、架构原则与设计决策](#二架构原则与设计决策) -- [三、方案设计](#三方案设计) -- [四、单模块重构(youlai-boot)](#四单模块重构youlai-boot) -- [五、多模块重构(youlai-boot-multi)](#五多模块重构youlai-boot-multi) -- [六、Security Starter 抽离与发布方案](#六security-starter-抽离与发布方案) -- [七、测试策略](#七测试策略) -- [八、实施阶段](#八实施阶段) -- [九、AI 协作指南](#九ai-协作指南) -- [十、代码审查清单](#十代码审查清单) -- [附录 A:术语表](#附录-a术语表) -- [附录 B:变更日志](#附录-b变更日志) - ---- - -## 〇、文档使用指南(AI 协作必读) - -> **本节专为 AI 协作设计。任何 AI 在修改 security 相关代码前,必须先阅读本节。** - -### 0.1 文档定位 - -本文档是 youlai-boot security 模块的**唯一权威重构方案**,合并了早期的架构研究(`deep-research-report.md`)和实施计划。所有 security 相关的架构变更、代码修改、命名约定均以本文档为准。 - -### 0.2 如何使用本文档 - -| 你的目标 | 阅读顺序 | -|----------|----------| -| **理解为什么要改** | 第一节 → 第二节 | -| **执行单模块重构** | 第三节 → 第四节 → 第八节(P0→P1→P2→P3)| -| **执行多模块重构** | 第三节 → 第五节 → 第八节(P0→P1→P2→P3) | -| **抽离为 Starter(实操)** | 先完成第四/五节 → 第六节 → 6.11(实操步骤) | -| **发布到 Maven Central** | 6.12(GPG + Sonatype + deploy) | -| **其他项目接入 Starter** | 6.13(引入依赖 + 实现适配器 + 验证) | -| **修改 security 代码** | 第九节(AI 协作指南)→ 第十节(审查清单)| -| **新增端口/适配器** | 第二节 2.3 → 第九节 9.3 | - -### 0.3 关键决策摘要(不可违背) - -| 编号 | 决策 | 原因 | -|------|------|------| -| D1 | 端口数量固定为 **2 个**:`UserAuthenticationPort`、`PermissionPort` | `RolePort` 冗余——roles 已包含在 `SecurityUser` 中;在线用户不纳入本次重构 | -| D2 | 将 `UserAuthInfo` **重命名为 `SecurityUser`** 作为端口返回模型 | `SecurityUser` 语义更中性,作为 Starter 公开 API 更规范;原 `UserAuthInfo` 偏向"认证信息"但实际承载用户安全数据;重命名而非新建,改动最小 | -| D3 | `SecurityUserDetails.authorities` 字段类型 `Collection` → `Set roles` | 消除序列化问题,`getAuthorities()` 改为实时计算 | -| D4 | **删除 `UserSession`**,发布后旧 Token 全部失效 | 其 5 个字段 100% 是 `SecurityUserDetails` 的子集,中间层无意义;不保留历史登录态 | -| D5 | 端口接口放在 `framework.security.port` 包,适配器放在 `system.security.adapter` 包 | 物理隔离端口定义与实现,符合六边形架构 | -| D6 | Starter 模块命名为 **`youlai-security-spring-boot-starter`** | 遵循 Spring Boot 官方 starter 命名规范 | -| D7 | `SocialPlatformEnum` 下沉到 `youlai-common` 后再定义端口 | 端口和 Starter 不允许出现任何 `com.youlai.boot.system.*` 引用 | -| D8 | Starter 不内置业务 JSON 响应写出逻辑,不依赖 `ResponseWriter` | 认证失败和鉴权失败走 Spring Security 标准异常与使用方 `AuthenticationEntryPoint` / `AccessDeniedHandler` | - -### 0.4 代码中的空占位文件说明 - -> ⚠️ **重要发现**:当前 `framework/security/service/` 下已存在 4 个空文件,表明端口模式**已被原开发者规划但未实施**: - -| 空文件 | 推测用途 | 重构后处理 | -|--------|----------|------------| -| `UserAuthQueryService.java` | 用户认证查询接口占位 | **删除**,由 `port/UserAuthenticationPort.java` 替代 | -| `RolePermissionService.java` | 角色权限查询接口占位 | **删除**,由 `port/PermissionPort.java` 替代 | -| `WxMaUserAuthQueryService.java` | 微信小程序认证查询占位 | **删除**,合并入 `UserAuthenticationPort.getAuthInfoByOpenid()` | -| `WxMaBindInfo.java` | 微信绑定信息模型占位 | **删除**,微信绑定流程留在使用方 Provider 内实现 | - ---- - -## 一、问题诊断 - -### 1.1 模型冗余(3 → 2) - -当前三个模型字段高度重叠(`SecurityUser` 为 `UserAuthInfo` 重命名后的名称,见 D2): - -| 字段 | `SecurityUser`
(原 `UserAuthInfo`) | `SecurityUserDetails` | `UserSession` | -|------|:---:|:---:|:---:| -| userId | ✓ | ✓ | ✓ | -| username | ✓ | ✓ | ✓ | -| deptId | ✓ | ✓ | ✓ | -| dataScopes | ✓ | ✓ | ✓ | -| roles | ✓ | — | ✓ | -| authorities | — | ✓ | — | -| password | ✓ | ✓ | — | -| enabled | — | ✓ | — | -| nickname | ✓ | — | — | -| status | ✓ | — | — | - -**数据流**:`SecurityUser → SecurityUserDetails → UserSession → SecurityUserDetails`,中间经历两次无意义字段拷贝。`UserSession` 的 5 个字段 100% 是 `SecurityUserDetails` 的子集,存在的唯一原因是 `SimpleGrantedAuthority` 不便序列化。 - -### 1.2 框架 ↔ 业务循环依赖 - -``` -framework.security ──→ system(SecurityUserDetailsService 直接注入 UserService、UserSocialService) -framework.security ──→ system(PermissionService 直接注入 RoleMenuService) -system ──→ framework.security(SecurityUtils、SecurityUser、SecurityUserDetails) -``` - -`SecurityUserDetailsService` 和 `PermissionService` 中大量 `import com.youlai.boot.system.*`,违反了依赖方向。在多模块项目中,这导致 `youlai-framework` 无法独立于 `youlai-system` 发布。 - -### 1.3 序列化问题(已临时修复) - -`JacksonJsonRedisSerializer` 反序列化时丢失类型信息,`UserSession` 被还原为 `LinkedHashMap`。 - -**当前修复方案**:提取 `JsonMapper` 为共享 Bean,使用 `convertValue` 显式转换。✅ 已生效,但 `UserSession` 中间层本身是不必要的。 - -### 1.4 现状 vs 目标对比 - -| 维度 | 当前实现(耦合严重) | 目标实现(Ports & Adapters) | -|------|---------------------|------------------------------| -| **依赖关系** | `framework/security` 直接 `import com.youlai.boot.system.*` | 安全模块仅引用自身端口接口,不持有 system 引用 | -| **用户认证** | `SecurityUserDetailsService` 注入 `UserService` 查询 | 注入 `UserAuthenticationPort`,由 system 侧适配器实现 | -| **权限校验** | `PermissionService` 注入 `RoleMenuService` | 注入 `PermissionPort`,由 system 侧适配器实现 | -| **角色获取** | `SecurityUserDetails` 构造时转换 `authorities` | `roles` 字段直接存储 `Set`,`getAuthorities()` 实时计算 | -| **在线用户** | 无专门接口 | 不纳入本次 Security Starter 重构 | -| **序列化** | `UserSession` 中间层 + `convertValue` 修补 | 直接存取 `SecurityUserDetails`(纯 JDK 类型) | -| **可测试性** | 安全代码测试需引入 system 服务 | 针对端口接口 Mock 测试,解耦 | -| **模块化** | 无法独立发布 security 模块 | 端口定义可随 Starter 独立发布 | - ---- - -## 二、架构原则与设计决策 - -### 2.1 Ports & Adapters 模式(精简) - -**六边形架构**核心思想:应用核心业务通过抽象的**端口(Port)**与外部通信,外部实现通过**适配器(Adapter)**连入端口。 - -- **端口(Port)**:定义安全模块需要的功能接口,放在 `framework.security.port` 包。命名以 `Port` 后缀结尾。 -- **适配器(Adapter)**:在 `system` 模块实现端口接口,内部调用具体 Service。标注 `@Component`。 -- **解耦效果**:安全模块编译期不持有任何 `com.youlai.boot.system.*` 引用。 - -### 2.2 端口定义决策 - -| 端口 | 方法 | 职责 | 是否核心 | -|------|------|------|:---:| -| `UserAuthenticationPort` | `getAuthInfoByUsername(String)`
`getAuthInfoByMobile(String)`
`getAuthInfoByOpenid(SocialPlatformEnum, String)` | 用户认证信息查询 | ✅ | -| `PermissionPort` | `getRolePerms(Set roleCodes)` | 角色权限集合查询 | ✅ | - -> **为什么不设 `RolePort`?** -> 角色编码(roles)已作为 `Set` 包含在 `SecurityUser` 中,登录时一次性获取,无需单独端口查询。`SecurityUtils.getRoles()` 直接从 `SecurityUserDetails.roles` 字段取值即可。 - -> **为什么不做在线用户端口?** -> 在线用户管理属于业务运营能力,不属于认证鉴权最小内核。本次重构不提供 `OnlineUserPort`,后续如需在线用户功能,由业务模块基于登录/登出事件自行实现,不进入 Security Starter。 - -### 2.3 命名规范 - -| 类型 | 命名规则 | 示例 | -|------|----------|------| -| 端口接口 | `<领域意图>Port` | `UserAuthenticationPort`、`PermissionPort` | -| 适配器实现 | `<领域意图>Adapter` | `UserAuthenticationAdapter`、`PermissionAdapter` | -| 端口包路径 | `framework.security.port` | — | -| 适配器包路径 | `system.security.adapter`(单模块)
`youlai-system/.../security/adapter`(多模块) | — | -| 端口返回模型 | `SecurityUser`(由 `UserAuthInfo` 重命名,纯 POJO,无 system 依赖) | — | - -> **禁止**:使用 `Service`/`Query` 后缀命名端口(如 `UserAuthQueryService`),这与业务 Service 混淆。当前代码中的空文件 `UserAuthQueryService.java`、`RolePermissionService.java` 就是错误命名的占位,应删除并用 `Port` 后缀替代。 - ---- - -## 三、方案设计 - -### 3.1 消除 UserSession - -**核心改动**:`SecurityUserDetails` 将 `Collection authorities` 替换为 `Set roles`,`getAuthorities()` 改为实时计算。 - -```java -@Data -@NoArgsConstructor -public class SecurityUserDetails implements UserDetails { - - private Long userId; - private String username; - private String password; - private Boolean enabled; - private Long deptId; - private List dataScopes; - private Set roles; // ← 替换 authorities - - 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.roles = user.getRoles(); - } - - @Override - public Collection getAuthorities() { - if (CollectionUtil.isEmpty(roles)) { - return Collections.emptySet(); - } - return roles.stream() - .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) - .collect(Collectors.toSet()); - } - - // 其他 UserDetails 方法不变... -} -``` - -**效果**:所有字段均为纯 JDK 类型,`SecurityUserDetails` 可直接序列化存储,`UserSession` 文件删除。 - -### 3.2 端口/适配器解耦 - -``` -┌─────────────────────────────────┐ ┌──────────────────────────────────┐ -│ framework/security │ │ system │ -│ ┌───────────────────────────┐ │ │ ┌────────────────────────────┐ │ -│ │ port/ │ │ │ │ security/adapter/ │ │ -│ │ ├ UserAuthenticationPort │◄─┼─────┼──│ ├ UserAuthenticationAdapter│ │ -│ │ └ PermissionPort │◄─┼─────┼──│ └ PermissionAdapter │ │ -│ └───────────────────────────┘ │ │ └────────────────────────────┘ │ -│ ┌───────────────────────────┐ │ │ ┌────────────────────────────┐ │ -│ │ service/ │ │ │ │ service/ │ │ -│ │ ├ SecurityUserDetailsService │──┼─────┼──│ (注入 Port,不注入 system)│ │ -│ │ └ PermissionService │──┼─────┼──│ │ │ -│ └───────────────────────────┘ │ │ └────────────────────────────┘ │ -└─────────────────────────────────┘ └──────────────────────────────────┘ - ↑ 编译期零 system 依赖 ↑ 运行期注入 Port 实现 -``` - -**端口定义**(framework 层): - -| 端口 | 方法 | 职责 | -|------|------|------| -| `UserAuthenticationPort` | `getAuthInfoByUsername(String)`
`getAuthInfoByMobile(String)`
`getAuthInfoByOpenid(platform, openid)` | 用户认证信息查询 | -| `PermissionPort` | `getRolePerms(Set roleCodes)` | 角色权限集合查询 | - -**适配器实现**(system 层): - -| 适配器 | 实现端口 | 委托目标 | -|--------|----------|----------| -| `UserAuthenticationAdapter` | `UserAuthenticationPort` | `UserService` + `UserSocialService` | -| `PermissionAdapter` | `PermissionPort` | `RoleMenuService` | - -### 3.3 SecurityUtils 简化 - -```java -// 之前:5 步链式(getAuthorities → filter ROLE_ → strip) -// 之后:直接取 roles 字段 -public static Set getRoles() { - return getUser().map(SecurityUserDetails::getRoles).orElse(Set.of()); -} -``` - -### 3.4 Token 管理器适配 - -- **RedisTokenManager**:直接存储 `SecurityUserDetails`(password 置 null),删除 `UserSession` 中间层和 `buildUserDetails` 重建逻辑 -- **JwtTokenManager**:JWT claims 使用 `roles` 字段保存不带 `ROLE_` 前缀的角色编码;`parseToken` 只解析 `roles`,`getAuthorities()` 实时补 `ROLE_` 前缀 -- **旧 Token 策略**:不接受历史 JWT / Redis Token。发布时清理 Redis 中 `auth:*` 登录态 key,并轮换 `security.session.jwt.secret-key`,所有用户重新登录 - ---- - -## 四、单模块重构(youlai-boot) - -### 4.1 端口接口定义 - -先将 `SocialPlatformEnum` 下沉到 `youlai-common`,再新建包 `framework.security.port`: - -``` -framework/security/ -├── port/ -│ ├── UserAuthenticationPort.java # 新增 -│ └── PermissionPort.java # 新增 -├── model/ -│ ├── SecurityUserDetails.java # 改造 -│ ├── SecurityUser.java # 重命名(原 UserAuthInfo) -│ └── UserSession.java # 删除 -├── service/ -│ ├── SecurityUserDetailsService.java # 改造(注入 Port) -│ ├── PermissionService.java # 改造(注入 Port) -│ ├── UserAuthQueryService.java # 删除(空占位) -│ ├── RolePermissionService.java # 删除(空占位) -│ ├── WxMaUserAuthQueryService.java # 删除(空占位) -│ └── WxMaBindInfo.java # 删除(空占位,如在 model 包则一并清理) -└── ... -``` - -```java -// framework/security/port/UserAuthenticationPort.java -package com.youlai.boot.framework.security.port; - -import com.youlai.boot.framework.security.model.SecurityUser; -import com.youlai.boot.common.enums.SocialPlatformEnum; - -/** - * 用户认证信息查询端口。 - *

- * 由 system 模块提供适配器实现,framework 层通过此接口获取认证数据。 - * 框架层不直接依赖 system 模块的 Service。 - * - * @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); -} -``` - -```java -// framework/security/port/PermissionPort.java -package com.youlai.boot.framework.security.port; - -import java.util.Set; - -/** - * 权限查询端口。 - *

- * 由 system 模块提供适配器实现,framework 层通过此接口获取角色权限集合。 - * - * @see com.youlai.boot.system.security.adapter.PermissionAdapter - */ -public interface PermissionPort { - - /** - * 根据角色编码集合查询权限标识集合。 - * - * @param roleCodes 角色编码集合 - * @return 权限标识集合,如 "sys:user:create" - */ - Set getRolePerms(Set roleCodes); -} -``` - -> **硬性要求**:`UserAuthenticationPort` 必须引用 `com.youlai.boot.common.enums.SocialPlatformEnum`。端口接口中禁止出现任何 `com.youlai.boot.system.*` 引用。 - -### 4.2 SecurityUserDetails 改造 - -```java -// framework/security/model/SecurityUserDetails.java -// 改动点: -// 1. authorities 字段类型 Collection → Set roles -// 2. 构造函数直接赋值 roles -// 3. getAuthorities() 改为实时计算 - -@Data -@NoArgsConstructor -public class SecurityUserDetails implements UserDetails { - - private Long userId; - private String username; - private String password; - private Boolean enabled; - private Long deptId; - private List dataScopes; - private Set roles; // ← 替换 authorities - - 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.roles = user.getRoles(); // ← 直接赋值,无需转换 - } - - @Override - public Collection getAuthorities() { - if (CollectionUtil.isEmpty(roles)) { - return Collections.emptySet(); - } - return roles.stream() - .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) - .collect(Collectors.toSet()); - } -} -``` - -### 4.3 适配器实现 - -在 system 模块新建 `system.security.adapter` 包: - -```java -// system/security/adapter/UserAuthenticationAdapter.java -package com.youlai.boot.system.security.adapter; - -import com.youlai.boot.framework.security.model.SecurityUser; -import com.youlai.boot.framework.security.port.UserAuthenticationPort; -import com.youlai.boot.common.enums.SocialPlatformEnum; -import com.youlai.boot.system.service.UserService; -import com.youlai.boot.system.service.UserSocialService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -/** - * 用户认证信息查询适配器。 - *

- * 实现 framework 层的 UserAuthenticationPort,委托 system 层服务完成查询。 - * 此类是端口与系统服务的唯一耦合点。 - */ -@Component -@RequiredArgsConstructor -public class UserAuthenticationAdapter implements UserAuthenticationPort { - - private final UserService userService; - private final UserSocialService userSocialService; - - @Override - public SecurityUser getAuthInfoByUsername(String username) { - return userService.getAuthInfoByUsername(username); - } - - @Override - public SecurityUser getAuthInfoByMobile(String mobile) { - return userService.getAuthInfoByMobile(mobile); - } - - @Override - public SecurityUser getAuthInfoByOpenid(SocialPlatformEnum platform, String openid) { - return userSocialService.getAuthInfoByOpenid(platform, openid); - } -} -``` - -```java -// system/security/adapter/PermissionAdapter.java -package com.youlai.boot.system.security.adapter; - -import com.youlai.boot.framework.security.port.PermissionPort; -import com.youlai.boot.system.service.RoleMenuService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import java.util.Set; - -/** - * 权限查询适配器。 - *

- * 实现 framework 层的 PermissionPort,委托 RoleMenuService 查询权限。 - */ -@Component -@RequiredArgsConstructor -public class PermissionAdapter implements PermissionPort { - - private final RoleMenuService roleMenuService; - - @Override - public Set getRolePerms(Set roleCodes) { - return roleMenuService.getRolePermsByRoleCodes(roleCodes); - } -} -``` - -### 4.4 服务层改造 - -```java -// framework/security/service/SecurityUserDetailsService.java(改造后) -package com.youlai.boot.framework.security.service; - -import com.youlai.boot.framework.security.model.SecurityUserDetails; -import com.youlai.boot.framework.security.model.SecurityUser; -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。 - *

- * 通过 UserAuthenticationPort 获取认证信息,不直接依赖 system 模块。 - */ -@Service -@RequiredArgsConstructor -@Slf4j -public class SecurityUserDetailsService implements UserDetailsService { - - private final UserAuthenticationPort userAuthPort; // ← 替换 UserService - - @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; - } - } -} -``` - -```java -// framework/security/service/PermissionService.java(改造后) -package com.youlai.boot.framework.security.service; - -import com.youlai.boot.framework.security.port.PermissionPort; -import com.youlai.boot.framework.security.util.SecurityUtils; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.util.PatternMatchUtils; -import java.util.Set; - -@Component("ss") -@RequiredArgsConstructor -@Slf4j -public class PermissionService { - - private final PermissionPort permissionPort; // ← 替换 RoleMenuService - - public boolean hasPerm(String requiredPerm) { - // ... 前置校验不变(超管放行等)... - Set roleCodes = SecurityUtils.getRoles(); - Set rolePerms = permissionPort.getRolePerms(roleCodes); // ← 通过 Port 调用 - // ... 后续逻辑不变(通配符匹配等)... - } -} -``` - -### 4.5 Token 管理器适配 - -**RedisTokenManager** 改动: - -```java -// 之前:UserSession session = new UserSession(user); -// redisTemplate.opsForValue().set(key, session); -// 之后:直接存 SecurityUserDetails(password 置 null) -SecurityUserDetails sessionUser = new SecurityUserDetails(); -sessionUser.setUserId(user.getUserId()); -sessionUser.setUsername(user.getUsername()); -sessionUser.setDeptId(user.getDeptId()); -sessionUser.setDataScopes(user.getDataScopes()); -sessionUser.setRoles(user.getRoles()); -sessionUser.setEnabled(user.isEnabled()); -sessionUser.setPassword(null); // 不存密码 -redisTemplate.opsForValue().set(key, sessionUser); -``` - -```java -// 之前:UserSession raw = jsonMapper.convertValue(obj, UserSession.class); -// return buildUserDetails(raw); -// 之后:直接还原 -SecurityUserDetails raw = jsonMapper.convertValue(obj, SecurityUserDetails.class); -return raw; -``` - -**JwtTokenManager** 改动: - -```java -// parseToken 中: -// 之前:从 claims 取 authorities,构造 Set -// 之后:从 claims 取 roles(Set),直接 setRoles() -Set 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 自动装配规范设计。所有代码示例中的包名、类名、方法签名均与实际代码库对齐。

- 公众号「有来技术」
+ 公众号「有来技术」
公众号「有来技术」
     - 小程序「有来技术」
+ 小程序「有来技术」
小程序「有来技术」
     - 添加作者微信
+ 添加作者微信
添加作者微信