OA 后端与 MySQL 接入 IAM 对接文档
OA 后端与 MySQL 接入 IAM 对接文档
Section titled “OA 后端与 MySQL 接入 IAM 对接文档”1. 文档目的与职责边界
Section titled “1. 文档目的与职责边界”本文根据当前 oain-web 和 oacn-api 代码整理,交付给 OA 后端同事,用于修改 oacn-api 和 OA MySQL 数据库,使 OA 接入当前 IAM 统一身份认证。
| 系统 | 技术栈 | 负责人 | 本次职责 |
|---|---|---|---|
| IAM | Better Auth 1.7.1、Hono、PostgreSQL | IAM 负责人 | 创建并配置 OA OAuth/OIDC Client,提供 issuer、Client ID、Client Secret 和 Claims 契约 |
oacn-api |
Laravel 10、Sanctum、Bouncer | OA 后端同事 | 实现 OIDC 登录、Token 校验、OA 用户绑定、OA Token 签发和 Back-Channel Logout |
| OA 数据库 | MySQL | OA 后端同事 | 审查并执行 Laravel migration,修改 users 和 personal_access_tokens |
oain-web |
Vue 3、Vite、Pinia、Axios | OA 前端负责人 | 发起登录、接收一次性 ticket、保存 OA Sanctum Token;当前代码已具备目标流程,联调时验证即可 |
边界结论:
- OA 后端同事同时负责修改
oacn-api和 OA MySQL。 - IAM 负责人可以修改 IAM 自己的 PostgreSQL,但不直接修改 OA MySQL。
- OA Web 不接触 OIDC Client Secret,也不直接用 Authorization Code 换取 IAM Token。
- 本文描述的是当前工作区中的目标实现;后端同事仍需在自己的正式分支审查、迁移、测试并发布。
2. 当前代码基线与方案
Section titled “2. 当前代码基线与方案”| 项目 | 当前基线 |
|---|---|
oain-web commit |
46aad626b114f44767112bc4072d6b4a79dc734e |
oacn-api commit |
1a323270a47c6d13362d6c31bba53edc304d3897 |
| OA Web | Vue 3.5、Vite 6、TypeScript、Axios、Pinia |
| OA API | PHP ^8.1、Laravel ^10.10、Sanctum ^3.2、Bouncer |
| OA 数据库 | MySQL |
| IAM | Better Auth 1.7.1,OAuth 2.1/OIDC Provider |
| IAM 当前 JWT 签名算法 | RS256,RSA 2048 |
| OA 本地 Token 默认期限 | 建议 300 分钟,以最终环境配置为准 |
采用服务端 BFF 模式:
sequenceDiagram
autonumber
participant U as 浏览器
participant W as oain-web
participant A as oacn-api
participant I as IAM
participant M as OA MySQL
U->>W: 点击统一身份登录
W->>A: GET /api/auth/iam/start?return_to=/...
A->>A: 生成 state、nonce、PKCE,写入共享缓存
A-->>U: 302 跳转 IAM authorize
U->>I: 企业微信登录并授权
I-->>A: 302 callback?code=&state=
A->>I: 服务端使用 code + verifier 换 Token
A->>I: 获取 JWKS,验证 ID Token,调用 UserInfo
A->>M: 匹配并绑定 OA 用户
A-->>U: 302 oain-web /iam/callback?ticket=...
W->>A: POST /api/auth/iam/exchange
A->>M: 创建关联 IAM sid 的 Sanctum Token
A-->>W: 返回 OA Token
W->>A: GET /api/auth/user
A-->>W: 返回 OA 本地 Bouncer 角色
核心原则:
- IAM 负责证明用户身份和判断用户能否进入 OA 应用。
- OA 继续使用自己的用户、Sanctum Token、Bouncer 角色和业务权限。
- IAM Access Token 不能直接作为 OA 业务接口的 Bearer Token。
- IAM
app_role当前只用于日志或后续扩展,不能覆盖 OA 本地 Bouncer 角色。 - 默认不自动创建 OA 用户;统一登录用户必须先存在于 OA。
3. oain-web 已确定的后端契约
Section titled “3. oain-web 已确定的后端契约”当前 oain-web 已实现统一登录入口和回调页,后端必须保持以下接口契约。
3.1 发起登录
Section titled “3.1 发起登录”前端整页跳转:
GET {VITE_BASE_URL}/auth/iam/start?return_to=%2Ftarget其中 VITE_BASE_URL 已包含 /api。return_to 仅允许 OA 站内绝对路径;前后端都应拒绝 //evil.example、带 scheme、反斜杠或控制字符的地址。
3.2 前端回调
Section titled “3.2 前端回调”OA 后端完成 IAM 回调和用户匹配后,跳转:
https://<oa-web-domain>/iam/callback?ticket=<one-time-ticket>失败时跳转:
https://<oa-web-domain>/iam/callback?error=<error-code>/iam/callback 是公开路由。前端读取参数后会立即清理地址栏,避免 ticket 留在浏览历史中。
3.3 ticket 换 OA Token
Section titled “3.3 ticket 换 OA Token”请求:
POST /api/auth/iam/exchangeContent-Type: application/json
{"ticket":"<one-time-ticket>"}成功响应必须沿用 OA API 的统一响应包装,使前端能够读取 result.data.token:
{ "code": 200, "message": "success", "data": { "token": "<sanctum-plain-text-token>", "token_type": "Bearer", "role": ["<oa-local-role>"] }}实际顶层 code、message 名称以 OA 现有响应封装为准,但 data.token 不得改变。前端保存 OA Token 后会继续请求:
GET /api/auth/userAuthorization: Bearer <oa-sanctum-token>APP-DATA-SYS: NDD/api/auth/user 返回的 data.role 是前端最终使用的 OA 本地角色。没有 OA 角色时,前端应拒绝进入业务页面。
3.4 兼容现有登录和退出
Section titled “3.4 兼容现有登录和退出”- 现有账号密码登录和企业微信旧登录可在迁移期保留,是否下线另行安排。
- 新 IAM 映射流程不得复用旧企业微信登录中的“按手机号自动创建用户”逻辑。
- 当前 OA 页面退出只删除 OA 本地 Token,不会退出 IAM 或其他系统;页面提示应保持这一语义。
- IAM 主动退出通过 Back-Channel Logout 通知 OA 撤销对应 Token,不依赖浏览器前端回调。
4. IAM 负责人需要提供的配置
Section titled “4. IAM 负责人需要提供的配置”4.1 创建 OA OIDC Client
Section titled “4.1 创建 OA OIDC Client”建议通过 IAM 管理端创建,不直接手写 IAM PostgreSQL 记录。
| 配置项 | 值 |
|---|---|
| Client Name | OA |
| Client 类型 | 机密客户端 |
| Redirect URI | https://<oa-api-domain>/api/auth/iam/callback |
| Token Endpoint Auth Method | client_secret_basic |
| Grant Types | authorization_code |
| Response Types | code |
| Scope | openid profile email |
| Require PKCE | true,只接受 S256 |
| Back-Channel Logout URI | https://<oa-api-domain>/api/auth/iam/backchannel-logout |
| Back-Channel Logout Session Required | true |
| Skip Consent | 按产品要求;内部系统可评估开启 |
根据当前 Web 环境文件可识别的地址,联调前应由后端同事和运维再次确认:
| 环境 | OA API 候选地址 | OA Web 候选地址 |
|---|---|---|
| 生产 | https://oain-api.ndd-x.cn |
https://oa-intl.chaoqing-i.com |
| 测试 | https://oain-api.dev.ndd-x.cn |
https://oa-intl-dev.chaoqing-i.com |
| 本地 | http://127.0.0.1:8000 |
http://localhost:9001 |
候选地址不能直接当作最终发布配置。Redirect URI 必须与实际环境逐字符一致,包括协议、域名、端口、路径和末尾斜杠。
生产 Back-Channel Logout URI 必须是 IAM 服务端能够访问的 HTTPS 地址,不能包含凭据或 fragment。本地环境无法接收服务器间回调时,应在公网测试环境完成统一下线验收。
4.2 安全交付参数
Section titled “4.2 安全交付参数”IAM 负责人通过 Secret 管理系统或其他安全渠道提供:
IAM_OIDC_ISSUER=https://iam.cq-i.cn/api/authIAM_OIDC_CLIENT_ID=<oa-client-id>IAM_OIDC_CLIENT_SECRET=<oa-client-secret>Client Secret 仅允许配置在 oacn-api,不得进入 oain-web、Git 仓库、群聊截图、日志或监控事件。
4.3 Claims 契约
Section titled “4.3 Claims 契约”| Claim | 用途 |
|---|---|
sub |
IAM 稳定用户标识;绑定后作为 OA 首要匹配键 |
sid |
IAM 会话标识;用于精确撤销对应 OA Sanctum Token |
wecom_user_id |
首次绑定时优先匹配 OA 的 wechat_work_userid |
email |
允许配置开启后作为首次绑定候选 |
email_verified |
只有严格布尔值 true 才允许邮箱首次绑定 |
app_role |
仅记录,不覆盖 OA Bouncer 角色 |
IAM 当前使用 RS256 签发 ID Token 和 Logout Token。OA 后端可保留 EdDSA/Ed25519 验签作为旧密钥兼容能力,但不得把它当作当前主算法。
5. oacn-api 代码改造要求
Section titled “5. oacn-api 代码改造要求”5.1 新增四个公开路由
Section titled “5.1 新增四个公开路由”以下路由不能放在 auth:sanctum 中间件后面:
Route::prefix('auth/iam')->group(function () { Route::get('start', [IamAuthController::class, 'start']); Route::get('callback', [IamAuthController::class, 'callback']); Route::post('exchange', [IamAuthController::class, 'exchange']); Route::post('backchannel-logout', [IamAuthController::class, 'backchannelLogout']);});| 方法与路径 | 用途 |
|---|---|
GET /api/auth/iam/start |
创建 state、nonce 和 PKCE,跳转 IAM |
GET /api/auth/iam/callback |
交换 Token、验证身份、绑定 OA 用户、签发一次性 ticket |
POST /api/auth/iam/exchange |
一次性 ticket 换 OA Sanctum Token |
POST /api/auth/iam/backchannel-logout |
验证 IAM Logout Token,按 sid 撤销 OA Token |
5.2 start 必须完成的行为
Section titled “5.2 start 必须完成的行为”- 校验 IAM 配置完整。
- 校验
return_to只能是 OA 站内路径。 - 使用加密安全随机数生成
state、nonce和code_verifier。 - 计算
code_challenge = BASE64URL(SHA256(code_verifier))。 - 以 state 为键,将 verifier、nonce、return_to 写入 Laravel Cache,建议有效期 300 秒。
- 整页 302 跳转 IAM Authorization Endpoint。
Authorization 请求至少包含:
client_idredirect_uriresponse_type=codescope=openid profile emailstatenoncecode_challengecode_challenge_method=S2565.3 callback 必须完成的行为
Section titled “5.3 callback 必须完成的行为”- 使用原子读取并删除方式消费 state;state 不存在、过期或重放立即失败。
- 服务端通过
client_secret_basic,使用 code 和原始 verifier 请求 Token Endpoint。 - 从 Discovery 获取
issuer、端点和jwks_uri,不在代码中猜测端点。 - 使用 JWKS 验证 ID Token 签名,并验证
iss、aud、必要时的azp、exp、iat、nonce、sub。 - 必须取得非空
sid,否则不能建立支持统一下线的 OA 会话。 - 使用 IAM Access Token 调用 UserInfo,并确认 UserInfo 的
sub与 ID Token 一致。 - 按第 7 节规则查找并绑定 OA 用户,检查用户本地状态。
- 生成不可预测、单次消费、建议 60 秒有效的一次性 ticket,仅缓存
user_id、iam_session_id和 return_to。 - 302 跳转 OA Web
/iam/callback,不得把 IAM Access Token、ID Token 或 OA Token 放入 URL。
5.4 exchange 必须完成的行为
Section titled “5.4 exchange 必须完成的行为”- 原子消费 ticket,防止并发重复兑换和重放。
- 再次检查 OA 用户存在且状态有效。
- 创建 OA Sanctum Token,名称建议为
iam-oidc。 - 将 IAM
sid写入该 Token 对应的personal_access_tokens.iam_session_id。 - 写入 OA 现有登录日志。
- 按第 3.3 节返回
data.token、token_type和 OA 本地角色。
5.5 backchannel-logout 必须完成的行为
Section titled “5.5 backchannel-logout 必须完成的行为”接收 IAM 表单请求中的 logout_token,完整验证:
- JWT 签名和允许的算法;
iss、aud、iat、exp;- 非空且未使用过的
jti; events包含 OIDC Back-Channel Logout 事件;- 包含
sid或sub,当前 OA 实现要求使用sid; - 不允许存在
nonce。
验证通过后,仅删除:
DELETE FROM personal_access_tokensWHERE iam_session_id = :sid;不能按用户删除其全部 OA Token,否则会误伤该用户在其他设备或 IAM 会话中的登录。jti 必须写入共享缓存防重放。
5.6 缓存与多实例要求
Section titled “5.6 缓存与多实例要求”state、ticket、JWKS/Discovery 缓存和 Logout Token jti 防重放都依赖 Laravel Cache。生产多实例部署不能使用本机 file 缓存,应使用 Redis 等所有实例共享且支持原子 pull/add 的缓存后端。
建议有效期:
| 数据 | 建议有效期 |
|---|---|
| OAuth state/nonce/verifier | 300 秒 |
| 一次性 ticket | 60 秒 |
| Discovery/JWKS | 3600 秒 |
Logout Token jti 防重放 |
至少覆盖 Token 可接受时间窗口 |
6. OA MySQL 迁移要求
Section titled “6. OA MySQL 迁移要求”以下 migration 由 OA 后端同事审查和执行,IAM 负责人不直接修改 OA MySQL。
6.1 users 表
Section titled “6.1 users 表”新增字段:
| 字段 | 类型 | 约束 | 用途 |
|---|---|---|---|
iam_subject |
varchar(255) |
nullable、unique | 保存 IAM sub |
iam_linked_at |
timestamp |
nullable | 首次绑定时间 |
iam_last_login_at |
timestamp |
nullable | 最近一次 IAM 登录时间 |
同时更新 User Model:
- 将三个字段加入允许赋值配置;
- 将两个时间字段加入 datetime cast;
- 不删除现有企业微信字段和角色关系。
6.2 personal_access_tokens 表
Section titled “6.2 personal_access_tokens 表”新增:
| 字段 | 类型 | 约束 | 用途 |
|---|---|---|---|
iam_session_id |
varchar(191) |
nullable、index | 保存 IAM sid,用于精确撤销 Token |
该字段不能设为 unique,因为同一 IAM 会话可能因重新兑换或业务需要产生多个 OA Token。
6.3 上线前数据检查
Section titled “6.3 上线前数据检查”在实际 OA 数据库执行只读检查,表名或状态值如与生产不一致需相应调整:
-- 企业微信 UserID 重复时不能自动决定绑定哪个 OA 用户SELECT wechat_work_userid, COUNT(*) AS totalFROM usersWHERE wechat_work_userid IS NOT NULL AND wechat_work_userid <> ''GROUP BY wechat_work_useridHAVING COUNT(*) > 1;
-- 邮箱重复或大小写归一后的冲突SELECT LOWER(email) AS normalized_email, COUNT(*) AS totalFROM usersWHERE email IS NOT NULL AND email <> ''GROUP BY LOWER(email)HAVING COUNT(*) > 1;
-- 核对现有状态值,确定哪些状态允许登录SELECT status, COUNT(*) AS totalFROM usersGROUP BY status;如果企业微信 UserID 存在重复,必须先由业务负责人确认并清洗,不能让代码静默选择第一条记录。
6.4 迁移执行与验收
Section titled “6.4 迁移执行与验收”php artisan migrate --pretendphp artisan migrate执行后检查:
SHOW COLUMNS FROM users LIKE 'iam_%';SHOW INDEX FROM users WHERE Key_name LIKE '%iam%';SHOW COLUMNS FROM personal_access_tokens LIKE 'iam_session_id';SHOW INDEX FROM personal_access_tokens WHERE Key_name LIKE '%iam%';迁移需同时准备可恢复方案。回滚前应确认是否已经产生 IAM 登录数据,避免直接删除仍在使用的绑定和 Token 关联字段。
7. OA 用户匹配与权限规则
Section titled “7. OA 用户匹配与权限规则”首次登录按以下顺序匹配,命中后写入 iam_subject:
users.iam_subject = sub:已绑定用户,直接使用。users.wechat_work_userid = wecom_user_id且iam_subject IS NULL:优先绑定现有企业微信身份。- 仅在
IAM_ALLOW_EMAIL_LINK=true时,用已验证邮箱匹配iam_subject IS NULL的用户。 - 均未命中则拒绝登录,提示联系管理员;不得自动创建 OA 用户。
邮箱绑定还必须满足:
email_verified必须是严格布尔值true;- 邮箱格式有效;
- 拒绝企业微信占位邮箱,例如
@wecom.invalid、@wecom.com; - 查询结果必须唯一;有歧义时拒绝绑定。
权限规则:
- OA 用户必须处于本地允许登录状态。
- OA 最终角色来自 Bouncer,例如
$user->getRoles()。 - IAM
app_role不写入或覆盖 OA 角色。 - IAM 管理端的应用准入与 OA 本地业务授权是两道独立检查,缺一不可。
8. oacn-api 环境变量
Section titled “8. oacn-api 环境变量”建议在各环境配置:
IAM_OIDC_ISSUER=https://iam.cq-i.cn/api/authIAM_OIDC_CLIENT_ID=IAM_OIDC_CLIENT_SECRET=IAM_OIDC_REDIRECT_URI=http://localhost:8000/api/auth/iam/callbackIAM_FRONTEND_CALLBACK=http://localhost:9001/iam/callbackIAM_POST_LOGOUT_REDIRECT_URI=http://localhost:9001/loginIAM_ALLOW_EMAIL_LINK=trueSANCTUM_TOKEN_EXPIRATION=300后端可继续提供 scope、HTTP timeout、Discovery/JWKS 缓存时间和端点覆盖等高级配置,但默认应通过 Discovery 获取标准端点。
特别注意:
- 不要把当前
.env.example中的DB_DATABASE直接覆盖到生产;必须保留实际 OA MySQL 数据库名称。 IAM_OIDC_REDIRECT_URI必须与 IAM Client 中登记的 Redirect URI 完全一致。IAM_FRONTEND_CALLBACK必须指向当前环境真实的oain-web地址。- Secret 只配置在后端运行环境。
- 如果保留 Ed25519 旧密钥兼容,PHP 运行环境需启用
ext-sodium;当前 IAM 主链路为 RS256。
9. 建议实施顺序
Section titled “9. 建议实施顺序”- OA 后端确认测试、生产的 OA API/Web 实际域名。
- IAM 负责人创建测试环境 OA Client,并安全交付 Client ID/Secret。
- OA 后端执行第 6.3 节的数据检查,处理重复身份。
- OA 后端合入代码和 migration,先运行
migrate --pretend,再迁移测试库。 - OA 后端配置共享缓存、OIDC 环境变量和 Sanctum 期限。
- OA Web 指向测试 API,完成登录、回调、角色和 return_to 联调。
- 验证 IAM Back-Channel Logout 能按
sid精确撤销 OA Token。 - 完成安全与异常用例后,再创建/核对生产 Client 并按变更窗口发布。
发布依赖顺序建议为:数据库 migration → oacn-api → oain-web 环境配置/发布。旧登录入口可暂时并存,因此无需为了本次接入立刻删除旧接口。
10. 当前工作区参考文件
Section titled “10. 当前工作区参考文件”oain-web
Section titled “oain-web”| 文件 | 作用 |
|---|---|
src/api/index.ts |
/auth/iam/start URL 和 ticket exchange 请求 |
src/pages/login/index.vue |
统一身份登录入口 |
src/pages/login/iamCallback.vue |
ticket 回调、兑换、用户和角色加载 |
src/router/index.ts |
/iam/callback 公开路由 |
src/utils/safeRedirect.ts |
return_to 安全校验 |
src/utils/loginRedirect.ts |
登录后统一跳转 |
src/utils/axios.ts |
OA Sanctum Bearer Token 与回调页 401 处理 |
src/utils/userInfo.ts |
Token、角色和用户信息存储 |
oacn-api
Section titled “oacn-api”| 文件 | 作用 |
|---|---|
routes/api.php |
IAM 四个公开路由 |
app/Http/Controllers/Api/IamAuthController.php |
登录、回调、ticket exchange、后通道退出 |
app/Repositories/IamOidcRepository.php |
Discovery、PKCE、Token、JWKS 和 JWT 校验 |
app/Services/IamUserMapper.php |
OA 用户匹配和首次绑定 |
app/Models/User.php |
IAM 绑定字段 |
config/services.php |
IAM OIDC 配置 |
config/sanctum.php |
OA Token 有效期 |
database/migrations/2026_08_28_000001_add_iam_fields_to_users_table.php |
OA 用户绑定字段 migration |
database/migrations/2026_08_28_000002_add_iam_session_id_to_personal_access_tokens_table.php |
IAM sid migration |
工作区中的这些文件是实现参考,不代表 migration 已在测试或生产 OA 数据库执行。后端同事必须以目标环境的 migration 记录和表结构为准。
11. 测试与验收
Section titled “11. 测试与验收”11.1 后端自动化测试
Section titled “11.1 后端自动化测试”php artisan test \ tests/Feature/IamAuthFeatureTest.php \ tests/Unit/IamOidcIdTokenTest.php \ tests/Unit/IamOidcPkceTest.php \ tests/Unit/IamReturnToValidationTest.php \ tests/Unit/IamUserMapperTest.php11.2 前端回归
Section titled “11.2 前端回归”在 oain-web 执行项目现有测试、类型检查和构建命令,并至少人工验证:
- 从登录页进入统一身份登录;
- callback URL 中 ticket 被及时清理;
data.token被保存,/api/auth/user能加载 OA 角色;- return_to 返回原站内页面;
- ticket 无效时不产生 OA 会话;
- callback 页面出现 401 时不会刷新丢失 ticket;
- 密码登录和旧企业微信登录未被本次改造意外破坏。
11.3 必须通过的安全用例
Section titled “11.3 必须通过的安全用例”- state、nonce、ticket 和 Logout Token
jti重放均失败; - PKCE 只接受 S256;
- 错误
iss、aud、azp、签名、nonce、过期 Token 均失败; - UserInfo
sub与 ID Token 不一致时失败; - 未验证邮箱、占位邮箱、重复邮箱和重复企业微信 UserID 均不能自动绑定;
- 无 OA 用户、停用用户或无 OA 角色不能进入 OA;
- 恶意 return_to 不能跳出 OA 站点;
- IAM 退出后,只撤销同一
sid的 OA Token,其他会话继续有效; - 日志中不出现 Client Secret、Authorization Code、ID Token、Access Token、ticket 或完整 OA Token。
12. 后端交付清单
Section titled “12. 后端交付清单”- 四个 IAM 路由已合入
oacn-api - Authorization Code + PKCE S256 已启用
- ID Token、UserInfo 和 Logout Token 校验完整
- 用户按
sub→ 企业微信 UserID → 已验证邮箱的顺序绑定 - 未匹配用户不会自动创建
- OA 状态和 Bouncer 角色仍为本地授权依据
-
users三个 IAM 字段已迁移并核对唯一索引 -
personal_access_tokens.iam_session_id已迁移并建立普通索引 - ticket 一次性、短时有效,响应包含
data.token - 生产使用共享缓存
- Back-Channel Logout 已按
sid精确撤销 Token - 测试环境全链路和安全用例通过
- Secret 未进入前端、仓库和日志
- 生产域名、Redirect URI 和 Back-Channel Logout URI 已最终确认