每个业务系统,不该都重造一遍认证
团队里的业务系统各自为政:有的自签 AKSK,有的发 API Key,有的完全没有认证。 想统一接入公司身份体系,每家都要去研究一遍身份源的私有 OAuth —— 重复、易错、难维护。
没有 Vouch
- 每个平台各自对接身份源 OAuth,协议细节重复踩坑
- 自签 AKSK / 静态 API Key,泄露即裸奔
- CLI、Web、服务间调用各一套玩法,用户体验割裂
- 上游身份源一旦要换,所有业务方跟着改
有了 Vouch
- 身份源对接只做一次,脏活集中在 Vouch
- 业务方只见到 SDK 方法,几行代码完成接入
- CLI / Web / 无界面环境统一 SSO 体验
- 多身份源可扩展、可替换,业务方零改动
工作原理:三步,一次授权全程通用
Vouch 不是 IdP —— 飞书、玻尔才是身份源,用户账号始终归身份源管。 Vouch 是中间的联邦 / 代理层,把身份源的私有协议「翻译」成业界标准 OIDC。
统一授权
用户在你的应用里触发登录,用飞书或玻尔账号完成授权。有界面走授权码(Auth Code + PKCE),无界面环境自动切设备码(Device Code)。
Vouch 签发 JWT
Vouch 向身份源核验用户身份后,用 Ed25519 签出一张短期标准 JWT,带上 sub / name / email 与 audience。
业务离线验签
你的服务端启动时拉一次公钥(JWKS),之后每个请求本地验签、不再回调 Vouch,合法即从 JWT 读出用户身份。
客户端侧 取 token
你的应用代表用户拿到一个 JWT。SDK 自动适配运行环境:有图形界面打开浏览器走授权码; SSH / CI / 容器等无界面环境自动切设备码 —— 用户在另一台设备上打开链接、输入 6 位短码完成授权。 token 本地缓存、过期自动刷新,用户无感。
服务端侧 验 token
被调用的服务校验 JWT 是否合法,合法就放行并读出用户身份。全程离线验签, 不需要会话、不存密码、不逐请求回调 Vouch;公钥轮换由 SDK 自动处理。 token 只对指定 audience 有效,发给别的服务的 token 自动被拒。
特性一览
认证该有的样子:标准、安全、对业务方透明。
标准 OIDC 接口
Discovery + JWKS、Auth Code + PKCE、Device Code、Refresh Token。任何标准 OIDC 客户端都能对话,不被私有协议锁定。
离线验签
服务端拉一次 JWKS 公钥即可本地验签,不依赖 Vouch 可用性。Ed25519 签名,验签开销极小。
Audience 隔离
token 只对指定的服务有效。调用方与被调方共享同一份 audience 契约,SDK 自动强校验,跨服务冒用直接拒。
CLI / Web / 无界面全覆盖
浏览器授权码、无界面设备码(6 位短码、5 分钟过期)、纯前端 SPA(Service Worker 存 token,不落 localStorage)。
委派凭证
给 Agent Sandbox 等无人值守环境的「受限替身」:1 小时存活上限、audience 有界、可撤销、带 act claim 可审计。
多身份源,上游可替换
已支持飞书与玻尔两种身份源;IdentityProvider 协议已抽象,未来接钉钉 / 企微 / AD,业务方代码零改动。
SDK 接入:两侧各几行代码
四种语言 SDK,覆盖「取 token」与「验 token」两侧。包发布在内网 Package Registry —— 内网源地址与只读凭证,联系 Vouch 团队获取。
pip install vouch --index-url <内网 PyPI 源> # 客户端侧
pip install 'vouch[fastapi]' --index-url <内网 PyPI 源> # 服务端侧(flask / django 同理)
import vouch
vouch.configure(client_id="acme-cli", audience="billing")
# 自动带 Authorization: Bearer <JWT>;
# 首次无缓存自动登录,之后复用缓存、自动刷新
with vouch.authed_client() as client:
resp = client.get("https://billing/items")
from fastapi import Depends, FastAPI
from vouch.fastapi import requires_auth
from vouch.server.user import User
app = FastAPI()
@app.get("/me")
def me(user: User = Depends(requires_auth)) -> dict:
return {"sub": user.sub, "name": user.name,
"email": user.email}
服务端 audience 来自环境变量 VOUCH_AUDIENCE;无 token / token 非法 → 401,audience 不匹配 → 403。
npm install @vouch/sdk # 客户端侧 + 服务端侧,一个包
npm install express # 仅 Express 服务端额外需要(peer dependency)
import { configure, authedFetch } from "@vouch/sdk";
configure({ clientId: "acme-cli", audience: "billing" });
// 自动带 Authorization: Bearer <JWT>;
// 首次无缓存自动登录,之后复用缓存、自动刷新
const fetch = await authedFetch();
const res = await fetch("https://billing/items");
import express from "express";
import { requiresAuth, bootEager } from "@vouch/sdk/express";
const app = express();
bootEager(); // 启动即校验配置
app.get("/me", requiresAuth(), (req, res) => {
res.json(req.user); // 验过签的用户身份
});
纯浏览器 SPA 用 @vouch/sdk/browser:token 存 Service Worker 的 IndexedDB,不落 localStorage。非 Express 框架用通用 Validator 自行接入。
# 模块路径与访问凭证,联系 Vouch 团队获取
go get <内网 Go module 路径>@sdk/go/v0.2.0
cfg, _ := client.LoadConfig()
_ = client.Login(ctx, cfg, client.LoginOpts{
Audience: "billing", ClientID: "my-cli",
})
token, err := client.GetToken(ctx, cfg, "billing", nil)
validator, err := verifier.ValidatorFromEnv() // 读 VOUCH_AUDIENCE
r := chi.NewRouter()
r.Use(validator.Middleware())
r.Get("/me", func(w http.ResponseWriter, req *http.Request) {
u, _ := verifier.UserFromContext(req.Context())
fmt.Fprintf(w, "hello %s", u.Sub)
})
SDK 按角色拆成 client / verifier / exchange 三个包,只引入你的程序扮演的那个角色。
[dependencies]
vouch = { git = "<内网 Git 地址>", package = "vouch",
tag = "sdk/rust/v0.2.0", features = ["server"] }
use vouch::client::{login, get_token, Config, LoginOpts};
let cfg = Config::load()?;
login(&cfg, LoginOpts {
audience: "billing".into(),
client_id: "my-cli".into(),
..Default::default()
}).await?;
let token = get_token(&cfg, "billing", None).await?;
use vouch::server::{Validator, VouchUser};
async fn me(VouchUser(user): VouchUser) -> String {
format!("you are {}", user.sub)
}
let validator = Validator::from_env()?; // 读 VOUCH_AUDIENCE
let app = Router::new()
.route("/me", get(me))
.with_state(validator);
cargo feature 按需开启:client / server / axum。客户端与 vouch-helper 共享同一份磁盘 token 存储。
接入流程
不需要控制台、不需要自助注册 —— 一次 onboard,两侧各几行代码。
-
1
联系 Vouch 团队 onboard
拿到一个
client_id(业务标识,接入门禁)和一个或多个audience(服务标识,调用方与被调方共享的契约)。我们分配,你不用自己编。 -
2
安装 SDK
从内网 Package Registry 安装对应语言的 SDK(Python / TypeScript / Go / Rust)。源地址与只读凭证随 onboard 一并提供。
-
3
两侧各写几行
客户端
configure()写入 client_id / audience;服务端配VOUCH_AUDIENCE环境变量并挂一个依赖 / 中间件。完成。
常见问题
Vouch 是一个 IdP 吗?
不是。飞书、玻尔才是身份源(IdP),用户账号始终归身份源管。Vouch 是中间的联邦 / 代理层:把身份源的私有认证协议翻译成标准 OIDC,并把每个业务方都要做的对接脏活集中到一处。
Vouch 管权限和角色吗?
不管。Vouch 只做认证(你是谁),不做授权(你能干什么)。角色 / 权限归属业务系统自身 —— 你拿到验过签的用户身份后,按自己的授权逻辑处理。
服务端验签需要每次调用 Vouch 吗?
不需要。服务端启动时拉一次 JWKS 公钥,之后每个请求本地离线验签,不再回调 Vouch。公钥轮换由 SDK 自动处理。
token 泄露了怎么办?
多层兜底:access token 本身短 TTL;refresh 时定期回查身份源账号状态(离职 / 禁用即续期失败);Agent Sandbox 用的委派凭证 1 小时存活上限,且所有者可随时显式撤销、管理员可一键撤销某用户的全部委派。
支持纯前端 SPA 吗?
支持。@vouch/sdk/browser 基于 Service Worker:token 存在 SW 的 IndexedDB 里,页面通过 postMessage 按需获取,不在 localStorage 落地。Chrome / Firefox / Safari / Edge 均可。
未来能接钉钉 / 企微吗?
可以。当前已支持飞书与玻尔两种身份源,且 IdentityProvider 协议已抽象:钉钉、企微、AD 等是规划中的后续实现,新增上游对业务方零改动。