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

13 KiB
Raw Blame History

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":{...}},与既有控制台契约一致。

令牌怎么用(资源服务视角)

  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 必填) 无默认值,缺失即启动失败

部署

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 会先打基线再跳过已应用的版本。