feat(auth): 实现统一认证模块及登录策略
- 新增 ApiResult 统一响应封装类,规范前端接口返回结构 - 添加 AuthController,支持多种登录方式及用户信息接口 - 实现 AuthService,包含登录、登出、密码修改、绑定手机号、重置密码等功能 - 集成图形验证码校验和登录失败锁定机制,增强安全性 - 支持邮箱登录和手机号验证码登录,完善多样化认证策略 - 实现 JWT 令牌签发与吊销,确保会话安全管理 - 配置不同环境 application.yml 及配置,支持多租户和灵活缓存配置 - 增加 .gitignore 规则,屏蔽构建产物、IDE 文件和敏感配置 - 引入 AuthProperties 以配置登录与验证码策略参数
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
# iam-api · 统一身份中心
|
||||
|
||||
> 技术栈:Java 25 + Spring Boot 3.5 + MyBatis-Plus + Spring Security + Flyway + Redis
|
||||
> 定位:**open-api 与 dp-api 共用的唯一账号源**,让数据中台可以脱离开放平台独立售卖。
|
||||
|
||||
## 这个服务解决什么问题
|
||||
|
||||
此前 `dp-web` 控制台的登录直接复用 `open-api` 的账号体系,`dp-api` 没有自己的身份。
|
||||
一旦 `dp-api` 要独立对外售卖 / 部署,账号体系就成了耦合点。iam-api 把「用户 / 租户 /
|
||||
角色 / 权限 / 菜单 / 登录 / 令牌签发」抽成独立服务,`open-api` 与 `dp-api` 都只作为
|
||||
它的**资源服务**:通过 JWKS 验签,不共享用户表、不共享对称密钥。
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ iam-api │
|
||||
│ 账号/租户/角色/权限/菜单/令牌 │
|
||||
└───────────┬──────────────────┘
|
||||
RS256 令牌 + JWKS │ RS256 令牌 + JWKS
|
||||
┌─────────────────────┴─────────────────────┐
|
||||
▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│ open-api │ │ dp-api │
|
||||
└─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
详细规划见 [`docs/iam-api-dev-plan.md`](docs/iam-api-dev-plan.md)。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 方式一:零依赖演示(推荐先跑这个)
|
||||
|
||||
```bash
|
||||
# H2 内存库 + 进程内缓存,不连 MySQL / Redis
|
||||
./run.sh demo
|
||||
|
||||
# 或
|
||||
./mvnw -Pdemo spring-boot:run -Dspring-boot.run.profiles=demo
|
||||
```
|
||||
|
||||
启动后访问 `http://localhost:8900/swagger-ui.html`(演示环境已开接口文档)。
|
||||
演示 profile 默认仍开启图形验证码,联调时可临时关掉:
|
||||
|
||||
```bash
|
||||
./mvnw -Pdemo spring-boot:run -Dspring-boot.run.profiles=demo \
|
||||
-Dspring-boot.run.arguments=--config.auth.captcha-enabled=false
|
||||
```
|
||||
|
||||
### 方式二:连接真实依赖
|
||||
|
||||
```bash
|
||||
cp config/iam-jwt.properties.example config/iam-jwt.properties
|
||||
bash scripts/gen-iam-key.sh >> config/iam-jwt.properties # 生成 RS256 密钥
|
||||
|
||||
export DB_HOST=127.0.0.1 DB_USERNAME=root DB_PASSWORD=yourpass DB_NAME=iam
|
||||
export REDIS_HOST=127.0.0.1 INTERNAL_KEY=your-internal-key JWT_SECRET=your-32byte-secret
|
||||
|
||||
./run.sh # dev profile
|
||||
```
|
||||
|
||||
> 系统 `java` 若是 17,直接 `./mvnw` 会报 `release version 25 not supported`,
|
||||
> 用 `./run.sh`(内部固定 JDK 25)或自行 `export JAVA_HOME=<jdk25>`。
|
||||
|
||||
初始账号:`admin / admin123`(租户 1)。首次登录后请立刻改密码。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
src/main/java/com/gxwebsoft/
|
||||
├─ common/core/ 内核:返回结构、异常、分页、租户上下文、缓存抽象、安全过滤器
|
||||
│ ├─ cache/ CacheStore:RedisCacheStore / MemoryCacheStore
|
||||
│ ├─ config/ SecurityConfig / MybatisPlusConfig / CacheConfig ...
|
||||
│ ├─ security/ HS256 会话令牌(JwtUtil / JwtAuthenticationFilter / 吊销名单)
|
||||
│ └─ web/ ApiResult / PageParam / BaseController / TenantFilter
|
||||
├─ common/system/ 身份与授权:租户 / 用户 / 角色 / 菜单 / 登录审计 / 验证码
|
||||
│ ├─ controller/ AuthController、CaptchaController、User/Role/Menu/TenantController
|
||||
│ ├─ entity/ mapper/ param/ sys_tenant / sys_user / sys_role / sys_menu / sys_login_record
|
||||
│ └─ security/ SystemLoginUserProvider(登录主体 + 权限集合)
|
||||
└─ iam/ 统一令牌与接入方
|
||||
├─ config/ IamJwtConfig(RS256 + JWKS)
|
||||
├─ controller/ IamOAuthController(/api/v1/oauth/**)、IamClientController
|
||||
└─ entity/ iam_client、iam_auth_code
|
||||
```
|
||||
|
||||
## 接口一览
|
||||
|
||||
| 分组 | 接口 | 认证 |
|
||||
|---|---|---|
|
||||
| 认证 | `POST /api/login`、`POST /api/loginByEmail`、`POST /api/loginBySms` | 免登录 |
|
||||
| 认证 | `POST /api/logout`、`GET /api/me`、`POST /api/changePassword`、`POST /api/bindPhone[/captcha]` | 会话令牌 |
|
||||
| 认证 | `POST /api/resetPassword` | 免登录(凭验证码) |
|
||||
| 验证码 | `GET /api/captcha`、`POST /api/sendSmsCaptcha`、`POST /api/sendEmailCaptcha` | 免登录 |
|
||||
| 租户 | `GET/POST/PUT/DELETE /api/iam/tenant`、`POST /api/iam/tenant/open` | 权限点 `iam:tenant:*` |
|
||||
| 用户 | `GET/POST/PUT/DELETE /api/iam/user`、`POST /api/iam/user/{id}/resetPassword` | 权限点 `iam:user:*` |
|
||||
| 角色 | `GET/POST/PUT/DELETE /api/iam/role`、`PUT /api/iam/role/{id}/menus` | 权限点 `iam:role:*` |
|
||||
| 菜单 | `GET /api/iam/menu/tree`、`POST/PUT/DELETE /api/iam/menu` | 权限点 `iam:menu:*` |
|
||||
| 接入方 | `GET/POST/PUT/DELETE /api/iam/client`、`POST /api/iam/client/{id}/secret` | 权限点 `iam:client:*` |
|
||||
| 令牌 | `POST /api/v1/oauth/authorize`、`POST /api/v1/oauth/token` | 终端用户 / client_id+secret |
|
||||
| 令牌 | `GET /api/v1/oauth/jwks` | 免登录 |
|
||||
| 令牌 | `POST /api/v1/oauth/introspect`、`POST /api/v1/oauth/revoke` | `X-Internal-Key` |
|
||||
|
||||
统一响应:`{"code":0,"message":"操作成功","data":{...}}`,与既有控制台契约一致。
|
||||
|
||||
### 令牌怎么用(资源服务视角)
|
||||
|
||||
1. 启动时拉取 `http://iam-host/api/v1/oauth/jwks`,缓存公钥。
|
||||
2. 校验 `Authorization: Bearer <token>` 的签名、`iss`、`exp`、`aud`,并确认
|
||||
`token_type == access`(refresh token 不能当 access token 用)。
|
||||
3. 从 claim 里取 `tenant_id` / `user_id` / `roles` / `scope` 做业务授权。
|
||||
4. 需要感知吊销时,再用 `X-Internal-Key` 调 introspect 复核。
|
||||
|
||||
access token 默认 2 小时,refresh token 默认 30 天;**刷新即轮换**,接续方必须
|
||||
用返回的新 refreshToken 覆盖本地,否则下次刷新会失败。
|
||||
|
||||
### 内置接入方
|
||||
|
||||
迁移脚本预置了两条,`client_id` 分别是 `open-api` 与 `dp-api`,**演示密钥为
|
||||
`admin123`**(与初始管理员密码同一个 BCrypt 哈希,便于开箱联调)。
|
||||
**上线前必须在「接入方管理」里重置为随机密钥并妥善保存**,明文只在创建 / 重置时返回一次。
|
||||
|
||||
## 配置项
|
||||
|
||||
| 配置 | 说明 | 默认 |
|
||||
|---|---|---|
|
||||
| `server.port` | 服务端口 | `8900` |
|
||||
| `iam.cache.type` | `redis` / `memory` | `redis`(demo/test 为 `memory`) |
|
||||
| `config.jwt.secret` | 控制台会话令牌密钥(HS256,≥32 字节) | 开发默认值,生产必填 |
|
||||
| `config.iam-jwt.private-key` | 资源服务令牌私钥(RS256,PEM) | 缺省时临时生成并告警 |
|
||||
| `config.iam-jwt.issuer` / `audience` | 令牌 `iss` / 默认 `aud` | `https://iam.websoft.top` / `websoft-platform` |
|
||||
| `config.iam-jwt.access-expire` / `refresh-expire` | 令牌有效期(秒) | `7200` / `2592000` |
|
||||
| `config.internal-key` | introspect / revoke 自证密钥 | `change-me`,生产必填 |
|
||||
| `config.auth.captcha-enabled` | 登录是否强制图形验证码 | `true` |
|
||||
| `config.auth.login-fail-threshold` / `login-lock-seconds` | 失败锁定策略 | `5` 次 / `600` 秒 |
|
||||
| `config.auth.register-enabled` | 自助注册开关(当前无注册接口,账号由平台运营开通) | `false` |
|
||||
| `config.sms.enabled` | 是否真正下发短信 | `false`(仅写日志) |
|
||||
| `config.mail.enabled` / `spring.mail.*` | 邮件验证码通道 | 未配 SMTP 时仅写日志 |
|
||||
| `spring.flyway.user` / `spring.flyway.password` | Flyway 迁移专用账号(`prod` 必填) | 无默认值,缺失即启动失败 |
|
||||
|
||||
## 部署
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
必须提供的环境变量:`DB_PASSWORD`、`JWT_SECRET`、`INTERNAL_KEY`,以及(首次启动前)
|
||||
挂载到 `/app/config/iam-jwt.properties` 的 RS256 密钥。
|
||||
`prod` 还需要 `FLYWAY_USER` / `FLYWAY_PASSWORD`(迁移专用账号,见下方常见问题)。
|
||||
详见 [`docker-compose.yml`](docker-compose.yml)。
|
||||
|
||||
无 1Panel / 单机部署:`./mvnw clean package` 产出 `target/iam-api-0.1.0.jar`,
|
||||
`java -jar` 启动,用 `SPRING_PROFILES_ACTIVE=prod` 与上面的环境变量即可。
|
||||
|
||||
### 部署检查清单
|
||||
|
||||
| # | 检查项 | 说明 |
|
||||
|---|---|---|
|
||||
| 1 | RS256 密钥已固定 | 不配 `private-key` 会每次重启换密钥,已签发令牌全部失效 |
|
||||
| 2 | `JWT_SECRET` / `INTERNAL_KEY` 已换成随机值 | 默认值只能用于本地 |
|
||||
| 3 | 内置接入方密钥已重置 | `open-api` / `dp-api` 的演示密钥是 `admin123` |
|
||||
| 4 | `admin` 初始密码已修改 | 演示密码 `admin123` |
|
||||
| 5 | Redis 已接入并纳入健康检查 | 验证码、失败计数、令牌吊销都依赖它 |
|
||||
| 6 | 接口文档按需关闭 | 生产默认 `API_DOCS_ENABLED=false` |
|
||||
| 7 | 反向代理放行 `Authorization` 与 `X-Internal-Key` | 漏掉会导致「登录成功但接口 401」 |
|
||||
| 8 | `FLYWAY_USER` / `FLYWAY_PASSWORD` 已配好 | 迁移账号需有库 DDL 权限 + `performance_schema.user_variables_by_thread` 的 SELECT,建号脚本见 `scripts/create-flyway-user.sql` |
|
||||
|
||||
## 与另外两个服务的关系(迁移路径)
|
||||
|
||||
| 阶段 | 动作 |
|
||||
|---|---|
|
||||
| 现在 | iam-api 可独立运行,接口与数据自洽 |
|
||||
| 下一步 | open-api 改为 iam-api 的接入方:登录代理 / 跳转,`/api/me` 改读 iam-api |
|
||||
| 下一步 | dp-api 接入 JWKS 验签,用 `tenant_id` 做数据隔离 |
|
||||
| 之后 | 存量用户从 `db_open.sys_user` 迁移到 iam-api,迁移期以 iam-api 为准、禁止双向写 |
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
./mvnw test # H2 + 内存缓存,覆盖登录 / 权限 / 令牌 / JWKS / 自省 / 授权码
|
||||
./mvnw clean package # 生产包(不含 H2)
|
||||
```
|
||||
|
||||
## 当前未完成(见开发计划 M5/M6)
|
||||
|
||||
- SSO 会话表与跨域免登票据、MFA、登录设备管理。
|
||||
- open-api / dp-api 的接入改造与存量用户迁移脚本。
|
||||
- 真实的短信 / 邮件通道实现(当前预留 `SmsSender` 扩展点,默认只写日志)。
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 启动报 `SELECT command denied to user 'xxx'@'...' for table 'user_variables_by_thread'`
|
||||
|
||||
不是业务代码的查询,是 **Flyway 的 MySQL 支持在建立连接时固定执行的探测**:
|
||||
它会读 `performance_schema.user_variables_by_thread`(MariaDB 则是
|
||||
`information_schema.user_variables`)来记录 `foreign_key_checks` / `sql_safe_updates`,
|
||||
迁移结束后再用 `SET foreign_key_checks=?, sql_safe_updates=?` 还原。是否启用该探测
|
||||
**只按数据库版本判断(MySQL ≥5.7 就启用),不看账号权限**,所以业务账号权限被收敛后就会撞上。
|
||||
|
||||
#### 本项目的选择:方案 B —— Flyway 用独立账号
|
||||
|
||||
`application-prod.yml` 已经启用:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
flyway:
|
||||
user: ${FLYWAY_USER}
|
||||
password: ${FLYWAY_PASSWORD}
|
||||
```
|
||||
|
||||
理由:业务账号的权限是被有意收敛的(它不该有 DDL 权限),为了跑迁移去给它开
|
||||
`performance_schema` 反而放大了它的权限面。迁移用独立账号,权限各归各位。
|
||||
|
||||
落地三步:
|
||||
|
||||
```bash
|
||||
# 1) 用 root / 高权限账号建迁移账号(脚本里替换 4 个占位符后执行)
|
||||
mysql -h <host> -u root -p < scripts/create-flyway-user.sql
|
||||
|
||||
# 2) 自检:应返回一行,而不是 SELECT command denied
|
||||
mysql -h <host> -u <FLYWAY_USER> -p -e \
|
||||
"SELECT VARIABLE_VALUE FROM performance_schema.user_variables_by_thread WHERE VARIABLE_NAME='foreign_key_checks' LIMIT 1;"
|
||||
|
||||
# 3) 部署时注入变量
|
||||
export FLYWAY_USER=flyway_iam FLYWAY_PASSWORD='<强密码>'
|
||||
```
|
||||
|
||||
两个设计细节值得知道:
|
||||
|
||||
- **不需要配 `spring.flyway.url`**。只给 `user`/`password` 时,Spring Boot 会从主数据源的
|
||||
URL 与驱动派生一个只换账号密码的迁移数据源(已实测派生出的 url 与主库完全一致),
|
||||
所以 JDBC URL 只有 `datasource` 一处,不用两边同步维护。
|
||||
- **两个变量故意不给默认值**。`spring.flyway.user` 为空时 Spring 会静默回退到业务账号,
|
||||
也就是再次撞上同一个权限错误——而且是换一副面孔报出来。让它缺变量就启动失败,
|
||||
比事后排查便宜得多。
|
||||
|
||||
#### 备选方案
|
||||
|
||||
**A. 给业务账号补权限**(如果以后想少一个账号,且托管库允许)
|
||||
|
||||
```sql
|
||||
-- 主机按报错里的来源地址写;账号若建在 % 上就写 '%'
|
||||
GRANT SELECT ON performance_schema.user_variables_by_thread TO '业务账号'@'来源主机';
|
||||
FLUSH PRIVILEGES;
|
||||
```
|
||||
|
||||
**C. 关掉 Flyway,手工执行迁移**(云数据库连 `performance_schema` 都不让授权时的兜底)
|
||||
|
||||
```bash
|
||||
export SPRING_FLYWAY_ENABLED=false
|
||||
|
||||
# 用有权限的账号(通常是跳板机上的 root)执行
|
||||
DB_HOST=... DB_NAME=iam DB_USERNAME=root DB_PASSWORD=... \
|
||||
bash scripts/apply-migrations.sh
|
||||
```
|
||||
|
||||
脚本按文件名顺序执行 `src/main/resources/db/migration/*.sql`,密码走 `MYSQL_PWD`、
|
||||
不会出现在 `ps` 里。它不写 `flyway_schema_history`;之后若要交回 Flyway,
|
||||
应用的 `baseline-on-migrate=true` 会先打基线再跳过已应用的版本。
|
||||
Reference in New Issue
Block a user