给你的应用接 OpenRouter 登录:OAuth PKCE 流程怎么走
一、这个流程是替你解决什么麻烦的
做一个要调大模型的应用,最先卡住的往往不是代码,而是”这笔钱算谁的”。
如果你在服务端塞一把自己的 API key,所有用户的调用都记在你头上,你就得自己做计量、自己做限额、自己承担别人刷你额度的风险。反过来,让用户手动去平台生成一把 key、复制粘贴进你的应用,流程能跑通,但每多一步复制粘贴就多掉一批人。
OpenRouter 官方文档《OAuth PKCE》页(openrouter.ai/docs/guides/overview/auth/oauth)给的是第三条路:用户在你的应用里点一下,跳到 OpenRouter 登录并授权你的应用,回来之后你的应用拿到一把 user-controlled API key——文档原文就是这么描述这把 key 的归属的。用量记在用户自己的账号下,你的应用只负责拿着这把 key 发请求。
这套流程走的是 PKCE(Proof Key for Code Exchange),文档在开头就把这个名字链到了 PKCE 标准本身,并在提示框里另外链了一份第三方的 PKCE 参数说明。需要说明的是:PKCE 相比授权码模式免去 client secret,这是 OAuth 标准层面的通用常识,不是 OpenRouter 这一页写的内容——这一页从头到尾没有出现 client secret 这个词,也没有交代它对哪类应用更合适。下面凡是讲 OpenRouter 具体行为的部分,都只按这一页写明的来。
二、前置条件:先确认你属于哪一种形态
这一步最容易被跳过,但它决定了后面参数怎么填。官方文档在 Step 1 里明确区分了三种部署形态:
形态一,有公开可访问的回调地址。 你有一个网站,能接住浏览器的重定向。这是文档描述的默认情形。
形态二,本地应用(Localhost Apps)。 文档写明 localhost 回调支持任意端口,原文举的例子是 http://localhost:51423/callback——这对那些临时向操作系统要一个空闲端口来接回调的 CLI 工具和本地优先的应用很实用,你不用提前去后台登记端口。
形态三,headless(SSH 会话、远程开发机、容器)。 这类环境里 localhost 回调根本到不了浏览器。文档给的做法是完全省略 callback_url 参数。
另外需要准备的东西:如果你要用推荐的 S256 方式,得能算 SHA-256。文档给的示例用的是 Web Crypto API 加 Buffer API,并且专门提醒了一句——在浏览器里用 Buffer API 需要一个打包器(bundler)。这句话别忽略,它是文档自己写的前置约束。
三、按文档走一遍:三步加一个可选动作
Step 1:把用户送到 OpenRouter 的 /auth
起点是一个带查询参数的 URL。文档给了三个并列版本:
https://openrouter.ai/auth?callback_url=<YOUR_SITE_URL>&code_challenge=<CODE_CHALLENGE>&code_challenge_method=S256
https://openrouter.ai/auth?callback_url=<YOUR_SITE_URL>&code_challenge=<CODE_CHALLENGE>&code_challenge_method=plain
https://openrouter.ai/auth?callback_url=<YOUR_SITE_URL>
三个版本的差别只在 challenge 上:S256、plain、以及完全不带。文档的原话是 code_challenge 参数可选但推荐,并且在提示框里进一步说,为了更高的安全性,把 code_challenge_method 设为 S256,把 code_challenge 设为 code_verifier 的 sha256 哈希的 base64 编码。
注意这里的措辞——code_challenge 是你算出来的,code_verifier 是你自己留着的那个随机串。这两个名字长得像,方向配反了肉眼很难看出来。文档在错误码那一节专门给这类情况留了处置说明(403 Invalid code or code_verifier 一条就是让你回头确认 code_verifier 与 code_challenge_method 是否正确),可见它是个值得单独确认一遍的地方。
生成 challenge 的代码文档是这么写的:
import { Buffer } from 'buffer';
async function createSHA256CodeChallenge(input: string) {
const encoder = new TextEncoder();
const data = encoder.encode(input);
const hash = await crypto.subtle.digest('SHA-256', data);
return Buffer.from(hash).toString('base64url');
}
const codeVerifier = 'your-random-string';
const generatedCodeChallenge = await createSHA256CodeChallenge(codeVerifier);
base64url 而不是普通 base64,这个细节写在代码里,容易看漏。示例中的 'your-random-string' 只是占位,真实场景下这个串必须每次随机生成。
用户在 OpenRouter 完成登录并授权之后,会被重定向回你的站点,URL 上带一个 code 查询参数。
headless 形态的 URL 长这样,注意它没有 callback_url,多了一个 key_label:
https://openrouter.ai/auth?code_challenge=<CODE_CHALLENGE>&code_challenge_method=S256&key_label=<YOUR_APP_NAME>
文档写明:这种模式下授权完成后不做重定向,而是把授权码显示出来,由用户自己复制、粘回你的应用(比如粘到一个终端提示符后面),然后你照常做 Step 2 的兑换。并且——这种模式下 code_challenge 是必需的,不是可选的。文档给了理由(属文档自述):因为授权码是显示出来的,PKCE 保证了没有你的应用那份 code_verifier 的人拿到码也没用。文档同时写明,这个码单次使用,签发后 10 分钟过期。
Step 2:把 code 换成 key
先从 URL 上取码,文档给的就是最朴素的浏览器 API:
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
然后 POST 到 https://openrouter.ai/api/v1/auth/keys:
const response = await fetch('https://openrouter.ai/api/v1/auth/keys', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: '<CODE_FROM_QUERY_PARAM>',
code_verifier: '<CODE_VERIFIER>', // If code_challenge was used
code_challenge_method: '<CODE_CHALLENGE_METHOD>', // If code_challenge was used
}),
});
const { key } = await response.json();
三个字段里,code 必带;后两个字段文档的注释是”如果用了 code_challenge 就带”。返回体里解构出来的字段名是 key。
这里有一处配对关系值得单独拎出来:Step 1 用的 code_challenge_method 和 Step 2 传的 code_challenge_method 必须是同一个值。这不是我的推断,是文档在错误码那一节里明写的处置建议。
Step 3:拿 key 发请求
文档给了两种写法,一种走官方 TypeScript SDK:
import { OpenRouter } from '@openrouter/sdk';
const openRouter = new OpenRouter({
apiKey: key, // The key from Step 2
});
const completion = await openRouter.chat.send({
model: '~openai/gpt-latest',
messages: [{ role: 'user', content: 'Hello!' }],
stream: false,
});
一种直接 fetch,把 key 放进 Authorization: Bearer ${key}。代码里的 ~openai/gpt-latest 只是官方文档当时写的示例值,平台上有哪些模型 slug 随时在变,别把它当可用模型清单看。
存放位置文档只给了一句原则:安全地存在用户浏览器里,或者存在你自己的数据库里。具体怎么加密、怎么轮换,官方文档没有说明这一点。
可选动作:拿 key 的哈希做深链
如果你想在自己的界面里给出”去看我的用量""去改这把 key”的入口,文档给了一个做法:把 key 做 SHA-256,取小写十六进制摘要,拼进两个地址:
const keyHash = await sha256Hex(key);
const logsUrl = `https://openrouter.ai/logs?api_key_hash=${keyHash}`;
const settingsUrl = `https://openrouter.ai/keys/${keyHash}`;
文档特别写了这两个链接的可见性语义:只对已登录的这把 key 的所有者有效;如果哈希在当前访问者身上解析不出来,页面返回 404,而不是把无过滤的数据显示出来。这一点在做分享类功能时值得记住——它是按访问者的登录身份判定的。
四、边界:文档划了线的地方
localhost 应用有一项固定行为。 文档写明:localhost 应用会被分配一个与主机和端口匹配的固定标题(例如 localhost:3000),并且不会出现在 OpenRouter 的 marketplace 与 rankings 里。想要自定义应用名和 marketplace 露出,就得用一个公开 URL 作为回调。文档给的迁移建议是:上生产时把 localhost 回调换成公开 URL(你的项目官网或者一个 GitHub 仓库链接),以拿到完整的 app attribution。
这条正好接上另一页——《App Attribution》(openrouter.ai/docs/app-attribution)。那一页写明 HTTP-Referer 请求头是 app attribution 的必需项,没有它不会创建应用页、用量也不会进榜;X-OpenRouter-Title(旧名 X-Title 仍向后兼容)单独设置不会创建应用页,必须和 HTTP-Referer 配对使用。还有一条容易漏:用 localhost URL 的应用必须同时带上 X-OpenRouter-Title 才会被追踪。这两页放在一起看才完整:OAuth 那页决定你的回调地址长什么样,attribution 那页决定这个地址会不会被算作一个应用。
几处文档没说的,我就不替它说。 这把 user-controlled key 的有效期、能不能刷新、用户在自己账号里撤销之后你的应用会看到什么错误、同一个用户重复走一遍流程是复用旧 key 还是新签一把——官方文档在这一页没有说明这一点,别按常见 OAuth 实现的习惯去猜。
这一页也没有出现 beta / preview / experimental / deprecated 之类的标记,所以我不给任何一步加”尚在预览”的标注;同样,我也不据此断言它一定稳定。
五、怎么验证配对了
最实在的验证入口是文档给的四个错误码,它们几乎一一对应到某一步的具体配错:
| 错误 | 文档给的处置 |
|---|---|
400 Invalid code_challenge_method | 确认 Step 1 和 Step 2 用的是同一个 challenge 方法 |
403 Invalid code or code_verifier | 确认用户已登录 OpenRouter,且 code_verifier 与 code_challenge_method 正确 |
403 Authorization code expired | 授权码签发 10 分钟后过期,重走一遍流程并尽快兑换 |
405 Method Not Allowed | 确认用的是 POST 且走 HTTPS |
按这张表倒推很省事:拿到 400 就只查方法名两边一不一致,别去动 verifier;拿到 405 先看有没有把请求发成了 GET 或者掉到 HTTP 上。
流程本身跑通的判据也很直接:Step 2 的响应体里能解构出 key,并且用这把 key 发一次 Step 3 的请求能拿到正常回包。再往后一层,如果你在意 attribution,就带上 HTTP-Referer(localhost 场景再加 X-OpenRouter-Title),按《App Attribution》页的说法去 openrouter.ai/apps?url=<your-referer-url> 看应用页有没有被创建。
Windows 与 Linux/macOS 的差别。 官方文档这一页没有按操作系统分叙,只写了”localhost 回调支持任意端口”这一条与本地环境相关的约束。实际落地时,Windows 上跑 CLI 工具接回调常见的两个绊脚点是本地端口被安全软件或防火墙拦下、以及浏览器唤起方式不同——这两点属于通用的本地开发经验,不是 OpenRouter 官方文档的内容,我不把它写成产品行为。如果你的 Windows 端接不到回调,先按上面第二节的形态三处理:省略 callback_url 走 headless,把码人工粘回来,兑换步骤完全一样。
以上代码片段均按官方文档原样抄录;组合参数时请以官方文档中的参数语义为准,未经实测。该平台迭代频繁,参数名、端点路径与错误码以官方文档最新内容为准。
本文依据 OpenRouter 官方文档(openrouter.ai/docs)于 2026-08-18 的公开内容整理。
该平台闭源,本文只复述官方文档写明的机制,不推断其内部实现;
我们没有对文中涉及的功能做过实测,因此不涉及界面外观与运行表现的任何描述。
该平台的供应商、模型与路由策略随时变动,文中不列具体供应商名单与模型清单;
价格、额度与限流的具体数值请以官方定价页与用量说明为准。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。