林中通行证是林中科技的统一身份认证(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 请求位置 | 必须由服务端调用 | 前端可直接调用 |
| ① | 在 开放平台 使用通行证账号登录,提交开发者入驻申请,等待平台审核 |
| ② | 审核通过后创建应用:填写名称、描述、回调地址,选择客户端类型,勾选 scope |
| ③ | 应用审核通过后获得 client_id;机密客户端可查看/轮换 client_secret(仅创建或轮换时展示一次,平台只存哈希) |
| · | 必须为完整的 http(s):// URL,可登记多条 |
| · | 授权请求中的 redirect_uri 与登记记录精确匹配(协议、域名、端口、路径、query 全部一致),无通配符 |
| · | 生产环境必须 HTTPS;本地调试允许 http://localhost:端口 |
| · | 修改回调地址会导致已签发令牌全部撤销,请谨慎操作 |
| scope | 用户授权页显示 | 返回内容 |
|---|---|---|
| openid | 确认你的身份标识(用户ID) | userinfo 的 sub;签发 IdToken |
| profile | 获取你的昵称 | nickname |
| 获取你的注册邮箱 | email | |
| avatar | 获取你的头像 | picture(用户未设置头像时字段缺省) |
invalid_scope。应用新增 scope 后,已授权用户需重新弹窗授权。适用于服务端渲染站点、后端 API 应用。完整流程:构造授权跳转 → 用户授权回调 → 服务端换令牌 → 调 userinfo。
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 ← 可选,强制弹出账号选择页
GET https://your.app/callback?code=授权码&state=原样返回的state 校验顺序: 1. state 与你发起时保存的完全一致(不一致立即中止,防 CSRF) 2. code 在 5 分钟内且未被使用过(一次性,重放会连带撤销已发令牌对)
# 方式一: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…"
}
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,且该用户此应用全部令牌被撤销。
# 用户解绑 / 登出你的应用时调用;撤销该 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 是否存在)
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 仅作展示,可能被用户修改。# 用户在你的应用解绑账号时调用:撤销该用户在本应用的全部令牌,并删除授权关系
# (用户在通行证「授权管理」中该应用的记录同步消失,下次使用需重新走授权确认页)
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,请退避重试)
没有服务端秘密的应用。核心机制是 PKCE:用「验证器—挑战」替代 Secret,授权码即使被拦截也无法换出令牌。
-._~),必须用密码学安全随机源生成,不得复用BASE64URL(SHA256(code_verifier)),无填充// 浏览器 / 小程序生成示例
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 交换时要提交原文
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 直接被拒绝
GET https://your.app/callback#或?code=授权码&state=原样返回的state 校验:state 与发起前保存在 sessionStorage 的值完全一致,不一致立即中止。
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)
curl -X POST https://passport.lztech.top/oauth/revoke \ -d client_id=你的client_id \ -d token=要撤销的access_token
与机密客户端完全一致:携带 Authorization: Bearer 调 userinfo;IdToken 请在前端用验签库(如 jose)配合 jwks.json 校验,见下一节。
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 与之完全一致(防重放) |
| 端点 | 方法 | 说明 |
|---|---|---|
| /oauth/authorize | GET | 授权码入口(浏览器跳转,不返回 JSON) |
| /oauth/consent | POST | 授权确认提交(由平台授权页内部调用,第三方无需对接) |
| /oauth/token | POST | code 换令牌 / 刷新令牌(form-urlencoded) |
| /oauth/userinfo | GET | 用户信息(Authorization: Bearer) |
| /oauth/revoke | POST | 撤销令牌对(RFC 7009) |
| /api/oauth/revoke-user | POST | 整户撤销:吊销某用户在本应用的全部令牌并删除授权关系(解绑场景,仅机密客户端) |
| /.well-known/openid-configuration | GET | OIDC 发现(issuer、端点、能力声明) |
| /.well-known/jwks.json | GET | IdToken 验签公钥(RS256 JWK) |
| error | 说明 |
|---|---|
| invalid_request | 参数缺失 / 公开客户端缺 PKCE 或 method 非 S256 / response_type 非 code |
| invalid_scope | 请求的 scope 超出应用登记范围 |
| access_denied | 用户在授权确认页点了「拒绝」 |
| unauthorized_client | 应用不存在、未过审或已被禁用/停用 |
| error | HTTP | 说明 |
|---|---|---|
| invalid_client | 401 | client_id / secret 错误;公开客户端提交了 Secret |
| invalid_grant | 400 | code 过期/已用/重放、redirect_uri 不一致、PKCE 验证失败、refresh_token 已轮换或被撤销 |
| invalid_scope | 400 | scope 不合法 |
| unsupported_grant_type | 400 | 仅支持 authorization_code / refresh_token |
| unauthorized_client | 401 | 应用未过审或被停用 |
| invalid_token | 401 | userinfo 的 Bearer 令牌无效、过期、被撤销或用户已注销 |
| server_error | 429 | 触发限流(60 次/分钟/IP),请退避重试 |
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 整户撤销 |
不会。授权关系一旦建立(用户未撤销),后续授权请求直接静默发码;若用户已持有通行证登录态,整个跳转瞬时完成。用户可在「授权管理中心」随时撤销,撤销后恢复弹窗。
不需要。access 到期后重新发起一次授权跳转即可:已授权用户静默发码,只要用户在通行证侧的登录态未过期(7 天),全程无感。这是公开客户端的标准安全模型——不在前端放长期凭据。
可以。回调地址登记为 http://localhost:端口/… 即可调试;平台强制 HTTPS 的规则对 localhost 回调不做要求,但生产环境回调必须 HTTPS。
按序检查:code 是否已使用过(一次性)→ 是否超过 5 分钟 → redirect_uri 是否与授权请求及登记完全一致 → 公开客户端 code_verifier 是否为发起时的原文(Challenge 计算方式是否 BASE64URL(SHA256) 无填充)。
© 2026 林中科技 · 林中通行证开放平台