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 节写明这种模式「不应」返回它,过期了直接重新申请即可,很多脚本里的刷新分支是走不到的死代码。
一、实验环境
用 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 的一行日志里。
三、改进写法
3.1 集合级认证
在集合的 Authorization 标签里选择 Bearer Token,值填 {{access_token}},集合内的请求保持默认的「Inherit auth from parent」。这样主请求里没有任何认证代码。
3.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 秒即可。
3.3 实测结果
| 场景 | 常见写法 | 改进写法 |
|---|---|---|
| 正常:8 个请求 | 8 个 200,申请 4 次 token | 8 个 200,申请 4 次 token |
client_secret 错误 | 业务请求照常发出,收到业务接口的 401 | 业务请求 0 次发出,每个请求记一条「获取 token」失败断言 |
| Newman 退出码(密钥错误) | 取决于业务请求有没有写状态码断言 | 1 |
3.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 注入 |
配套实验
- codesphere-labs/engineering/postman-token-refresh:模拟 OAuth 服务与 7 个集合,常见写法与改进写法在正常、密钥错误时的表现,以及变量作用域、顶层
await、回调里throw三个坑(Node 24.21、Newman 6.2.2;验证记录)
参考资料