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_usernamenameemail,然后按自己的逻辑创建或登录用户。

环境和目标

示例环境如下,域名和密钥均已脱敏:

组件 角色
DevOps 前端 提供飞书登录入口和 DB 平台入口
DevOps 后端 IAM、飞书 OAuth、准入审核、OIDC Broker
飞书 OAuth 只负责用户身份认证
Yearning DB 审核平台,作为 OIDC Client
LDAP 可选账号存在性校验

目标不是让 Yearning 直接接飞书,而是让 DevOps 变成一个“公司内部身份网关”:

飞书负责证明“你是谁”
DevOps负责判断“你能不能进”
Yearning只消费标准OIDC结果

整体链路如下:

Yearning + DevOps OIDC Broker总览

飞书登录 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 登录过,就不需要再跳一次飞书授权页。

流程图如下:

DevOps OIDC Broker登录时序

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"

这里有几个容易配错的点:

  • AuthUrlTokenUrlUserUrl 都指向 DevOps,不是飞书
  • RedirectUrL 是 Yearning 自己的 OIDC 回调地址
  • UserNameKey 应该使用 DevOps Broker 输出的用户名字段,例如 preferred_username
  • ClientSecret 必须和 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标准登录用户
posted @ 2026-07-16 16:57  Hello_worlds  阅读(6)  评论(0)    收藏  举报