Postman 自动管理 Token:脚本能跑通,不代表失败时会报错
把 token 刷新脚本放进 Pre-request 之后,一切正常时它确实能用。但把
client_secret故意改错再跑一遍:token 接口返回 401,脚本只打了一行日志,业务请求照样带着空的 Authorization 发了出去,然后收到另一个让人摸不着头脑的 401。自动化脚本最该验证的是失败路径。
本文用一个本地模拟的 OAuth 服务和 Newman 6.2.2,对比常见写法与改进写法的行为,整理出一个可以放进 CI 的 token 管理方案。
一、先说结论
- 标准 OAuth 2.0 流程先用 Postman 内置的授权配置。它会在 token 过期前自动刷新,但官方文档说明:定时运行、Monitor、Postman CLI 和 Newman 不支持自动刷新。需要在命令行里跑的集合,还是得用脚本。
client_credentials模式本来就不该有 refresh token。RFC 6749 第 4.4.3 节写明这种模式「不应」返回 refresh token,过期了直接重新申请即可。很多脚本里的刷新分支是走不到的死代码。- 脚本写在集合级,只写一次。主请求不写认证代码,在集合的 Authorization 里配置
Bearer {{access_token}}。 - 获取失败时要让运行失败:用
pm.test记一条失败断言,再用pm.execution.skipRequest()跳过主请求。实测改进后,密钥错误时业务请求一次都不会发出,Newman 以退出码 1 结束。 - 密钥不要放在全局变量里:全局变量对整个工作区可见,
client_secret应放在环境变量(secret 类型)或 Postman Vault 中。
二、实验环境
用 Node.js 写了一个最小的模拟服务:
POST /token:校验client_secret,返回access_token和expires_in: 3(3 秒过期,方便观察刷新),按 RFC 不返回refresh_token;GET /orders:校验 Bearer token 是否存在且未过期,否则返回 401。
集合里放 8 个 GET /orders 请求,用 Newman 以 0.8 秒的间隔依次发送:
npx newman run collection.json --delay-request 800三、常见写法的问题
常见写法是在每个请求的 Pre-request 里放一段脚本:检查全局变量里的 token 是否过期,有 refresh token 就刷新,否则重新申请,最后把 token 写回全局变量并设置请求头。
正常路径是通的。 实测 8 个请求全部返回 200,期间共申请了 4 次 token。但它有四个问题:
| 问题 | 后果 |
|---|---|
刷新分支依赖 refresh_token,而 client_credentials 不返回它 | 刷新逻辑永远走不到;pm.globals.set("refresh_token", undefined) 还会写进一个无意义的值 |
| 脚本复制到每个请求里 | 改一处要改几十处,漏改的请求行为不一致 |
token 和 client_secret 都存在全局变量里 | 全局变量对工作区里所有集合可见;导出、分享时容易带出去 |
| 获取 token 失败只打日志 | 业务请求照样发出,收到的是业务接口的 401,排查方向被带偏 |
最后一条是实测出来的:把 client_secret 改错后,每个请求都先收到 token 接口的 401,接着业务请求带着空 token 发出,又收到业务接口的 401。报错指向的是业务接口,真正的原因只留在 Console 的一行日志里。
四、改进写法
4.1 集合级认证
在集合的 Authorization 标签里选择 Bearer Token,值填 {{access_token}},集合内的请求保持默认的「Inherit auth from parent」。这样主请求里没有任何认证代码。
4.2 集合级 Pre-request 脚本
// 集合级 Pre-request:集合里每个请求发送前都会先执行
const SKEW_SECONDS = 60; // 提前刷新的余量,避免请求在路上时 token 过期
const now = Math.floor(Date.now() / 1000);
const token = pm.collectionVariables.get("access_token");
const expiresAt = Number(pm.collectionVariables.get("token_expires_at") || 0);
if (!token || now >= expiresAt - SKEW_SECONDS) {
pm.sendRequest({
url: pm.variables.get("base_url") + "/token",
method: "POST",
header: { "Content-Type": "application/x-www-form-urlencoded" },
body: {
mode: "urlencoded",
urlencoded: [
{ key: "grant_type", value: "client_credentials" },
{ key: "client_id", value: pm.variables.get("client_id") },
{ key: "client_secret", value: pm.variables.get("client_secret") }
]
}
}, (err, response) => {
if (err || response.code !== 200) {
pm.collectionVariables.unset("access_token");
// 记一条失败的断言,让本次运行明确失败;并跳过主请求,不带着无效 token 继续发
pm.test("获取 token", () => {
throw new Error(`HTTP ${err || response.code + " " + response.text()}`);
});
pm.execution.skipRequest();
return;
}
const body = response.json();
pm.collectionVariables.set("access_token", body.access_token);
pm.collectionVariables.set("token_expires_at", now + body.expires_in);
});
}实验时把 SKEW_SECONDS 设成 1(模拟 token 只有 3 秒有效期),正式使用时按 token 的实际有效期设置,一般 30~60 秒即可。
4.3 实测结果
| 场景 | 常见写法 | 改进写法 |
|---|---|---|
| 正常:8 个请求 | 8 个 200,申请 4 次 token | 8 个 200,申请 4 次 token |
client_secret 错误 | 业务请求照常发出,收到业务接口的 401 | 业务请求 0 次发出,每个请求记一条「获取 token」失败断言 |
| Newman 退出码(密钥错误) | 取决于业务请求有没有写状态码断言 | 1 |
4.4 实验中踩到的三个坑
变量作用域。 第一版脚本用 pm.collectionVariables.get("client_secret") 读配置,结果命令行传入的 --env-var client_secret=wrong 完全没有生效。pm.collectionVariables 只读集合这一层;pm.variables.get 才会按「局部 → 数据文件 → 环境 → 集合 → 全局」的优先级解析。读配置用 pm.variables.get,写 token 用 pm.collectionVariables.set,这样同一个集合可以通过切换环境指向不同的认证服务。
脚本出错时请求仍会发出。 另一版脚本在顶层使用了 await pm.sendRequest(...)。Postman 桌面端的文档里有这种写法,但 Newman 6.2.2 直接报 SyntaxError: await is only valid in async functions and the top level bodies of modules,而且脚本报错后,主请求照样发送,请求头里的 {{access_token}} 是一段没有被替换的原文。所以要在命令行里运行的集合,脚本要用回调写法,并且在 Newman 里实际跑一遍。
throw 不会让运行失败。 在 pm.sendRequest 的回调里直接 throw,Newman 的统计里 prerequest-scripts 失败数仍然是 0,退出码也是 0。要让 CI 感知到失败,得通过 pm.test 记一条失败断言。
五、密钥放在哪
| 位置 | 可见范围 | 适合放什么 |
|---|---|---|
| 全局变量 | 整个工作区 | 几乎什么都不该放,尤其是密钥 |
| 集合变量 | 这个集合,导出时会带上 | 运行中产生的 token、过期时间 |
| 环境变量(secret 类型) | 选中该环境的请求;界面上默认遮蔽 | 各环境的 base_url、client_id、client_secret |
| Postman Vault | 仅本地,不同步到云端 | 个人的高敏感凭据,脚本中通过 pm.vault 读取 |
| CI 的密钥管理 | 流水线 | 命令行运行时通过 --env-var 注入 |
六、常见误区
- 「client_credentials 要处理 refresh token」:规范不建议返回,过期重新申请即可。
- 「Pre-request 脚本报错,请求就不会发」:实测照样发出,而且带着未替换的变量。
- 「在回调里 throw 就能让运行失败」:Newman 不计入失败,要用
pm.test。 - 「内置 OAuth 2.0 会自动刷新,命令行也一样」:官方文档明确说明 Newman 和 Postman CLI 不支持自动刷新。
- 「token 放全局变量最方便」:方便的代价是所有集合共享,密钥也更容易被导出。
小结
token 自动化的正常路径很容易写对,难的是失败路径:获取失败时,要让主请求停下来,并且让运行结果明确失败,而不是留下一行日志后继续发请求。把脚本收敛到集合级、读写变量分清作用域、密钥放进环境或 Vault,再用 Newman 把成功和失败两条路径各跑一遍,这套方案才算真正可以放进 CI。
配套实验
- codesphere-labs/engineering/postman-token-refresh:模拟 OAuth 服务与 7 个集合,常见写法与改进写法在正常、密钥错误时的表现,以及变量作用域、顶层
await、回调里throw三个坑(Node 24.21、Newman 6.2.2;验证记录)
参考资料