Yearning 接入 OIDC:用 DevOps 做飞书 OAuth Broker 实现统一登录
Yearning 接入 OIDC:用 DevOps 做飞书 OAuth Broker 实现统一登录
很多内部系统接统一登录时容易走偏:业务系统直接接飞书 OAuth,每个系统各写一套 code 换 token、用户映射、准入审核和权限判断。短期能登录,长期会变成多套账号入口并存,权限边界也分散。
更稳的做法是把飞书只当身份入口,把真正的账号、角色、权限和准入规则收敛到一个内部平台。本文用一个脱敏后的实现说明这条链路:
- DevOps 平台自己接飞书 OAuth,完成用户身份识别
- DevOps 内部 IAM 决定用户能不能登录平台、能不能进入 DB 平台
- Yearning 不直接接飞书,只作为标准 OIDC Client 接 DevOps
- DevOps 对 Yearning 暴露一组最小 OIDC Broker 接口
最后效果是:用户先登录 DevOps,再进入 DB 平台时不需要再理解飞书、LDAP、准入审核这些细节。Yearning 只拿到标准的 preferred_username、name、email,然后按自己的逻辑创建或登录用户。
环境和目标
示例环境如下,域名和密钥均已脱敏:
| 组件 | 角色 |
|---|---|
| DevOps 前端 | 提供飞书登录入口和 DB 平台入口 |
| DevOps 后端 | IAM、飞书 OAuth、准入审核、OIDC Broker |
| 飞书 OAuth | 只负责用户身份认证 |
| Yearning | DB 审核平台,作为 OIDC Client |
| LDAP | 可选账号存在性校验 |
目标不是让 Yearning 直接接飞书,而是让 DevOps 变成一个“公司内部身份网关”:
飞书负责证明“你是谁”
DevOps负责判断“你能不能进”
Yearning只消费标准OIDC结果
整体链路如下:

飞书登录 DevOps 是怎么走的
先看最基础的 DevOps 飞书登录。它本质不是“飞书控制 DevOps 权限”,而是“飞书给 DevOps 提供身份信息”。
链路如下:
1.前端点击“飞书登录”
↓
2.前端向DevOps后端请求飞书授权地址
↓
3.浏览器跳转到飞书OAuth页面
↓
4.飞书授权完成后回调DevOps后端
↓
5.DevOps用code换飞书用户信息
↓
6.DevOps做准入审核和IAM用户映射
↓
7.DevOps生成自己的session token
↓
8.前端保存DevOps token,后续API带Authorization: Bearer <token>
关键点在第 6 步。飞书返回的是身份材料,例如 open_id、邮箱、姓名、头像、部门信息等。DevOps 不能因为飞书认证成功就直接放行,还要做自己的判断:
- 这个飞书用户是否在允许的邮箱域名内
- 是否已通过 DevOps 登录准入审核
- 是否能映射到内部 IAM 用户名
- 是否需要自动创建或更新 IAM 用户资料
- 该用户拥有哪些角色和权限
所以 DevOps 的权限控制仍然在自己的 IAM 里。飞书只是登录入口,不是权限系统。
一个简化后的后端逻辑是:
def feishu_callback(code, state):
oauth_user = feishu.exchange_code(code)
approval = upsert_login_approval(oauth_user)
if approval.status != "approved":
return redirect_frontend(status=approval.status)
iam_user = ensure_user_with_role(
username=approval.username,
role="staff",
display_name=approval.name,
email=approval.email,
department=approval.department,
)
ticket = create_one_time_login_ticket(approval)
return redirect_frontend(status="approved", ticket=ticket)
前端拿到一次性 ticket 后,再换成 DevOps 自己的 session token。后续所有 DevOps API 都走:
Authorization: Bearer <devops-session-token>
这一步把外部 OAuth 和内部权限体系分开了。
飞书登录准入和一次性 ticket
飞书登录 DevOps 还有一个容易被忽略的细节:OAuth 回调不能直接把长期登录态放进 URL。更稳的做法是让后端生成一个短期、一次性 ticket,前端再用这个 ticket 换 DevOps 自己的 session token。
这里至少有两类 ticket:
| 类型 | 什么时候生成 | 用途 | 建议有效期 |
|---|---|---|---|
| pending ticket | 飞书认证成功,但 DevOps 准入还在待审核 | 前端轮询审核状态 | 可长一些,例如 60 分钟 |
| approved ticket | DevOps 准入已通过 | 换取 DevOps session token | 短有效期,例如 5 分钟 |
approved ticket 的风险更高,因为它一旦被换票成功,用户就能拿到 DevOps 登录态。所以它必须短有效期、一次性消费。pending ticket 只用于等待审核和查询状态,有效期可以稍长,否则管理员稍晚审核,用户页面就会很快失效。
前端回调页不要只按 URL 上的 status 做一次性判断。真实链路里会出现竞态:
1.用户完成飞书授权
2.后端生成 ticket 并回到前端
3.前端立刻用 ticket 换 DevOps token
4.管理员几秒后才点审核通过
如果第 3 步发生时准入还没变成 approved,后端可能返回 pending。这时即使 URL 看起来是 status=approved,前端也不能卡死在“正在完成登录”,而应该切入轮询:
ticket接口返回 pending
↓
前端每5秒继续查询
↓
审核通过后同一个ticket换DevOps token
↓
换票成功后标记ticket已消费
过期也要按用户体验处理。不要只显示“票据无效或已过期”这类后端错误。更好的页面是:
标题: 登录申请已超时
主按钮: 重新飞书登录
次按钮: 返回登录页
用户点“重新飞书登录”后重新走 OAuth。如果准入已经审核通过,新的回调会直接生成 approved ticket 并完成登录。
为什么 DB 平台不直接接飞书
Yearning 支持 OIDC,看起来可以直接把飞书 OAuth 地址填进去。但直接接飞书有几个问题:
1.飞书 OpenAPI 返回结构不一定等同标准 OIDC userinfo
Yearning 这类 OIDC Client 通常期望 userinfo 返回顶层 claims:
{
"preferred_username": "zhangsan",
"name": "张三",
"email": "zhangsan@example.com"
}
而飞书 OAuth 更偏平台 API 结构,字段可能包在 data 里,字段名和含义也不是完全按标准 OIDC Provider 设计。
2.准入逻辑不应该散落在 DB 平台
DB 平台登录要额外判断:
- 是否有 DB 平台登录权限
- 是否存在 LDAP 账号
- 是否已通过 DevOps 飞书登录准入
- 是否被禁用或撤权
这些逻辑如果写进 Yearning,后续别的系统也要接时还会再写一遍。
3.账号治理要集中
DevOps 已经有 IAM 用户、角色和权限。让 DevOps 统一做判断,可以让 DB 平台、监控平台、发布平台后续都复用同一套身份和权限模型。
因此更合适的结构是:DevOps 做一个最小 OIDC Broker。
DevOps 作为 OIDC Broker
Broker 的职责很清楚:对 Yearning 看起来像一个标准 OIDC Provider,但内部实际可以复用 DevOps 登录态、飞书 OAuth、准入审核和权限系统。
对 Yearning 暴露的接口大概是:
GET /api/auth/oidc/yearning/auth
POST /api/auth/oidc/yearning/token
GET /api/auth/oidc/yearning/userinfo
同时 DevOps 前端内部还会用两个辅助接口:
GET /api/auth/oidc/yearning/eligibility
GET /api/auth/oidc/yearning/entry
eligibility 用来告诉前端当前用户是否满足登录 DB 平台的条件。它通常检查:
1.是否已登录DevOps
2.是否有 yearning.login 权限
3.是否存在 LDAP 账号
entry 用于已登录 DevOps 的用户直接生成 Yearning 回调地址。用户已经在 DevOps 登录过,就不需要再跳一次飞书授权页。
流程图如下:

