林中通行证 · OAuth 2.0 / OIDC 对接文档

林中通行证是林中科技的统一身份认证(SSO)与开放授权平台。第三方应用通过标准 OAuth 2.0 授权码模式 获取用户授权,通过 OIDC 获取签名的身份令牌(IdToken,RS256)。本文档按客户端类型分为两条对接路线,请先完成选型。

Issuer(签发方)https://passport.lztech.top
协议能力OAuth 2.0 授权码模式 · OIDC(IdToken RS256)· PKCE(S256)· Token Revocation(RFC 7009)
发现端点GET /.well-known/openid-configuration
令牌有效期授权码 5 分钟(一次性)· access_token 30 分钟 · refresh_token 14 天(仅机密客户端)
强制 HTTPS所有 OAuth 接口仅接受 HTTPS 请求,公网 HTTP 直接拒绝(403)

目录

① 客户端选型

创建应用时必须选择客户端类型,两类客户端的对接方式不同。核心判断:你的应用能否在服务端保管一个秘密。

对比项机密客户端(confidential)公开客户端(public)
适用形态有自己服务端的 Web 应用、后端服务SPA 单页应用、移动 App、小程序、纯前端应用
ClientSecret有,只能存放在服务端没有,也不允许提交任何 Secret
PKCE可选(建议启用)强制(不带或 method 非 S256 直接拒绝)
refresh_token发放(14 天,轮换机制)不发放;access 到期后重新走一次授权(已授权过的用户静默发码,无感)
token 请求认证HTTP Basic 或表单 client_id + client_secret仅表单 client_id(发 Secret 反而被拒绝)
token 请求位置必须由服务端调用前端可直接调用
不确定怎么选?只要你的应用有可信服务端,就选机密客户端(能力更全);纯前端无法安全保管 Secret,一律选公开客户端。

② 接入准备(两种客户端通用)

1. 注册并创建应用

①在 开放平台 使用通行证账号登录,提交开发者入驻申请,等待平台审核
②审核通过后创建应用:填写名称、描述、回调地址,选择客户端类型,勾选 scope
③应用审核通过后获得 client_id;机密客户端可查看/轮换 client_secret(仅创建或轮换时展示一次,平台只存哈希)

2. 回调地址(redirect_uri)规范

·必须为完整的 http(s):// URL,可登记多条
·授权请求中的 redirect_uri 与登记记录精确匹配(协议、域名、端口、路径、query 全部一致),无通配符
·生产环境必须 HTTPS;本地调试允许 http://localhost:端口
·修改回调地址会导致已签发令牌全部撤销,请谨慎操作

3. scope 说明

scope用户授权页显示返回内容
openid确认你的身份标识(用户ID)userinfo 的 sub;签发 IdToken
profile获取你的昵称nickname
email获取你的注册邮箱email
avatar获取你的头像picture(用户未设置头像时字段缺省)
请求的 scope 必须是应用登记范围的子集,超出报 invalid_scope。应用新增 scope 后,已授权用户需重新弹窗授权。

③ 机密客户端路线(有服务端)

适用于服务端渲染站点、后端 API 应用。完整流程:构造授权跳转 → 用户授权回调 → 服务端换令牌 → 调 userinfo。

A1 · 构造授权跳转(浏览器 302 跳转)

GET https://passport.lztech.top/oauth/authorize
    ?response_type=code
    &client_id=你的client_id
    &redirect_uri=https%3A%2F%2Fyour.app%2Fcallback
    &scope=openid%20profile%20email
    &state=随机不可预测字符串          ← 防 CSRF,回调时原样带回,必须校验
    &nonce=随机字符串                 ← 可选,会绑定进 IdToken
    &code_challenge=PKCE_S256摘要     ← 可选,建议启用
    &code_challenge_method=S256
    &prompt=select_account             ← 可选,强制弹出账号选择页

A2 · 回调处理(服务端)

GET https://your.app/callback?code=授权码&state=原样返回的state

