面向实际项目复用的 Spring Boot 3 多模块基础工程,统一提供认证鉴权、数据访问、缓存、HTTP 客户端、异常协议、链路追踪、操作审计、邮件通知和工程质量约束。
本项目不是一个包含演示业务的脚手架,而是一套可以继续扩展领域模块的后端基础架构。使用时应保留模块边界、安全不变量和数据库迁移规范,不建议把业务代码继续堆放到 common 模块。
- JDK 17
- Spring Boot 3.5.3
- Maven 3.8.6 及以上,仓库已提供 Maven Wrapper
- MySQL 8
- Redis 6 及以上
- MyBatis-Plus 3.5.12
- Sa-Token
- Flyway
- Redisson
- Forest 1.8.0
- Testcontainers
构建阶段通过 Maven Enforcer 校验 Java、Maven 版本及依赖收敛情况,不符合要求时会直接终止构建。
base-springboot
├── common
│ ├── common-core 纯 Java 公共契约、错误码和基础工具
│ ├── common-cache Redis、Redisson 和缓存原子操作
│ ├── common-http-client Forest HTTP 客户端、TraceId 透传和安全日志策略
│ ├── common-security 登录主体、Sa-Token、密码与会话安全
│ ├── common-web Servlet、统一响应、JSON 和 Web 拦截器
│ ├── common-ip 可信代理客户端 IP 与归属地解析
│ ├── common-mail 邮件发送和邮件配置
│ ├── common-data MyBatis-Plus 和审计字段填充
│ ├── common-audit 审计注解、事件和切面
│ ├── common-rbac 统一 RBAC 数据模型、权限域规则和授权实现
│ └── common-framework 可执行应用的基础设施装配入口、跨模块协调器和异步执行器
├── database-migrations admin/business 共库使用的统一 Flyway 迁移制品
├── business 业务端应用、用户认证、业务授权消费和支付宝 OAuth 登录
├── admin 管理端应用、业务用户管理和统一权限配置入口
├── architecture-tests 共享 Schema、bootstrap 与部署边界集成测试
└── db 数据库迁移规范说明
模块依赖方向固定为:
architecture-tests -> admin / business -> common-rbac / common-framework -> 各职责模块 -> common-core
必须遵守以下边界:
common-core不依赖 Spring Web、MyBatis、Redis、邮件和第三方平台 SDK。common-framework是可执行应用的装配层,只承载跨模块基础设施协调,不承载用户、订单、支付等领域规则。admin、business等独立可执行应用可以依赖common-framework;普通领域模块应按需依赖各职责模块,禁止把common-framework当作默认基础依赖。common-security定义认证、会话和授权提供器抽象,不依赖具体 RBAC 表。common-rbac实现授权提供器并维护统一角色、权限和菜单规则,不承载管理端接口。- 支付宝等业务专属 SDK 只能由使用它的业务模块声明。
business只消费 business 权限域;角色、权限和菜单管理入口统一放在admin。admin与business可以独立运行和发布,但必须连接同一个 MySQL Schema。- 数据库迁移只能放入
database-migrations,保证任一应用先启动都能得到完整表结构。 architecture-tests只参与测试,禁止被任何生产模块反向依赖。- 新业务优先建立独立模块;只有多个应用都需要且与领域无关的能力才允许下沉到
common。
新增模块应只声明代码直接使用的公共能力。不要依赖传递依赖“碰巧”提供某个类型,也不要让普通领域模块依赖完整装配层。
| 使用场景 | 建议直接依赖 |
|---|---|
| 公共契约、错误码、分页模型和纯 Java 工具 | common-core |
| MyBatis-Plus、分页和审计字段填充 | common-data |
| Redis、Redisson 和缓存原子操作 | common-cache |
| 调用第三方 HTTP API、TraceId 透传和 Forest 基础配置 | common-http-client |
| Servlet、统一响应、异常处理和 Web 拦截器 | common-web |
| 登录主体、密码、Sa-Token 和会话安全 | common-security |
| 统一角色、权限、菜单和授权实现 | common-rbac |
| 邮件发送和邮件配置 | common-mail |
| 操作审计注解、事件和切面 | common-audit |
| 可信代理客户端 IP 和归属地解析 | common-ip |
| 独立可执行应用的完整基础设施装配 | common-framework |
例如,订单领域模块通常直接依赖 common-core 和 common-data;需要缓存时再增加 common-cache。只有具备独立启动入口、Profile、端口和部署单元的应用模块,才考虑依赖 common-framework。
创建一个由管理端和业务端共用的空数据库,例如:
CREATE DATABASE base_app CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;business 和 admin 的数据源 URL 必须指向这个相同 Schema。两个应用依赖同一个
database-migrations 制品,并共用 flyway_schema_history;无论哪个应用先启动,Flyway
都会创建业务用户、管理员、统一 RBAC 和审计日志所需的完整结构。两个应用并发启动时由
Flyway 数据库锁保证迁移只执行一次。
Redis 默认同时承担业务缓存和 Sa-Token Session 存储。两类数据应使用不同 Redis database 或独立实例,具体连接信息通过本地环境变量或不入库的本地配置提供。
仓库内的 application-*.yml 只能保存非敏感默认值和环境变量占位符。真实数据库、Redis、邮件和第三方平台凭据不得写入被 Git 跟踪的配置文件。开发者应通过 IDE Run Configuration、Shell 环境变量,或明确加入 .gitignore 的本地 Profile 文件提供配置。
至少需要确认:
- MySQL URL、用户名和密码
- Redis 地址、端口、database 和密码
- Sa-Token 独立 Redis 配置
- 邮件服务器、发件人和模板
- 支付宝应用配置,仅
business需要 - IP 可信代理列表及是否启用归属地检查
当前仓库默认启用 dev Profile。启动前必须确认该 Profile 的连接目标;生产环境必须显式设置:
export SPRING_PROFILES_ACTIVE=prod
export CORS_ALLOWED_ORIGIN_PATTERN=https://your-frontend.example.com生产配置不得直接沿用开发环境的数据库、Redis、邮件密钥和第三方平台私钥。任何曾经提交到 Git 的凭据都应视为已经泄露并立即轮换,删除当前文件中的明文并不能消除 Git 历史中的泄露记录。
建议按以下环境职责维护配置:
| 环境 | Profile | 基础设施 | 外部功能 | 主要用途 |
|---|---|---|---|---|
| 单元测试 | 无固定 Profile | Mock 或纯内存对象 | 关闭 | 纯逻辑验证 |
| 集成测试 | integration |
Testcontainers MySQL/Redis | 使用替身或关闭 | CI 和真实基础设施验证 |
| 本地开发 | dev 或自定义 local |
本机容器或明确授权的开发环境 | 默认关闭、按需开启 | 日常开发调试 |
| 生产部署 | prod |
必须通过环境变量或密钥系统注入 | 按部署需要显式开启 | 正式运行 |
| 管理员初始化 | bootstrap |
仅连接目标 MySQL | Web、Redis、邮件和 Sa-Token 均关闭 | 一次性创建首个管理员 |
基础框架后续宜取消默认 Profile,并提供不含任何真实凭据的本地配置示例,使配置缺失时启动失败,而不是静默连接某个共享环境。
启动用户端,默认端口 6059:
./mvnw -pl business -am spring-boot:run启动管理端,默认端口 6060:
./mvnw -pl admin -am spring-boot:run首次启动时 Flyway 会自动创建表、索引、基础角色、权限和菜单数据,不需要手工执行全量 SQL。
管理端提供一次性 bootstrap Profile。它只允许在管理员表为空时执行,并会将首个管理员绑定到内置超级管理员角色。
export SPRING_PROFILES_ACTIVE=bootstrap
export BOOTSTRAP_DB_URL='jdbc:mysql://127.0.0.1:3306/base_app?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai'
export BOOTSTRAP_DB_USERNAME='root'
export BOOTSTRAP_DB_PASSWORD='your-password'
export BOOTSTRAP_ADMIN_LOGIN_CODE='root-admin'
export BOOTSTRAP_ADMIN_LOGIN_NAME='系统管理员'
export BOOTSTRAP_ADMIN_EMAIL='admin@example.com'
export BOOTSTRAP_ADMIN_PASSWORD='Framework2026'
./mvnw -pl admin -am spring-boot:run初始化完成后进程会自动退出。临时密码必须符合当前 security.password-policy,并应在首次登录后立即修改。
bootstrap 具有以下安全限制:
- 管理员表已有任何数据时拒绝运行。
- 超级管理员角色必须已经由 Flyway 初始化。
- 账号、角色关联在同一事务中创建。
- 不启动 Web、Redis、Sa-Token 等常规运行时基础设施。
用户端登录示例:
curl -X POST 'http://localhost:6059/auth/login' \
-H 'Content-Type: application/json' \
-d '{"loginCode":"your-account","password":"your-password"}'统一响应示例:
{
"code": 200,
"message": "操作成功",
"data": {
"token": "..."
},
"traceId": "..."
}携带 Token 访问受保护接口:
curl 'http://localhost:6059/user/info' \
-H 'Authorization: Bearer your-token'用户端和管理端属于不同 Sa-Token 安全域,用户 Token 不能访问管理端接口,管理端 Token 也不能作为用户 Token 使用。
以下流程以用户端 business 为例,默认地址为 http://localhost:6059。管理端没有开放注册入口,管理员登录和找回密码分别使用管理端同名接口,并运行在独立的 admin 安全域。
前端对接前应先统一以下规则:
- 所有请求和响应使用 UTF-8 JSON。
- 不能只根据响应体中的
code判断网络层状态,必须先处理 HTTP 状态码。 - HTTP 2xx 且响应体
code=200才表示业务成功。 - 受保护接口通过
Authorization: Bearer <token>传递登录凭证。 - 每个响应头和响应体中都有可用于排障的
Trace-Id/traceId,前端错误日志应一并记录。 - HTTP 401 表示 Token 缺失、失效或账号在其他位置重新登录,前端应清理本地登录状态并跳转登录页。
- HTTP 403 表示账号、身份状态或权限不允许,不能一律当作“未登录”处理。
- HTTP 409 表示账号、邮箱等唯一数据冲突。
- HTTP 429 表示发送频率、登录失败次数或每日次数达到限制,前端应停止自动重试。
- HTTP 5xx 表示服务端异常,前端可以提示稍后重试,但禁止无限循环请求。
- 按钮提交期间应进入 loading 状态并禁止重复点击;网络超时后是否重试,应根据接口是否幂等分别处理。
失败响应示例:
{
"code": 2010,
"message": "账号或密码错误",
"data": null,
"traceId": "7a5f9c0c2f2245ef83a337dcd54e7ce4"
}前端建议建立统一 HTTP 拦截器:
api.interceptors.request.use((config) => {
const token = authStore.token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
api.interceptors.response.use(
(response) => {
const body = response.data;
if (body.code !== 200) {
return Promise.reject(new BusinessError(body.code, body.message, body.traceId));
}
return body.data;
},
(error) => {
if (error.response?.status === 401) {
authStore.clear();
router.replace({name: "login", query: {redirect: router.currentRoute.value.fullPath}});
}
return Promise.reject(error);
}
);以上代码是交互示意。实际项目应在 HTTP 错误分支中继续解析后端统一错误响应,否则会丢失业务码和 TraceId。
注册分为“发送邮箱验证码”和“提交注册”两步。注册成功后不会自动登录,前端应跳转登录页或主动调用登录接口。
注册页面
│
├─ 1. POST /user/send-captcha
│ │
│ ├─ 校验邮箱格式和是否已注册
│ ├─ Redis 原子占用发送窗口
│ ├─ 检查邮箱当日发送次数
│ ├─ 发送六位验证码邮件
│ └─ Redis 保存验证码和有效期
│
└─ 2. POST /user/register
│
├─ Redis 原子校验并预占验证码
├─ 规范化账号、邮箱、昵称和大陆手机号
├─ 校验账号和邮箱唯一性
├─ 创建 User 统一主体
├─ 创建 password 类型 UserIdentity
├─ 分配 business:member 默认角色
├─ 提交数据库事务
└─ 事务提交后删除验证码;回滚时恢复验证码
POST /user/send-captcha
Content-Type: application/json
{
"email": "user@example.com"
}成功响应:
{
"code": 200,
"message": "成功",
"data": null,
"traceId": "..."
}当前开发配置下,验证码有效期为 5 分钟,同一邮箱每天最多发送 3 次;具体值由 mail.register-time 和 mail.register-max 控制。
前端交互要求:
- 邮箱通过前端基础格式校验后才能启用“发送验证码”按钮,但后端校验仍是最终边界。
- 请求成功后启动与
mail.register-time一致的倒计时,并临时禁用重复发送。 - 倒计时只用于用户体验,刷新页面后是否允许重发仍以后端 Redis 状态为准。
- 收到业务码
8001时提示用户检查收件箱和垃圾邮件,不要自动循环发送。 - 收到
8005时提示邮箱已注册,并提供跳转登录或找回密码入口。 - 收到 HTTP 429/业务码
2009时停止当天重试。 - 网络超时不代表邮件一定未发送;前端不能在超时后立即高频重试。
POST /user/register
Content-Type: application/json
{
"loginCode": "demo-user",
"loginName": "示例用户",
"password": "Framework2026",
"linkPhone": "13800138000",
"email": "user@example.com",
"captcha": "123456"
}字段约束:
| 字段 | 约束 |
|---|---|
loginCode |
必填,4~50 位;服务端会统一去除首尾空格并规范化 |
loginName |
必填,2~50 位 |
password |
必填,满足 security.password-policy |
linkPhone |
可选;非空时必须为中国大陆手机号 |
email |
必填,合法邮箱,最长 100 位 |
captcha |
必填,必须与该邮箱当前有效验证码一致 |
前端交互要求:
- 注册按钮提交期间必须禁用,避免相同表单被重复发送。
- 不要在日志、埋点、URL 查询参数或错误上报中记录密码和验证码。
- 业务码
8002表示验证码不存在或已过期,应引导用户重新发送。 - 业务码
2007表示验证码不匹配,应保留其他表单字段让用户重新输入。 - HTTP 409/业务码
2003或8005表示账号或邮箱已被占用,应定位到对应输入框提示。 - 注册事务失败时验证码会恢复,用户可以再次提交;注册成功后验证码立即失效。
- 注册成功后清除密码和验证码字段,再跳转登录页。不要默认认为注册响应中包含 Token。
登录页面
│
└─ POST /auth/login
│
├─ 规范化登录账号并解析可信客户端 IP
├─ Redis 按账号和 IP 原子预占登录尝试
├─ 查询 password 类型 UserIdentity
├─ BCrypt 校验明文密码与数据库摘要
├─ 校验身份 verified/status 和 User status
├─ 加载角色、权限快照
├─ 构造 LoginPrincipal
├─ Sa-Token 建立账号 Session 并写入 Redis
├─ 返回 Token
└─ 异步更新设备、IP 归属地并检查异常登录
请求:
POST /auth/login
Content-Type: application/json
{
"loginCode": "demo-user",
"password": "Framework2026"
}成功响应:
{
"code": 200,
"message": "成功",
"data": {
"token": "0f85a061-4a8b-4b16-bb6d-47d667cb96f7"
},
"traceId": "..."
}前端收到 Token 后:
- 保存当前登录态。
- 后续请求增加
Authorization: Bearer <token>。 - 调用
GET /user/info获取当前主体公开资料、角色和后端权限。 - 调用
GET /user/menus获取当前角色可见菜单树。 - 根据后端权限控制按钮展示;真正的安全校验仍必须由后端完成。
- 登录成功后跳转到登录前保存的
redirect,没有 redirect 时进入默认首页。
Token 存储建议:
- 当前后端契约是“响应 JSON 返回 Token,并通过 Authorization Header 传递”,前端不能假设服务端写入了 HttpOnly Cookie。
- 浏览器端优先保存在应用内存状态;如果必须跨刷新保存,应充分评估 localStorage/sessionStorage 的 XSS 风险并落实 CSP、依赖治理和输出转义。
- 如果项目要求 HttpOnly、Secure、SameSite Cookie,需要前后端一起调整登录、跨域、CSRF 和注销设计,不能只由前端自行改存储方式。
- 禁止把 Token 放入 URL、页面埋点、前端异常日志或第三方统计参数。
登录错误处理:
| HTTP/业务码 | 前端行为 |
|---|---|
401 / 2010 |
统一提示账号或密码错误,不区分账号是否存在 |
429 / 2012 |
提示尝试次数过多并停止自动提交,等待统计窗口结束 |
403 / 2011 |
提示账号已停用,并提供联系管理员入口 |
403 / 2013 |
提示当前登录身份已停用 |
403 / 2014 |
提示当前登录身份尚未验证 |
400 / 1001 |
根据 message 提示参数格式问题 |
当前配置 sa-token.is-concurrent=false,同一账号新登录会使旧 Token 失效。因此其他设备收到 401 时,应退出旧会话并提示“账号可能已在其他设备重新登录”,不要在后台无限刷新 Token。
Token 采用服务端 Session 和七天滑动过期:登录时 Redis TTL 为七天,每次通过认证拦截器
的有效请求都会调用 Sa-Token renewTimeout,把 Token、Token-Session 和 Account-Session
剩余时间重新续签到七天。连续七天完全没有访问时 Redis 自动清理会话;续期不会生成新
Token,前端继续使用原 UUID。相关配置:
security:
session:
sliding-expiration:
enabled: true
timeout: 7d
invalidation:
retry-enabled: true
retry-delays: 1s,5s,30s
sa-token:
timeout: 604800
active-timeout: -1
auto-renew: false密码、账号状态、注销、角色和权限等安全数据发生变化时,系统不会等待七天自然过期,
而是在数据库事务成功提交后立即按照内部用户 ID 注销该账号的全部 Token。首次 Redis
注销失败不会把已经提交成功的业务接口错误地返回为失败,而是由独立调度线程池依次在
1 秒、5 秒和 30 秒后有限重试。重试耗尽后会输出包含安全域、主体 ID、失效原因和异常
类型的 ERROR 日志,并发布 SessionInvalidationExhaustedEvent,具体项目可以监听该事件
接入 Micrometer、Prometheus 或外部告警平台。
重试任务只保存在当前 JVM 内存中,不提供 MQ 或数据库 Outbox 级别的持久化保证。这是 通用基础框架对复杂度的明确控制;金融级或统一认证中心项目可以监听失败事件并替换为 持久化补偿实现。重新启用账号时同样会清理历史 Session,避免停用阶段残留的旧 Token 随账号恢复而重新获得访问能力。
管理端密码登录使用管理端地址的 POST /auth/login,请求字段相同,但 Token 属于独立 admin 安全域。前端如果同时提供用户端和管理端页面,必须使用不同的状态容器和 API Client,不能共用 Token。
支付宝小程序登录接口:
POST /ali-auth/login
Content-Type: application/json
{
"authCode": "支付宝客户端取得的一次性授权码"
}前端只能提交支付宝返回的一次性 authCode,不得提交或伪造支付宝 userId。后端负责使用可信应用凭据换取平台身份、绑定统一用户、加载 RBAC 快照并返回与密码登录相同结构的系统 Token。
找回密码分为“请求邮件”和“通过邮件链接提交新密码”两步。接口会故意隐藏邮箱是否已注册,防止被用于枚举系统用户。
忘记密码页面
│
├─ 1. POST /user/reset-password-email
│ │
│ ├─ 规范化邮箱
│ ├─ 查询邮箱对应的 User
│ ├─ 不存在时仍返回成功,隐藏账号存在性
│ ├─ Redis 原子占用发送窗口并检查每日次数
│ ├─ 创建包含 realm、subjectId、email 的临时 Token
│ ├─ 邮件发送前生成前端重置页面 URL
│ └─ 发送成功后保存 Token 窗口状态
│
└─ 用户点击邮件链接:reset-password.base-path?token=...
│
├─ 前端从 URL 读取 Token,展示新密码表单
└─ 2. POST /user/reset-password
│
├─ Redis 按 Token 摘要原子取得唯一消费权
├─ 解码并校验 user 安全域、subjectId 和邮箱
├─ BCrypt 编码并更新 password 身份凭据
├─ 提交数据库事务
├─ 标记 Token 已消费并删除原临时 Token
└─ 注销该用户全部旧会话
POST /user/reset-password-email
Content-Type: application/json
{
"email": "user@example.com"
}无论邮箱是否存在,正常情况下都返回统一成功响应。前端文案应使用:
如果该邮箱已注册,系统会发送重置密码邮件,请检查收件箱和垃圾邮件。
不要显示“邮箱不存在”,也不要通过响应耗时、自动跳转或不同倒计时泄露邮箱是否注册。
当前开发配置下,同一邮箱每天最多请求 3 次,重置链接有效期为 15 分钟,分别由 reset-password.max-number 和 reset-password.reset-pwd-time 控制。
前端交互要求:
- 请求成功后展示统一提示,不自动进入新密码输入页。
- 收到
8001时提示邮件已发送过,不重复发送。 - 收到 HTTP 429/业务码
3000时提示今日次数已达上限。 - 邮件发送失败
8000可以提示稍后重试,并记录 TraceId 供排查。
后端邮件中的链接由以下配置生成:
reset-password:
base-path: https://frontend.example.com/reset-password最终邮件链接类似:
https://frontend.example.com/reset-password?token=temporary-token
因此 reset-password.base-path 必须配置为前端重置密码页面,而不是后端 API 地址。
重置页面加载时:
- 从当前 URL 查询参数读取
token。 - Token 缺失时直接提示链接无效,不发送空请求。
- 不需要也不应该在页面加载时把 Token 发送给第三方统计、客服或埋点 SDK。
- 不要把 Token 长期写入 localStorage;保留在当前页面内存即可。
- 用户提交新密码时,将 Token 和新密码一起发送给后端。
POST /user/reset-password
Content-Type: application/json
{
"token": "temporary-token",
"newPassword": "NewFramework2026"
}前端交互要求:
- 新密码必须按当前
security.password-policy提供即时提示,但以后端校验结果为准。 - 提交期间禁用按钮。一个 Token 只允许成功消费一次,并发提交只会有一个成功。
- 业务码
3001表示链接过期、已消费、跨安全域或载荷不一致,应引导用户重新发起找回密码。 - 重置成功后立即从地址栏移除 Token,可以使用路由替换而不是保留当前历史记录。
- 重置成功会注销该用户所有旧 Token;前端应清理任何已有登录态并跳转登录页。
- 不要在重置成功后自动使用新密码登录,除非产品明确要求并重新评估安全策略。
管理端找回密码使用管理端地址下相同的:
POST /user/reset-password-email
POST /user/reset-password
管理端邮件 Token 包含 admin 安全域,不能提交到用户端接口。用户端与管理端前端应分别配置自己的重置页面地址和 API Base URL。
已登录用户修改密码使用:
POST /user/change-password
Authorization: Bearer <token>
Content-Type: application/json
{
"password": "CurrentPassword",
"newPassword": "NewFramework2026"
}- 修改密码必须携带当前有效会话并验证旧密码。
- 找回密码不要求登录,但必须持有邮件中的一次性 Token。
- 两种方式成功后都会注销该主体全部旧会话。
- 前端成功后都应清理本地 Token 并跳转登录页。
完整验证:
./mvnw clean verify该命令会执行单元测试和 Testcontainers 集成测试,覆盖:
- MySQL、Redis 和完整 Spring 上下文启动
- business/admin 从空库执行统一 Flyway 基线并验证重复迁移幂等
- Mapper XML 实际执行
- Redis Lua 原子行为
- Sa-Token Session 实际读写
- 注册、登录、限流、验证码并发消费
- 支付宝首次登录并发绑定
- 密码修改、重置和旧会话失效
- 用户端及管理端 RBAC 安全不变量
- 超级管理员并发保护、软删后重新注册和完整管理端 CRUD
- bootstrap 和共享数据库部署边界
测试环境需要可用的 Docker。普通单元测试禁止连接开发机上的真实 MySQL、Redis 或邮件服务。
只编译不运行测试:
./mvnw -DskipTests compile打包:
./mvnw clean package部署时使用可执行 JAR:
business/target/business-1.0.0-exec.jar
admin/target/admin-1.0.0-exec.jar
普通 JAR 用于模块依赖和架构测试,不是生产启动入口。
项目当前提供以下自定义注解。使用注解时应理解其职责边界,不要把业务逻辑继续塞入切面或拦截器。
包路径:
com.yxx.common.annotation.response.ResponseResult可标注在 Controller 类或方法上。只有显式标注的接口才会包装为 BaseResponse<T>,该能力不默认全局启用。
@ResponseResult
@RestController
@RequestMapping("/orders")
public class OrderController {
@GetMapping("/{id}")
public OrderResp detail(@PathVariable Long id) {
return orderService.detail(id);
}
}处理规则:
- 普通返回值包装为成功响应。
- 已经是
BaseResponse<?>时不会二次嵌套,只补充 TraceId。 ErrorResponse包装为失败响应。String返回值会显式序列化为 JSON,避免被StringHttpMessageConverter当作普通文本输出。
文件下载、流式响应或需要精确控制原始响应体的接口不要使用该注解。
包路径:
com.yxx.security.annotation.AllowAnonymous系统采用“默认需要登录”策略。只有明确标注 @AllowAnonymous 的 Controller 类或方法才允许不携带 Token 访问。
@AllowAnonymous
@PostMapping("/login")
public LoginRes login(@Valid @RequestBody LoginReq request) {
return authenticationService.login(request);
}使用要求:
- 只用于登录、注册、验证码、找回密码等确实公开的接口。当前工程未内置 Actuator 健康检查;后续增加探针时,只开放不包含敏感依赖详情的最小存活信息。
- 不要把它标在整个业务 Controller 上图省事。
- 匿名不等于无需参数校验、限流、审计或防重放。
包路径:
com.yxx.security.annotation.SaAdminCheckPermission该注解是指定了管理端 StpLogic 的 @SaCheckPermission 组合注解,避免误用默认用户端安全域。
@SaAdminCheckPermission(AdminSecurityCodes.PERMISSION_AUDIT_LOG_READ)
@GetMapping("/page")
public PageResponse<OperateLogResp> page(OperateLogReq request) {
return operateAdminLogService.operationLogPage(request);
}多个权限默认使用 AND:
@SaAdminCheckPermission({"order:read", "order:export"})需要满足任意一个权限时:
@SaAdminCheckPermission(
value = {"order:read", "order:review"},
mode = SaMode.OR)权限编码应定义为安全常量并使用冒号分层,例如 order:read、order:refund:approve,禁止在各 Controller 中散落手写字符串。
用户端权限校验可以使用 Sa-Token 原生 @SaCheckPermission;管理端必须使用 @SaAdminCheckPermission 或显式指定 type=admin。
包路径:
com.yxx.framework.audit.annotation.AuditLog示例:
@AuditLog(
module = "订单模块",
action = "取消订单",
eventType = AuditEventType.OPERATION,
resource = "order",
subjectType = "order",
subjectId = "#id")
@PostMapping("/{id}/cancel")
public void cancel(@PathVariable Long id) {
orderService.cancel(id);
}属性说明:
| 属性 | 必填 | 说明 |
|---|---|---|
module |
是 | 业务模块名称,用于日志展示和查询 |
action |
是 | 结构化操作名称,例如“取消订单” |
eventType |
否 | AUTHENTICATION、OPERATION 或 SECURITY,默认 OPERATION |
resource |
否 | 资源类型或说明,例如 order |
recordRequest |
否 | 是否记录脱敏后的请求参数,默认 true |
subjectType |
否 | 被操作主体或资源类型,例如 business-user、order |
subjectId |
否 | 主体稳定标识 SpEL,例如 #userId、#request.orderId |
subjectAccount |
否 | 尝试登录或被操作账号 SpEL,例如 #request.loginCode |
登录、修改密码、重置密码、验证码等敏感接口必须关闭请求参数记录:
@AuditLog(
module = "鉴权模块",
action = "用户密码登录",
eventType = AuditEventType.AUTHENTICATION,
recordRequest = false,
subjectAccount = "#request.loginCode")审计切面只采集上下文并发布 AuditEvent。数据库存储由具体应用的事件监听器负责,因此新增业务应用时可以建立自己的审计表和监听器,而不需要修改公共切面。
审计是辅助链路。审计发布或异步持久化失败会记录服务端错误,但不会覆盖原业务方法的成功或异常结果。
包路径:
com.yxx.security.validation.Password新密码使用完整策略:
@NotBlank(message = "新密码不能为空")
@Password
private String newPassword;登录密码和旧密码不能套用当前完整策略,因为用户密码可能创建于策略升级之前;此时只校验 BCrypt 最大字节限制:
@NotBlank(message = "密码不能为空")
@Password(enforcePolicy = false, message = "密码长度超过系统限制")
private String password;密码规则由 security.password-policy 配置:
security:
password-policy:
min-length: 12
max-bytes: 72
require-uppercase: true
require-lowercase: true
require-digit: true
require-special-character: false
allow-whitespace: false@Password 不负责空值校验,字段仍应配合 @NotBlank 使用。max-bytes 按 UTF-8 字节计算,因为中文和 Emoji 的字符数与 BCrypt 接收的字节数并不相同。
包路径:
com.yxx.common.validation.TrimmedSize适合登录账号、名称等需要先去除首尾空白,再校验规范化长度的字段:
@NotBlank(message = "名称不能为空")
@TrimmedSize(min = 2, max = 50, message = "名称规范化后应为2-50位")
private String name;该注解只负责长度校验,不会修改 DTO 的原始值;持久化前仍应显式调用统一规范化工具。
包路径:
com.yxx.common.annotation.jackson.QueryDateBoundary用于把查询请求中的日期转换为当天开始或结束时间:
@QueryDateBoundary(QueryDateBoundary.Boundary.START_OF_DAY)
private Date startTime;
@QueryDateBoundary(QueryDateBoundary.Boundary.END_OF_DAY)
private Date endTime;例如输入 2026-07-16 后:
START_OF_DAY转为当天00:00:00.000。END_OF_DAY转为当天结束时间。
该注解只用于 Jackson 反序列化的 Date 字段,适合请求体中的查询条件。URL 查询参数由 Spring 参数绑定处理,不会自动经过 Jackson 反序列化器;如需支持 URL 参数,应单独增加 Converter 或在服务层规范化。
用户端采用:
User(统一主体) 1 ---- N UserIdentity(登录身份)
密码、支付宝以及未来的微信、企业微信等身份都必须先映射为稳定的内部 userId,再由统一认证编排服务完成:
- 身份认证。
- 登录风险处理。
- 加载角色和权限快照。
- 构造
LoginPrincipal。 - 建立 Sa-Token Session。
LoginPrincipal 是运行时安全快照,不是数据库实体,也不应直接作为接口响应模型使用。
- 用户端使用 Sa-Token 默认安全域。
- 管理端使用独立的
admin安全域。 - 两个应用共用
rbac_role、rbac_permission、rbac_menu及三张关联表。 scope=admin表示管理后台权限域,scope=business表示业务用户权限域。subject_type=admin只能关联 admin 角色,subject_type=user只能关联 business 角色。- Java 服务校验、数据库
CHECK约束和复合外键共同阻止跨权限域授权。 - admin 可以管理两个权限域;business 只读取业务用户的 business 授权结果。
- 相同数据库 ID 在两个安全域内没有任何身份关联。
- 权限或角色变化后,必须在事务提交后注销受影响主体的全部会话,使旧权限快照立即失效。
common-security 中的 AuthorizationProvider 是稳定授权抽象,common-rbac 提供默认
数据库实现。后续接入 LDAP、IAM 或远程权限中心时,可以替换授权实现,而不需要修改登录
编排和 Sa-Token 会话代码。
管理端提供以下基础接口:
GET /management/business-users
GET /management/business-users/{userId}
PUT /management/business-users/{userId}/roles
PUT /management/business-users/{userId}/status
DELETE /management/business-users/{userId}
GET /management/admin-users
GET /management/admin-users/{userId}
POST /management/admin-users
PUT /management/admin-users/{userId}
PUT /management/admin-users/{userId}/status
PUT /management/admin-users/{userId}/roles
DELETE /management/admin-users/{userId}
GET /management/rbac/roles?scope=admin|business
GET /management/rbac/permissions?scope=admin|business
GET /management/rbac/menus?scope=admin|business
POST /management/rbac/roles
PUT /management/rbac/roles/{roleId}
DELETE /management/rbac/roles/{roleId}
POST /management/rbac/permissions
PUT /management/rbac/permissions/{permissionId}
DELETE /management/rbac/permissions/{permissionId}
POST /management/rbac/menus
PUT /management/rbac/menus/{menuId}
DELETE /management/rbac/menus/{menuId}
PUT /management/rbac/roles/{roleId}/permissions
PUT /management/rbac/roles/{roleId}/menus
业务用户没有直接修改角色的接口。前端管理系统先读取 business 权限域角色,再把最终
角色主键集合提交给业务用户角色接口。后端会验证目标用户存在、角色全部属于 business
权限域,并在事务提交后注销该用户旧会话。
业务用户注销采用可重新注册式软删:用户主体和全部登录身份保留原邮箱、手机号、登录账号 及第三方身份标识,但生成列唯一索引只约束未删除记录。这样既允许重新注册,也能基于历史 数据识别反复注销注册行为。业务端不提供全局审计日志查询权限;如果项目需要“我的操作 记录”,必须由服务端使用当前登录 userId 强制过滤。
角色和权限编码统一使用冒号分层:
business:member
business:operator
admin:administrator
admin:super-admin
business:order:read
business:order:create
admin:order:refund:approve
编码必须集中定义在安全常量类中,并同步维护 Flyway 初始化数据。菜单编码用于前端导航,后端接口权限使用独立权限表,二者不要复用同一字段表达不同语义。
以微信登录为例:
- 在
LoginMode中增加稳定编码,例如wechat。 - 新增实现
UserAuthenticationCommand的命令类型。 - 新增实现
UserAuthenticationStrategy的 Spring Bean。 - 策略负责验证平台凭据,并返回
AuthenticatedUser。 - 首次登录绑定逻辑放入独立事务服务,不要在外部网络调用期间占用数据库事务。
- 在
user_identity中增加对应身份记录,不要给User实体增加wechatOpenId等平台专属字段。 - 增加首次登录、已绑定登录、停用身份和并发首次登录集成测试。
策略骨架:
@Component
@RequiredArgsConstructor
public class WechatAuthenticationStrategy implements UserAuthenticationStrategy {
@Override
public String loginMode() {
return LoginMode.WECHAT;
}
@Override
public AuthenticatedUser authenticate(UserAuthenticationCommand command) {
// 1. 校验命令类型。
// 2. 使用服务端可信凭据向微信换取平台身份。
// 3. 查询或创建统一用户及 UserIdentity。
// 4. 校验身份和用户状态。
// 5. 返回统一认证结果,不在此处自行创建 Sa-Token Session。
throw new UnsupportedOperationException("请按项目认证流程实现微信登录");
}
}不要修改 UserAuthenticationService 增加 if/else。Spring 会自动收集新策略,并在启动时校验登录模式是否重复。
订单、库存等能力是否建立新模块,首先取决于它是普通领域模块,还是独立部署应用。两者的依赖和配置要求不同,不能统一依赖 common-framework。
普通领域模块作为现有应用的一部分运行,不提供独立启动类、端口和 Profile:
order
├── domain
├── service
├── mapper
└── test
普通领域模块的标准步骤:
- 在根
pom.xml注册模块。 - 按“公共模块选择指南”显式依赖代码直接使用的
common-*职责模块,禁止依赖common-framework。 - 建立本领域自己的实体、DTO、Mapper、错误码和安全常量。
- 由承载该领域的可执行应用提供 Controller、启动配置和最终基础设施装配。
- 如果仍使用当前共享 Schema,将表结构变化追加到
database-migrations的下一版本迁移。 - 增加 Mapper、事务、权限和核心业务测试,并由承载应用增加必要的集成测试。
只有当新能力需要独立启动、独立扩缩容或独立发布时,才建立新的可执行应用,例如 order-app。可执行应用可以依赖 common-framework,并必须设置唯一的 app.name、端口、Profile 和部署配置。是否继续连接当前共享 Schema 必须在架构设计中明确决定,不能因为 admin/business 当前共库就默认要求所有新应用共库。
如果新应用引入独立登录安全域,需要增加对应 StpLogic 和 CurrentActorProvider 适配;如果只是用户端下属业务,则继续复用用户身份和 business 权限域。
只有确定与领域无关、至少被多个应用复用的能力,才考虑拆到新的 common-* 模块。
- 在对应应用的安全常量类中增加权限编码;公共内置角色编码放在
RbacSecurityCodes。 - 新增共享 Flyway 迁移,写入正确
scope的角色、权限、菜单和必要关联。 - Controller 使用对应安全域的权限注解。
- 菜单只负责导航,接口权限只使用权限码。
- 运行期调整必须通过 admin 管理接口或公共替换服务,以确保权限域校验和会话失效。
- 内置超级管理员角色、权限通配符和“至少一个启用超级管理员”属于安全不变量,不得绕开服务直接修改关联表。
菜单约定:
status=false:菜单不可用,不进入菜单树。visible=false:只是不展示当前节点,不代表其可见子菜单也不可用。- 隐藏父菜单下的可见子菜单会提升到最近的可见祖先;没有可见祖先时提升为根节点。
- 菜单树构建器会防御循环父子关系,但数据库迁移和管理接口仍应阻止产生循环数据。
公共审计模块通过 Spring Application Event 发布 AuditEvent。新应用可以增加自己的监听器:
@Service
public class OrderAuditEventListener {
@Async("auditTaskExecutor")
@EventListener
public void save(AuditEvent event) {
// 将通用事件转换为本应用审计实体并持久化。
// 异步异常必须在监听器内记录,不能依赖调用方事务回滚。
}
}如需投递消息队列,可新增 AuditEventPublisher 实现或事件监听器,但应继续保持业务方法与具体存储技术解耦,并明确消息可靠性、失败告警和幂等策略。
- Redis Key 前缀集中维护,禁止在业务代码中散落字符串。
- 涉及“检查后修改”、计数器、一次性消费的逻辑必须使用 Redis 原子命令或 Lua。
- Redis Key 不直接放密码、Token、邮箱重置令牌等敏感原文,应使用摘要。
- 所有临时数据必须有 TTL;配置异常时宁可使用有限默认值,也不要创建永久安全状态。
- 不要自行创建新的
RedissonClient,连接生命周期由 Starter 和 Spring 容器统一管理。
Forest 是按需能力,不进入 common-framework。只有实际调用第三方 HTTP API 的模块才声明:
<dependency>
<groupId>com.yxx</groupId>
<artifactId>common-http-client</artifactId>
</dependency>连接池、超时、后端实现和重试使用 Forest 原生配置;基础框架只管理 TraceId 透传和日志安全策略:
forest:
connect-timeout: ${FOREST_CONNECT_TIMEOUT:3000}
read-timeout: ${FOREST_READ_TIMEOUT:5000}
max-connections: ${FOREST_MAX_CONNECTIONS:200}
max-route-connections: ${FOREST_MAX_ROUTE_CONNECTIONS:50}
max-retry-count: 0
variables:
partnerBaseUrl: ${PARTNER_BASE_URL:https://partner.example.com}
framework:
http-client:
trace-id-propagation-enabled: true
trace-id-header-name: Trace-Id
logging:
enabled: falseClient 接口放在实际调用方模块中,不放进 common-http-client:
@ForestClient
public interface PartnerClient {
@Get("${partnerBaseUrl}/health")
String health();
}应用启动包下的 Forest Client 会被自动扫描;Client 位于启动包之外时,在启动类使用
@ForestScan(basePackageClasses = PartnerClient.class) 显式限定扫描范围。第三方 DTO、认证签名、
错误码映射、限流和熔断策略属于具体外部系统契约,应留在调用方模块。
common-http-client 默认关闭 Forest 请求和响应日志,因为请求概要也可能包含 URL 查询参数。
确需排障时通过 framework.http-client.logging.* 逐项开启;禁止记录 Token、Authorization、签名、
验证码、密钥或敏感请求体。重试默认关闭,只有确认请求幂等并明确退避策略后才能开启;支付、创建、
扣减等非幂等请求不得仅靠统一重试配置处理。
新增可配置能力时:
- 使用
@ConfigurationProperties建立类型安全配置类。 - 提供安全且适合基础框架的默认值。
- 对 IDE 无法自动识别的属性维护
additional-spring-configuration-metadata.json。 - 敏感配置通过环境变量或密钥管理系统注入,不写入源码和生产配置模板。
- 为默认值、边界值和 Bean 注册唯一性增加测试。
- 普通辅助任务使用
applicationTaskExecutor。 - 审计持久化使用
auditTaskExecutor。 - 禁止使用
CompletableFuture默认公共线程池。 - 新增高吞吐、可独立积压的任务类型时,应建立独立有界线程池或消息队列。
- 异步线程不能直接读取
HttpServletRequest;必须在请求线程内提取必要数据。 - 需要链路追踪时应保留现有 MDC 传播和任务结束清理机制。
@ResponseResult为显式启用,不默认包装全部 Controller。- 业务异常使用
ApiException和ApiCode。 - HTTP 状态码负责表达通用协议语义,业务错误码负责表达具体业务场景。
- 未知异常只向客户端返回通用消息,完整堆栈保留在服务端日志。
- Controller 不直接返回 MyBatis-Plus
Page,统一转换为PageResponse<T>。
- 注册、登录、修改密码和重置密码统一使用同一个
PasswordEncoder。 PasswordEncoder.matches参数顺序是“请求明文、数据库摘要”,禁止直接用字符串比较密码。- 登录账号和邮箱在查询、缓存 Key 生成和持久化前统一规范化。
- 登录身份必须同时满足
status=true和verified=true。 - 新密码使用配置化密码策略;旧密码和登录密码只校验编码器安全上限。
- 密码修改和重置成功后,在事务提交后注销该主体的全部旧会话。
- 一次性验证码和重置 Token 支持并发唯一消费及事务失败重试。
- 每个 HTTP 请求都会生成或继承合法的
Trace-Id,并通过响应头返回。 - 请求结束后清理 ThreadLocal 和 MDC,防止容器线程复用导致串号。
- 密码、Token、授权头、密钥和签名进入日志前统一脱敏。
- dev、integration 和 bootstrap 等非生产 Profile 只输出控制台日志,避免测试或本地启动在不同工作目录生成多套日志文件;其中 dev 使用彩色格式,测试和 bootstrap 保持纯文本。
- prod Profile 同时输出控制台和滚动文件;admin、business 分别使用
admin.log、business.log。 - 生产环境应通过
LOG_PATH指定独立于程序目录的绝对日志路径。默认./logs相对于 JVM 当前工作目录,不代表项目根目录。 - Controller 访问日志默认关闭,可通过
WEB_ACCESS_LOG_ENABLED=true开启。 - 业务审计由显式
@AuditLog控制,与访问日志开关相互独立。 - 审计日志保存事件发生时的主体快照,历史查询不依赖当前用户资料。
- 分页大小上限为 200,MyBatis-Plus 拦截器执行最终限制。
- 禁止无条件全表更新和删除。
- Redis 连接由 Starter 管理,禁止业务代码自行维护第二套连接池。
- 登录失败通过账号和 IP 双维度 Redis Lua 原子预占。
- 密码重置 Token 使用摘要作为 Redis Key,事务提交后保留已消费标记至原 Token 过期。
- 只有请求直接来源位于
ip.trusted-proxies时才信任X-Forwarded-For和X-Real-IP。 - 生产环境必须按真实网关地址配置可信代理,不能使用不受控的全网段通配。
- IP 归属地和异常登录邮件属于辅助能力,解析失败不能阻断认证和业务请求。
common-ip/src/main/resources/ip2region/ip2region.xdb是运行时需要的归属地数据库资源,不应删除。
- 路径统一使用 kebab-case,例如
/send-captcha、/reset-password、/change-password。 - 当前项目仍处于架构阶段,不保留旧 camelCase 路径兼容入口。
使用基础框架时必须理解以下边界,不能把辅助能力误认为强一致保证:
- 操作审计采用异步 best-effort 持久化。队列拒绝或数据库故障时会记录错误,但不会回滚已经完成的业务操作;有合规审计要求时应改用事务 Outbox、消息队列或其他可靠投递机制。
- 密码修改、账号停用和权限变化会在数据库事务提交后注销会话;Redis 故障后的重试任务目前保存在 JVM 内存中,进程重启不会恢复未完成任务。
- Sa-Token Session 使用滑动过期,持续活跃的会话可以不断续期;需要绝对最长会话时间或敏感操作重新认证时,应在具体项目中补充策略。
- IP 归属地、登录风险邮件和普通访问日志属于辅助链路,失败不会阻断已经通过的认证和业务请求。
- admin 与 business 共用 MySQL Schema 是当前工程的部署约束,不代表后续所有领域应用都必须共库。
生产发布前至少完成以下检查:
- 显式设置
SPRING_PROFILES_ACTIVE=prod,并确认没有默认连接开发或共享测试环境。 - 数据库、Redis、SMTP 和第三方平台密钥全部通过环境变量或密钥管理系统注入;轮换任何曾进入 Git 历史的凭据。
- admin 与 business 的数据源指向同一个预期 Schema,并在发布前备份数据库、验证全部 Flyway 迁移。
- 业务 Redis 与 Sa-Token Session Redis 使用不同 database 或独立实例,并配置认证、网络访问控制和持久化策略。
- 配置准确的 CORS 来源、可信代理地址、前端密码重置页面 URL 和站点域名。
- 对外流量使用 HTTPS;SMTP 按服务商要求开启 TLS,不通过明文网络传输认证信息。
- bootstrap 只在首个管理员初始化期间启用,执行完成后使用正常生产 Profile 启动。
- 通过
LOG_PATH配置绝对日志目录,并配置日志采集、磁盘容量和敏感字段检查;为会话失效耗尽、邮件失败、审计失败建立告警。 - 执行
./mvnw clean verify,并使用生产等价配置完成启动与关键登录流程冒烟验证。
唯一迁移目录:
database-migrations/src/main/resources/db/migration/shared
管理端和业务端共同使用 flyway_schema_history。禁止在应用模块中再次创建独立迁移目录或
历史表,否则会重新引入启动顺序和跨模块外键无法统一管理的问题。
已经发布或提交到共享分支的迁移文件禁止修改。表、字段、索引、约束和初始化数据变化必须新增更高版本迁移,例如:
V2__add_order_permission.sql
V3__add_order_refund_record.sql
SQL 规范:
- 表和字段必须提供中文
COMMENT。 - 明确主键、唯一约束、外键策略和必要索引。
- RBAC 初始化数据的编码必须与 Java 安全常量保持一致。
- 迁移应同时验证从空库完整执行和从上一版本升级。
- 禁止继续维护可重复执行的全量建表脚本代替 Flyway。
详细说明见 db/README.md。
提交代码前至少确认:
- 新接口是否明确选择
@ResponseResult、登录要求、权限要求和审计要求。 - 匿名接口是否仍有参数校验、限流、防重放和敏感信息保护。
- DTO、实体、Session 主体和第三方平台模型是否保持边界清晰。
- 权限变化是否使旧会话失效。
- Redis 并发逻辑是否真正原子,并且所有临时 Key 都有 TTL。
- 数据库变化是否通过新 Flyway 迁移实现,并包含中文注释。
- 异步任务是否使用有界执行器并正确传播、清理 MDC。
- 核心业务是否有单元测试;涉及数据库、Redis、Mapper、事务和并发时是否有集成测试。
./mvnw clean verify是否完整通过。