Yearning 侧怎么配置
Yearning 侧只需要按标准 OIDC Client 配置。示例:
[Oidc]
Enable = true
ClientId = "yearning"
ClientSecret = "<client-secret>"
Scope = "openid profile email"
AuthUrl = "https://devops.example.com/api/auth/oidc/yearning/auth"
TokenUrl = "https://devops.example.com/api/auth/oidc/yearning/token"
UserUrl = "https://devops.example.com/api/auth/oidc/yearning/userinfo"
RedirectUrL = "https://db.example.com/oidc/_token-login"
UserNameKey = "preferred_username"
RealNameKey = "name"
EmailKey = "email"
SessionKey = "session_state"
这里有几个容易配错的点:
AuthUrl、TokenUrl、UserUrl都指向 DevOps,不是飞书RedirectUrL是 Yearning 自己的 OIDC 回调地址UserNameKey应该使用 DevOps Broker 输出的用户名字段,例如preferred_usernameClientSecret必须和 DevOps Broker 里的 client 配置一致
Yearning 不需要知道飞书的 App ID、App Secret,也不需要知道 DevOps 内部 IAM 表结构。
Broker 内部怎么发 code 和 token
OIDC Broker 至少要维护两类短期数据:
| 数据 | 用途 | 建议有效期 |
|---|---|---|
| authorization code | 一次性授权码,给 Yearning 回调用 | 约 5 分钟 |
| access token | Yearning 调 userinfo 使用 | 约 30 分钟 |
简化后的授权码结构:
client_id
code
redirect_uri
scope
state
username
name
email
expires_at
consumed_at
简化后的 token 结构:
client_id
token
username
name
email
scope
expires_at
revoked_at
code 必须只能消费一次。交换 token 时要同时校验:
client_id正确
client_secret正确
redirect_uri匹配
code未过期
code未被消费
一个简化后的 token 交换逻辑:
def exchange_code_for_token(client_id, client_secret, code, redirect_uri):
client = get_client(client_id)
assert_secret(client, client_secret)
assert_redirect_uri(client, redirect_uri)
auth_code = find_unconsumed_code(
client_id=client_id,
code=code,
redirect_uri=redirect_uri,
)
if not auth_code:
raise InvalidGrant()
token = create_access_token(
client_id=client_id,
username=auth_code.username,
name=auth_code.name,
email=auth_code.email,
scope=auth_code.scope,
)
mark_code_consumed(auth_code)
return token
userinfo 返回给 Yearning 的内容保持简单:
{
"sub": "zhangsan",
"preferred_username": "zhangsan",
"name": "张三",
"email": "zhangsan@example.com"
}
不要把内部角色、部门、权限、审批备注一股脑塞进 userinfo。Yearning 登录只需要身份 claims,权限仍应该在 DevOps 内部判断。
两条登录路径
这个设计里有两条路径。
第一条是推荐路径:用户从 DevOps 菜单进入 DB 平台。
用户已登录 DevOps
↓
前端调用 /oidc/yearning/eligibility
↓
DevOps 检查 yearning.login 和 LDAP
↓
前端调用 /oidc/yearning/entry
↓
DevOps 直接生成 code
↓
前端 iframe 或新窗口打开 Yearning 回调地址
↓
Yearning 用 code 换 token,再取 userinfo
第二条是兼容路径:用户从 Yearning 原生 OIDC 登录入口进入。
Yearning 跳到 /oidc/yearning/auth
↓
DevOps 检查是否已有登录态
↓
没有登录态则跳飞书 OAuth
↓
飞书回调 DevOps
↓
DevOps 检查准入状态
↓
通过后生成 code 回 Yearning
第一条路径用户体验更好,因为 DevOps 已登录时不需要再看到飞书授权页。第二条路径保留了标准 OIDC Client 的完整授权码流程。
权限边界
这套设计里要把三层权限说清楚。
第一层:飞书身份认证。
飞书只回答“这个浏览器背后的用户是谁”。它不决定用户能不能进 DevOps,也不决定用户能不能进 DB 平台。
第二层:DevOps 登录准入。
DevOps 根据飞书用户信息维护准入状态:
pending
approved
rejected
disabled
只有 approved 用户才能拿到完整平台登录能力。
第三层:DevOps IAM 权限。
DB 平台入口依赖单独权限,例如:
yearning.login
菜单展示、入口接口和 Broker 发 code 都应该校验这个权限。不要只在前端隐藏菜单,后端也必须拒绝没有权限的用户。
如果还要求用户必须有 LDAP 账号,则在发 code 前再做一次 LDAP 查询。这样能避免一个只有飞书身份、但没有内部账号基础设施的用户被放进 DB 平台。
为什么这个方案可维护
这个方案的核心收益不是“少写几行登录代码”,而是边界清晰:
- 飞书只做身份认证
- DevOps 管账号、准入、角色、权限
- Yearning 只消费标准 OIDC
- LDAP 只做账号存在性校验
这样以后如果还要接入别的平台,可以继续复用 Broker 思路:
新系统 = 标准 OIDC Client
DevOps = 统一 OIDC Broker
飞书/LDAP/IAM = DevOps 内部实现细节
业务系统不需要知道飞书 OpenAPI 的响应结构,也不需要关心公司内部审核流程。
快速参考
Yearning OIDC 配置模板:
[Oidc]
Enable = true
ClientId = "yearning"
ClientSecret = "<client-secret>"
Scope = "openid profile email"
AuthUrl = "https://devops.example.com/api/auth/oidc/yearning/auth"
TokenUrl = "https://devops.example.com/api/auth/oidc/yearning/token"
UserUrl = "https://devops.example.com/api/auth/oidc/yearning/userinfo"
RedirectUrL = "https://db.example.com/oidc/_token-login"
UserNameKey = "preferred_username"
RealNameKey = "name"
EmailKey = "email"
SessionKey = "session_state"
Broker 最小接口:
GET /api/auth/oidc/yearning/auth
POST /api/auth/oidc/yearning/token
GET /api/auth/oidc/yearning/userinfo
DevOps 内部入口:
GET /api/auth/oidc/yearning/eligibility
GET /api/auth/oidc/yearning/entry
权限检查建议:
1.飞书 OAuth 只做身份认证
2.DevOps 登录必须有准入状态
3.DB 平台入口必须有 yearning.login 权限
4.Broker 发 code 前必须后端再次校验权限
5.如依赖 LDAP,发 code 前检查 LDAP 账号存在
6.code 一次性消费,token 短有效期
7.userinfo 只返回身份 claims,不要泄露内部权限细节
飞书登录准入 ticket:
pending ticket: 用户已完成飞书授权,但准入待审核;用于前端轮询,有效期可稍长,例如60分钟
approved ticket: 准入已通过;用于换DevOps session token,有效期要短,例如5分钟
两类ticket都要有expires_at;换票成功后必须写consumed_at
前端遇到ticket接口返回pending时继续轮询,不要卡死在approved回调页
ticket过期时提供“重新飞书登录”,不要只展示后端错误
一句话总结:
飞书证明用户是谁,DevOps决定用户能去哪,Yearning只按OIDC标准登录用户

浙公网安备 33010602011771号