校验顺序:
1. state 与你发起时保存的完全一致(不一致立即中止,防 CSRF)
2. code 在 5 分钟内且未被使用过(一次性,重放会连带撤销已发令牌对)

A3 · code 换令牌(仅服务端调用)

# 方式一:HTTP Basic(推荐)
curl -X POST https://passport.lztech.top/oauth/token \
  -u "你的client_id:你的client_secret" \
  -d grant_type=authorization_code \
  -d code=收到的code \
  -d redirect_uri=https://your.app/callback

# 方式二:表单字段(client_secret_post)
#   追加 -d client_id=... -d client_secret=...

# 响应
{
  "access_token": "…",
  "token_type": "Bearer",
  "expires_in": 1800,
  "refresh_token": "…",
  "scope": "openid profile email",
  "id_token": "eyJhbGciOiJSUzI1NiIs…"
}

A4 · 刷新令牌(rotation 轮换)

curl -X POST https://passport.lztech.top/oauth/token \
  -u "你的client_id:你的client_secret" \
  -d grant_type=refresh_token \
  -d refresh_token=当前有效的refresh_token

# 响应为全新的一对 access_token + refresh_token;
# 旧 refresh_token 立即作废,请原子性地替换存储(先写新值再删旧值)。
# 使用已作废的旧 token 刷新 → invalid_grant,且该用户此应用全部令牌被撤销。

A5 · 撤销令牌

# 用户解绑 / 登出你的应用时调用;撤销该 token 所在令牌对(access + refresh 一起失效)
curl -X POST https://passport.lztech.top/oauth/revoke \
  -u "你的client_id:你的client_secret" \
  -d token=要撤销的token

# 响应恒为 200 {"ok":true}(RFC 7009,无需区分 token 是否存在)

A6 · 获取用户信息

curl https://passport.lztech.top/oauth/userinfo \
  -H "Authorization: Bearer {access_token}"

# scope=openid                  → { "sub": "lz-1" }
# scope=openid profile          → { "sub": "lz-1", "nickname": "张三" }
# scope=openid profile email    → { "sub": "lz-1", "nickname": "张三", "email": "z@example.com" }
# scope=openid profile avatar   → { "sub": "lz-1", "nickname": "张三", "picture": "https://img.lztech.top/api/img/…" }
账号关联规范:请以 sub(形如 lz-1,全局唯一且永不改变)作为你系统中的用户主键;nickname / email 仅作展示,可能被用户修改。

A7 · 整户撤销(解绑场景)

# 用户在你的应用解绑账号时调用:撤销该用户在本应用的全部令牌,并删除授权关系
# (用户在通行证「授权管理」中该应用的记录同步消失,下次使用需重新走授权确认页)
curl -X POST https://passport.lztech.top/api/oauth/revoke-user \
  -u "你的client_id:你的client_secret" \
  -d sub=lz-123          ← 该用户的 sub(从 userinfo / IdToken 获取)

# 响应:{ "ok": true, "revoked": 3 }    revoked = 本次吊销的令牌数

# 错误:401 invalid_client(凭证错误)· 400 invalid_request(sub 缺失或格式错误)
#      404 not_found(用户不存在)· 429 rate_limited(30 次/分钟/IP,请退避重试)
A5 与 A7 的区别:A5(/oauth/revoke)只吊销单个令牌对,授权关系保留,用户可静默重新授权;A7(/api/oauth/revoke-user)撤销该用户全部令牌并删除授权关系。登出场景用 A5 即可,仅解绑 / 注销等终止账号关联的场景使用 A7。

④ 公开客户端路线(SPA / App / 小程序)

没有服务端秘密的应用。核心机制是 PKCE:用「验证器—挑战」替代 Secret,授权码即使被拦截也无法换出令牌。

B1 · 生成 PKCE 验证器(首次授权前,本地执行)

// 浏览器 / 小程序生成示例
const verifier = b64url(crypto.getRandomValues(new Uint8Array(32))); // 43 字符
const challenge = b64url(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)));

