- 将数据库名从db_ima修改为db_iam - 将数据库用户名从db_ima修改为db_iam - 保持其他数据库连接属性不变
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。
快速开始
方式一:零依赖演示(推荐先跑这个)
# H2 内存库 + 进程内缓存,不连 MySQL / Redis
./run.sh demo
# 或
./mvnw -Pdemo spring-boot:run -Dspring-boot.run.profiles=demo
启动后访问 http://localhost:8900/swagger-ui.html(演示环境已开接口文档)。
演示 profile 默认仍开启图形验证码,联调时可临时关掉:
./mvnw -Pdemo spring-boot:run -Dspring-boot.run.profiles=demo \
-Dspring-boot.run.arguments=--config.auth.captcha-enabled=false
方式二:连接真实依赖
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":{...}},与既有控制台契约一致。
令牌怎么用(资源服务视角)
- 启动时拉取
http://iam-host/api/v1/oauth/jwks,缓存公钥。 - 校验
Authorization: Bearer <token>的签名、iss、exp、aud,并确认token_type == access(refresh token 不能当 access token 用)。 - 从 claim 里取
tenant_id/user_id/roles/scope做业务授权。 - 需要感知吊销时,再用
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 必填) |
无默认值,缺失即启动失败 |
部署
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。
无 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 为准、禁止双向写 |
验证
./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 已经启用:
spring:
flyway:
user: ${FLYWAY_USER}
password: ${FLYWAY_PASSWORD}
理由:业务账号的权限是被有意收敛的(它不该有 DDL 权限),为了跑迁移去给它开
performance_schema 反而放大了它的权限面。迁移用独立账号,权限各归各位。
落地三步:
# 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. 给业务账号补权限(如果以后想少一个账号,且托管库允许)
-- 主机按报错里的来源地址写;账号若建在 % 上就写 '%'
GRANT SELECT ON performance_schema.user_variables_by_thread TO '业务账号'@'来源主机';
FLUSH PRIVILEGES;
C. 关掉 Flyway,手工执行迁移(云数据库连 performance_schema 都不让授权时的兜底)
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 会先打基线再跳过已应用的版本。