# 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=`。 初始账号:`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 ` 的签名、`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 -u root -p < scripts/create-flyway-user.sql # 2) 自检:应返回一行,而不是 SELECT command denied mysql -h -u -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` 会先打基线再跳过已应用的版本。