function b64url(buf) {                     // Buffer → base64url(无 +/= 填充)
  return btoa(String.fromCharCode(...new Uint8Array(buf)))
    .replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'');
}
// ⚠️ verifier 必须保存在内存 / sessionStorage 中,token 交换时要提交原文

B2 · 构造授权跳转(强制 PKCE)

GET https://passport.lztech.top/oauth/authorize
    ?response_type=code
    &client_id=你的client_id
    &redirect_uri=https%3A%2F%2Fyour.app%2Fcallback
    &scope=openid%20profile%20email
    &state=随机不可预测字符串
    &nonce=随机字符串                ← 建议 OIDC 场景必带
    &code_challenge=上一步算出的challenge
    &code_challenge_method=S256      ← 缺失或非 S256 直接被拒绝

B3 · 回调处理(前端)

GET https://your.app/callback#或?code=授权码&state=原样返回的state

校验:state 与发起前保存在 sessionStorage 的值完全一致,不一致立即中止。

B4 · code 换令牌(前端直调,无 Secret)

curl -X POST https://passport.lztech.top/oauth/token \
  -d grant_type=authorization_code \
  -d client_id=你的client_id \
  -d code=收到的code \
  -d redirect_uri=https://your.app/callback \
  -d code_verifier=本地保存的verifier原文

# 响应(注意:没有 refresh_token 字段)
{
  "access_token": "…",
  "token_type": "Bearer",
  "expires_in": 1800,
  "scope": "openid profile email",
  "id_token": "eyJhbGciOiJSUzI1NiIs…"
}

# 若提交 code_verifier 缺失或算出的 challenge 对不上 → invalid_grant (PKCE verification failed)
# 公开客户端提交 client_secret → invalid_client(公开客户端禁止持有 Secret)

B5 · access_token 到期后的续期

公开客户端没有 refresh_token。access_token 30 分钟到期后,重新走一遍 B2 → B4。该用户此前已授权过你的应用时,平台会静默发码(不弹授权页,若已有有效登录态甚至不会看到登录页),用户全程无感。

B6 · 撤销令牌(登出 / 解绑)

curl -X POST https://passport.lztech.top/oauth/revoke \
  -d client_id=你的client_id \
  -d token=要撤销的access_token
公开客户端无 Secret,不能调用 /api/oauth/revoke-user 整户撤销(该接口仅限机密客户端,防止冒用 client_id 批量清除他人授权)。解绑后如需终止授权关系,请引导用户到通行证「授权管理」自行撤销;撤销后再次授权会重新弹确认页。

B7 · 获取用户信息与 IdToken 验签

与机密客户端完全一致:携带 Authorization: Bearer 调 userinfo;IdToken 请在前端用验签库(如 jose)配合 jwks.json 校验,见下一节。

⑤ IdToken(OIDC)验签方法

IdToken 是平台用 RS256 私钥签名的 JWT,请求 scope 含 openid 时随 token 响应一并返回。载荷(payload)示例:

{
  "iss": "https://passport.lztech.top",
  "sub": "lz-1",
  "aud": "你的client_id",
  "iat": 1726100000,
  "exp": 1726101800,
  "nonce": "发起授权时传入的nonce",
  "nickname": "张三",      ← scope 含 profile 时
  "email": "z@example.com" ← scope 含 email 时
  "picture": "https://…"   ← scope 含 avatar 且用户已设置头像时
}
步骤操作
1从 GET /.well-known/jwks.json 按 header 中的 kid 取 RS256 公钥(JWK)
2校验签名(RS256);校验 iss 等于 https://passport.lztech.top;校验 aud 等于你的 client_id
3校验 exp(与 access_token 同有效期,30 分钟)
4发起授权时若带了 nonce,必须校验 IdToken 中的 nonce 与之完全一致(防重放)
禁止只解码不验签。任何客户端都可以构造 JWT,未验证签名直接读取载荷等同没有安全边界。服务端推荐 jose / jsonwebtoken,浏览器推荐 jose,小程序可将 JWKS 公钥预置进包内验签。

⑥ 端点总览

