Skip to content

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 秒的间隔依次发送:

bash
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 的一行日志里。

四、改进写法 ​

发送任意请求集合内集合级 Pre-requesttoken 快过期了吗?直接使用{{access_token}}POST /tokenclient_credentials发出主请求Bearer 认证写入集合变量token 与过期时间获取失败:pm.test 记失败 + skipRequest否是主请求本身不写任何认证代码,只在集合的 Authorization 里配置 Bearer {{access_token}}
图 1 · token 逻辑只写一次,放在集合级;获取失败时记一条失败断言并跳过主请求,命令行运行会以退出码 1 结束

4.1 集合级认证 ​

在集合的 Authorization 标签里选择 Bearer Token,值填 {{access_token}},集合内的请求保持默认的「Inherit auth from parent」。这样主请求里没有任何认证代码。

4.2 集合级 Pre-request 脚本 ​

javascript
// 集合级 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 实测结果 ​

请求序号与携带的 token(深色 = 发送前刷新了 token)tok-5#1↻tok-5#2tok-6#3↻tok-6#4tok-7#5↻tok-7#6tok-7#7tok-8#8↻错误的 client_secret:8 次 POST /token 全部 401,业务请求 0 次发出,newman 退出码 1
图 2 · Newman 6.2.2 实测:token 有效期 3 秒、提前 1 秒刷新、请求间隔 0.8 秒,8 个请求只换了 4 次 token,全部返回 200
场景常见写法改进写法
正常:8 个请求8 个 200,申请 4 次 token8 个 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。


配套实验

参考资料

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权