Files
jczxw-java/iam-api/README.md
T
gxwebsoft 5e86ebe6b7 feat(auth): 实现统一认证模块及登录策略
- 新增 ApiResult 统一响应封装类,规范前端接口返回结构
- 添加 AuthController,支持多种登录方式及用户信息接口
- 实现 AuthService,包含登录、登出、密码修改、绑定手机号、重置密码等功能
- 集成图形验证码校验和登录失败锁定机制,增强安全性
- 支持邮箱登录和手机号验证码登录,完善多样化认证策略
- 实现 JWT 令牌签发与吊销,确保会话安全管理
- 配置不同环境 application.yml 及配置,支持多租户和灵活缓存配置
- 增加 .gitignore 规则,屏蔽构建产物、IDE 文件和敏感配置
- 引入 AuthProperties 以配置登录与验证码策略参数
2026-09-23 11:59:45 +08:00

257 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` 会先打基线再跳过已应用的版本。