端点方法说明
/oauth/authorizeGET授权码入口(浏览器跳转,不返回 JSON)
/oauth/consentPOST授权确认提交(由平台授权页内部调用,第三方无需对接)
/oauth/tokenPOSTcode 换令牌 / 刷新令牌(form-urlencoded)
/oauth/userinfoGET用户信息(Authorization: Bearer)
/oauth/revokePOST撤销令牌对(RFC 7009)
/api/oauth/revoke-userPOST整户撤销:吊销某用户在本应用的全部令牌并删除授权关系(解绑场景,仅机密客户端)
/.well-known/openid-configurationGETOIDC 发现(issuer、端点、能力声明)
/.well-known/jwks.jsonGETIdToken 验签公钥(RS256 JWK)

authorize 阶段错误(重定向回 callback)

error说明
invalid_request参数缺失 / 公开客户端缺 PKCE 或 method 非 S256 / response_type 非 code
invalid_scope请求的 scope 超出应用登记范围
access_denied用户在授权确认页点了「拒绝」
unauthorized_client应用不存在、未过审或已被禁用/停用

token / userinfo 阶段错误(HTTP JSON)

errorHTTP说明
invalid_client401client_id / secret 错误;公开客户端提交了 Secret
invalid_grant400code 过期/已用/重放、redirect_uri 不一致、PKCE 验证失败、refresh_token 已轮换或被撤销
invalid_scope400scope 不合法
unsupported_grant_type400仅支持 authorization_code / refresh_token
unauthorized_client401应用未过审或被停用
invalid_token401userinfo 的 Bearer 令牌无效、过期、被撤销或用户已注销
server_error429触发限流(60 次/分钟/IP),请退避重试
code 重放(同一 code 第二次换 token):除报 invalid_grant 外,平台会连带撤销该授权码签发过的全部令牌,请勿缓存 code 重试。

⑦ 安全要求(违反即下架)

①redirect_uri 精确登记、精确匹配,生产环境必须 HTTPS
②ClientSecret 严禁出现在前端代码、App 安装包、小程序源码、公开仓库与日志中;疑似泄露立即在工作台轮换
③SPA / 移动端 / 小程序一律使用公开客户端 + PKCE(S256),不持有任何 Secret
④state 必须校验且每次授权随机生成;OIDC 场景必须校验 nonce 绑定
⑤IdToken 必须验签(RS256 + iss + aud + exp + nonce),不得只解码使用
⑥以 sub 作为用户唯一标识做账号关联,不得以 email / nickname 为主键
⑦refresh_token 轮换后立即丢弃旧值;不得长期存储已作废令牌重试
⑧用户解绑 / 注销账号时必须撤销令牌:登出用 /oauth/revoke;解绑(终止账号关联)必须调用 /api/oauth/revoke-user 整户撤销

⑧ 常见问题

Q1:用户第二次登录会再弹授权页吗?

不会。授权关系一旦建立(用户未撤销),后续授权请求直接静默发码;若用户已持有通行证登录态,整个跳转瞬时完成。用户可在「授权管理中心」随时撤销,撤销后恢复弹窗。

Q2:SPA 拿不到 refresh_token,30 分钟后岂不是要用户重新点登录?

不需要。access 到期后重新发起一次授权跳转即可:已授权用户静默发码,只要用户在通行证侧的登录态未过期(7 天),全程无感。这是公开客户端的标准安全模型——不在前端放长期凭据。

Q3:本地开发可以用 http 吗?

可以。回调地址登记为 http://localhost:端口/… 即可调试;平台强制 HTTPS 的规则对 localhost 回调不做要求,但生产环境回调必须 HTTPS。

Q4:code 换 token 报 invalid_grant 怎么排查?

按序检查:code 是否已使用过(一次性)→ 是否超过 5 分钟 → redirect_uri 是否与授权请求及登记完全一致 → 公开客户端 code_verifier 是否为发起时的原文(Challenge 计算方式是否 BASE64URL(SHA256) 无填充)。

© 2026 林中科技 · 林中通行证开放平台