
很多刚接触接口测试的同学都会遇到同一个问题接口文档里明明白白写着需要 token 鉴权自己用 Jmeter 调试单个接口时手动把 token 粘进去还能跑通但一旦开始做完整的接口测试流程几十上百个接口都要手动维护 token光复制粘贴就够喝一壶的。更麻烦的是token 还有有效期过期之后又要重新去登录接口拿一次整套脚本瞬间变成体力活。这篇文章就从“配置全局 token”这个最常见的需求出发完整梳理一套能直接落到项目里的 Jmeter 接口测试方案。从最基础的测试计划结构怎么设计、登录接口怎么写到如何用 JSON Extractor 提取动态 token、如何用 props 在不同线程组之间全局传值再到如何用 BeanShell 自动处理 token 过期刷新把全链路拆开讲清楚。无论你是刚上手接口测试的测试新人还是已经在用 Jmeter 做回归测试、性能测试的老手这套方案都能帮你在实际项目里少踩几个坑。1. 为什么接口测试里一定要搞“全局 token”1.1 接口鉴权的核心痛点现在绝大多数业务系统的接口都不是裸奔的。登录之后服务端会给客户端发一个 token后续所有接口请求在 Header 里带上Authorization: Bearer token服务端校验通过才返回业务数据。这种做法本身没毛病真正麻烦的是它给接口测试脚本带来的连锁反应接口数量一多token 不能写死。你很难保证每次跑测试时系统里的 token 还是有效的把它写死在脚本里隔几个小时大概率就会收到一排 401。每个线程组都要用 token。如果你的测试计划里分了登录、订单、支付等多个线程组token 在其中一个线程组里生成另外一个线程组拿不到脚本就跑不起来。压测场景下 token 会过期。性能测试通常要持续跑十几分钟甚至几小时token 过期是一个必然发生的事件不做自动刷新就等于白跑。所以“全局 token”这四个字的本质不只是在 Jmeter 里配一个变量那么简单而是要把 token 的获取、存储、传递、刷新这一整条链路都打通。1.2 常规做法的三个层次在实际项目中我见过很多团队处理 token 的方式大致可以分成三个层次你可以先对照一下自己现在到哪一层了处理方式具体操作优点缺点适用场景手动粘贴打开浏览器开发者工具复制 token填到 HTTP Header Manager 里配置快逻辑简单维护成本极高token 一过期就失灵临时调试单个接口固定抽取用正则表达式或 JSON Extractor 从登录接口响应里提取 token存成变量自动化程度较高token 过期后依然要手动重启脚本功能测试、短期回归动态刷新把 token 存到全局变量里并通过逻辑控制器在 401 时自动重新登录、替换 token全自动可持续压测需要理解 Jmeter 的变量作用域配置相对复杂回归测试、长时压测这篇文章所说的“配置全局 token”默认指的是第三种层次。方案从表面上看只是多加了几个元件但它解决的是接口测试脚本可持续运行的问题这一点在长期回归和性能测试场景里非常关键。1.3 全局 token 的整体工作流程先给你一个完整的数据流转图景后面所有配置步骤都是围绕这条链路来的登录接口发起请求 → 从响应 JSON 中提取 access_token → 把 token 写入全局属性 → 所有业务接口从全局属性中动态读取 token → 请求 Header 自动携带 → 当接口返回 401 时触发重新登录 → 更新全局 token → 后续请求继续正常执行。这个链路里每个环节在 Jmeter 中都有对应的元件接下来我一步步给你拆。2. 方案选型与测试环境准备2.1 我为什么选“setUp 线程组 JSON Extractor props 传值”Jmeter 里实现全局 token 不止一种方案不同项目选型逻辑不同。这里先讲清楚我推荐这套组合的原因后面你改起来也有依据。网上很多教程会把登录接口和业务接口放在同一个线程组里登录请求放在最前面后面跟一串业务请求。这种做法在接口数量少的时候确实省事但它有一个致命问题在线程数为 N 的压测场景下每个线程都要先跑一遍登录接口等于登录接口也被压测了。而登录接口往往有验证码、有频率限制、有性能瓶颈这样做既不符合真实业务模型又容易把登录接口打挂。所以我更倾向于用setUp 线程组来单独跑登录。setUp 线程组在线程组启动前自动执行天然适合做测试前置准备而且 Jmeter 的 setUp 线程组和业务线程组之间是不同步的业务线程组会在前置准备完成之后才开始跑这个时序正好满足“先拿 token再跑业务”。至于 token 传递Jmeter 中的变量有两种作用域存储方式作用域使用方式适合场景vars当前线程内vars.get(token)/vars.put(token, value)同一个线程组内的临时变量propsJVM 级别全局props.get(token)/props.put(token, value)跨线程组共享数据普通变量通过 JSON Extractor 默认生成的都存储在vars里它的作用范围只限于当前线程。对于 setUp 线程组和业务线程组这种跨线程组的场景vars是拿不到的必须用props才能让 token 在整个测试计划内全局生效。这是这套方案里最容易被新手忽略的关键点后面我还会专门展开。2.2 前置准备Jmeter 版本与测试接口先说版本。文章里所有操作在JMeter 5.x环境中验证过5.4 及以上版本都能直接照着做。JSON Extractor 是 Jmeter 的核心组件不需要额外装插件包这一点比老版本方便很多。如果你还在用 4.x 或者更老的版本建议先升级因为老版本的正则表达式提取方式写起来更绕维护成本也更高。测试环境方面建议你准备一套带了标准登录鉴权的接口环境接口结构大致包含这两个角色登录接口返回 JSON 结构里面含access_token和expires_in字段。业务接口需要在 Header 中携带Authorization: Bearer token才能访问。如果你手上没有现成的接口用 Jmeter 自带的 HTTP 请求访问任意一个要求鉴权的 demo 服务也可以核心在于熟悉流程而不是依赖具体接口。2.3 测试计划结构提前规划在动手配置之前先在脑子里把测试计划的结构搭好。一个标准的全局 token 测试计划通常长这样测试计划 ├── setUp 线程组登录并生成 token │ ├── HTTP 请求登录接口 │ ├── JSON Extractor提取 token │ └── BeanShell 后置处理器写入 props 全局变量 ├── 业务线程组执行实际业务接口 │ ├── HTTP Header Manager引用全局 token │ ├── HTTP 请求查询订单 / 用户信息等 │ └── 其他业务请求 └── 查看结果树 / 聚合报告监听器这个结构里每个元件都是一个小角色后面配置的时候我会逐一说明。先把整体结构刻在脑子里配置的时候就不会乱。3. 登录取 token先把“源头”跑通3.1 登录请求配置与参数化登录账号在 setUp 线程组下添加一个 HTTP 请求取样器把它命名为“登录获取 token”。这里有一个通用原则登录接口是整套脚本的前置条件它的稳定性直接决定后面所有请求的成败所以这个请求的配置要尽量精确。线程数设置为 1循环次数为 1意思就是整个测试计划执行之前只登录一次只拿一个 token然后所有业务线程共享这一个 token。这在实际项目里是完全合理的因为压力测试要模拟的是登录后的用户并发操作行为不是登录过程本身的高频并发。登录接口的参数建议做参数化处理不要把账号密码直接写死在请求体里。你可以新建一个 CSV 数据文件第一行放账号密码用 CSV Data Set Config 引用也可以直接在用户自定义变量里维护。参数化的好处有两个一是测试环境账号变了不用改脚本只改数据文件二是以后要扩展到多账号并发场景只需要在 CSV 里多几行数据就能实现。请求体按实际接口文档来写大部分项目登录接口都是这样的 JSON 格式{ username: ${test_account}, password: ${test_password} }如果接口要求的是表单格式改成参数表形式填写即可不影响后续 token 提取逻辑。3.2 先跑通再谈全局登录请求配置完之后先加一个“查看结果树”监听器直接运行一次。这一步的目的不是验证全局 token而是确认登录接口本身能通响应里确实返回了你需要的 token 字段。我见过不少同学一上来就把整个全局 token 流程全部配好结果运行报错然后从一堆元件里排查问题效率非常低。正确的做法是先小步验证登录接口通了 → 看到响应数据 → 再去做提取和传值。把每个环节的风险控制在最小范围内。在查看结果树里重点看响应数据是否包含类似这样的结构{ code: 0, message: ok, data: { access_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx, expires_in: 7200, token_type: Bearer } }这里有两个信息很关键token 所在的路径data.access_token以及过期时间字段expires_in。记住这两个字段的路径下一步提取就会用到。4. 用 JSON Extractor 把 token 变成可引用的变量4.1 添加 JSON Extractor 并理解每个字段在登录 HTTP 请求上右键添加“后置处理器 → JSON Extractor”这是整个流程里的核心元件之一。它的作用是从登录接口的 JSON 响应中提取指定的值并存储成 Jmeter 变量。配置界面有四个核心字段Name of created variables变量名比如access_token。JSON Path expressionsJSONPath 表达式用于定位响应中的字段比如$.data.access_token。Default Values默认值如果提取失败时使用通常填一个明显的错误占位符比如TOKEN_NOT_FOUND方便排查。Match No.匹配序号对于登录接口来说响应只有一个 token填 1 即可。这里最值得留意的是变量名。JSON Extractor 里设置的变量名会直接成为 Jmeter 变量的名字后续在 HTTP Header Manager 中通过${access_token}来引用。变量名不要带点号或其他特殊字符否则引用的时候容易出错。JSONPath 的写法本身很简单逻辑上就是从根开始一层层往下定位$代表根节点.data.access_token表示取 data 对象里的 access_token 字段。如果你看到响应结构是数组嵌套的比如$.data.list[0].token也可以用索引定位。这个语法在实际项目中百分之九十以上的场景都能覆盖。4.2 不同响应结构的 JSONPath 写法对照实际测试中你遇到的接口不可能都长一样我整理了三种最常见的 token 响应结构你可以直接照着套用响应结构示例JSONPath 写法{data: {access_token: xxx, expires_in: 7200}}$.data.access_token{data: {token: xxx}, code: 0}$.data.token{access_token: xxx, token_type: Bearer}$.access_token如果你用的是 HBuilder、Apifox 这类工具也可以直接在里面验证 JSONPath 是否正确确认提取路径无后再填到 Jmeter 里省去反复试错的成本。4.3 从 vars 到 props跨线程组传值的关键一步到了这一步token 已经存在于登录线程的vars里了。但是前面说过vars是线程级的业务线程组里根本读不到。如果你只配到这里就停手业务线程组里的${access_token}永远是空的请求发出去还是会 401。所以关键的一步来了在登录请求下再加一个“后置处理器 → BeanShell 后置处理器”用一行脚本把 token 从线程局部变量提升为全局属性。脚本内容非常简单String token vars.get(access_token); props.put(global_token, token);这段代码的意义可以这么理解vars.get(access_token)把上一个 JSON Extractor 提取到的 token 拿回来props.put(global_token, token)把这 token 存到 Jmeter 的全局属性池里。props是 JVM 级别的整个测试计划里的任何线程、任何线程组都能通过props.get(global_token)拿到这个值。很多教程到这里就结束了但我要多说一句为什么是props而不是再去加一个“用户自定义变量”因为用户自定义变量配置在测试计划级别时是在测试启动时读取一次并且固定的而 token 是在测试运行过程中动态拿到的值两者在时序上就对不上。用props做运行时传值是符合 Jmeter 设计思路的也是最不容易踩坑的。5. BeanShell 处理 token 过期未雨绸缪5.1 为什么 token 过期之后容易“翻车”登录接口返回的expires_in通常是一个以秒为单位的时间戳比如 7200 表示两小时后过期。在短时功能测试中这个时间足够覆盖整个测试流程基本不会出问题。但如果你的脚本要跑一个小时的稳定性测试或者被拿去做了定时回归任务token 过期就会变成一个必然发生的事情。token 过期之后会发生什么所有业务接口统一返回 401。如果你没有做自动刷新逻辑整个测试报告里就会铺满 401 的错误记录这时候你分不清到底是接口本身有 bug还是测试脚本里的 token 失效了排查成本极高。为了避免这种“测试结果失真”的情况我习惯在拿到 token 的同时把它的过期时间也计算出来一并存到全局属性里。这个值在后面的自动刷新逻辑中会用到。5.2 BeanShell 脚本实现过期时间记录用 BeanShell 脚本计算过期时间核心思路是读取登录接口返回的expires_in秒数加上当前系统时间得到过期的时间戳。这里有一个细节如果你用的是上一节的 JSON Extractorexpires_in这个字段已经被存在vars里了直接取就行。在 BeanShell 后置处理器中写如下脚本import java.util.Date; import java.text.SimpleDateFormat; String expiresIn vars.get(expires_in); long currentTime System.currentTimeMillis(); long expireTime currentTime Long.parseLong(expiresIn) * 1000; props.put(global_token, vars.get(access_token)); props.put(token_expire_time, String.valueOf(expireTime));这里有个非常容易踩的坑expires_in通常以秒为单位而System.currentTimeMillis()返回的是毫秒两者相乘前一定要先乘以 1000 换算成毫秒否则你记录下来的过期时间会比真实时间早很多导致 token 还在有效期内就触发了不必要的刷新逻辑。第一行的 import 语句在 Jmeter 的 BeanShell 环境里可以省略因为 Jmeter 默认支持这些基本类库。但写上也不会有问题而且能保证脚本在复制到其他环境时依然能正常运行。5.3 如果 token 是 JWT 格式如何解析失效时间有一部分系统的 token 用的是 JWT 格式它本身不依赖服务器端存储直接解析 token 的载荷部分就能拿到过期时间。如果你拿到手的 token 长这样eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsImV4cCI6MTczMDAwMDAwMH0.signature那么中间那段eyJ1c2VyX2lkIjoxMjMsImV4cCI6MTczMDAwMDAwMH0就是 Base64 编码的 JSON 数据。用 BeanShell 脚本可以直接解析出exp字段import org.apache.commons.codec.binary.Base64; String token vars.get(access_token); String[] parts token.split(\\.); String payload new String(Base64.decodeBase64(parts[1].getBytes(UTF-8))); // payload 长这样{user_id:123,exp:1730000000} // 这里用最简单的截取方式拿到 exp 的值 String exp payload.replaceAll(.*\exp\:(\\d).*, $1); props.put(token_expire_time, String.valueOf(Long.parseLong(exp) * 1000));这段脚本的关键点有两个一是 JWT 的 payload 部分不是标准 JSON直接用 JSON 解析库会报错所以我用正则方式提取二是 JWT 里的exp字段是秒级 Unix 时间戳同样需要乘以 1000 转成毫秒。当然并不是所有项目的 token 都是 JWT如果你的登录响应里有现成的expires_in字段用第一种方式就够了。JWT 解析属于备用方案等真遇到再回来看这一段也不迟。6. HTTP Header Manager 挂全局 token一个配置全接口生效6.1 为什么放在线程组级别而不是请求级别token 已经存入props全局属性了接下来要解决的问题是如何让每个业务接口自动带上这个 token而不是一个个去配置。Jmeter 里的 HTTP Header Manager 是一个非常有“继承性”的元件它放在哪个层级就会对哪个层级下的所有 HTTP 请求生效放在测试计划级别对整个测试计划的所有请求生效。放在线程组级别对该线程组内所有请求生效。放在某个 HTTP 请求下只对该请求生效。全局 token 意味着所有业务接口都要带这个 Header所以最合理的做法是放在业务线程组级别而不是登录线程组里登录请求本身不需要带 token。这样只需要配置一个 Header Manager线程组下面挂的所有请求就全部自动生效了后续新增接口也无需额外配置。6.2 配置方式函数引用全局属性在业务线程组上添加“配置元件 → HTTP Header Manager”添加一行 Header字段名值AuthorizationBearer ${__P(global_token,)}这里的写法很多人第一次看到会犯错习惯性写成${global_token}。这两种写法有本质区别${global_token}引用的是vars里的变量但前面说过vars只在当前线程内有效跨线程组永远取不到。${__P(global_token)}是一个 Jmeter 函数它直接去全局属性池里取名为global_token的值这才是跨线程组唯一正确的引用方式。第二个参数是默认值我填了空字符串。万一属性还没有被写入比如登录失败请求会带着空的 Authorization 头发出去返回 401 之后你就能根据响应码快速定位问题。有的项目要求 Header 不叫 Authorization而是用 X-Access-Token 或者自定义的 token 头字段这个以你们接口文档为准替换 Header 名称即可配置逻辑完全一样。6.3 如何验证 token 确实拼上去了配置完之后先别急着跑完整流程先在业务线程组下加一个“调试取样器”Debug Sampler和“查看结果树”单独跑一次业务线程组。打开查看结果树展开调试取样器的响应数据你会看到类似这样的内容global_tokeneyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx token_expire_time1730000000000这里能同时看到global_token和token_expire_time说明 props 的写入和读取已经打通了。然后切到业务请求的“请求体”或“HTTP Header”选项卡确认 Authorization 头确实带上了 token 值。到这一步“全局 token”的基本骨架已经完整了登录接口生成 token → JSON Extractor 提取 → BeanShell 写入 props → 业务线程组通过__P函数引用。整个测试计划里不管挂多少业务接口都能自动携带同一个 token。7. 自动刷新 token跑长时压测不中断7.1 方案 A401 触发式刷新实现自动刷新的思路其实很直觉既然 token 过期会导致接口返回 401那就让脚本在拿到 401 响应码时自动去调一次登录接口把新的 token 更新到全局属性里然后继续往下跑。在 Jmeter 中实现这个逻辑核心是用If 控制器来拦截 401 响应。做法是在业务线程组最开始的位置添加一个 If 控制器条件判断当前请求的响应码是否等于 401如果等于就执行控制器里面的“重新登录”请求。If 控制器的条件写法如下${__P(token_expire_time, 0)} ! 0 ${__time()/1000} ${__P(token_expire_time, 0)}这段条件看起来有点唬人实际上拆开看很直白判断当前时间是否已经超过了 token 的过期时间如果超过了就说明 token 该换了。如果把判断条件换成前一个取样器的响应码也可以用${prev.code} 401但实际压测中我更推荐用过期时间戳来判断因为 401 触发意味着脚本要等到请求真正失败一次才能触发刷新而这就会在结果里留下一条 401 错误记录干扰测试报告数据。通过对过期时间的预先判断可以在 token 还没真正失效前就主动刷新测试结果更干净。7.2 方案 B真实刷新并替换全局 tokenIf 控制器里面放什么内容放一个最小化的登录请求请求参数和 setUp 线程组里的登录请求保持一致然后在它下面挂一个 BeanShell 后置处理器执行与登录线程组相同的逻辑String newToken vars.get(access_token); props.put(global_token, newToken);为什么这里还需要再存一次因为 newToken 如果只存在vars里它依然只对当前线程可见。必须重新props.put(global_token, ...)覆盖旧值后续其他线程发起的新请求才会拿到新 token。流程从逻辑上闭环了但还隐含着一个新问题如果多个线程同时发现 token 过期它们会同时发起登录请求导致登录接口被反复请求多次。这在压测中不是小概率事件尤其是 token 过期的那一瞬间大量并发线程会同时触发刷新。解决这个问题最简单的方式是在“重新登录”请求前面加一个临界控制器Critical Section Controller。临界控制器在 Jmeter 中表现为一个带有锁的容器同一时刻只允许一个线程进入执行其他线程会在锁外等待。具体配置添加“逻辑控制器 → 临界控制器”把“重新登录”请求拖进去然后设置一个锁名称比如global_token_lock。临界控制器本身不能完全避免“多个线程排队登录”的现象但它保证了一个瞬间只有一个线程去刷新 token其他线程等待锁释放后再读取的已经是新 token 了这就够用了。7.3 收尾验证连续跑 1 小时看结果配置完自动刷新逻辑后强烈建议做一次长时运行验证。把线程组循环次数设置成一个比较大的值持续跑一段时间。运行结束后打开查看结果树确认登录接口没有大量失败。业务接口没有出现成片的 401。全局属性值确实在运行过程中发生过更新可以通过在查看结果树里观察不同时间段请求携带的 token 来判断。循环里面需要避开一个隐藏的坑If 控制器里的“重新登录”请求如果配置了断言断言失败会导致取样器报错进而影响整个线程的行为。为了不影响主逻辑建议在“重新登录”请求上取消断言或者把断言的失败容忍级别设置为不影响后续取样器执行。8. 常见问题排查实录8.1 我已经配好了 Header Manager但请求头里没有 token这是新手最常遇到的问题。排查顺序按照下面这个走先看查看结果树中登录接口的响应确认 JSON Extractor 是否真的提取到了值。如果有 Debug Sampler看 response data 里是否有变量值。确认 Header Manager 里的引用写法是否为${__P(global_token)}而不是${global_token}。确认 Header Manager 放在业务线程组级别而不是测试计划级别。如果放在登录线程组里业务线程组是继承不到的。8.2 JSON Extractor 提取的值是空或者报 JSONPath 错误JSONPath 表达式写错是最常见的原因。重新核对登录接口实际返回的 JSON 结构注意大小写JSONPath 是严格区分大小写的。$.data.accessToken和$.data.access_token是两个完全不同的路径一个字母对不上就会提取失败。如果响应里返回的是一个整体字符串化的 JSON数据被包在引号里说明接口返回的字段是嵌套字符串需要先用 Beanshell 做二次 JSON 解析。这种结构比较少见遇到再说不用提前焦虑。8.3 线程组之间传值失败业务线程组始终拿不到 token核心原因十有八九是变量的作用域用错了。vars只能当前线程用跨线程组必须用props。再检查一遍你的 BeanShell 脚本里存的是props.put(...)而不是vars.put(...)取的时候用的是${__P(...)}而不是${...}。Jmeter 有个小工具能帮你检查属性值在业务线程组加一个“调试取样器”它会把所有 vars 和 props 都打出来一眼就能看出全局属性池里到底有没有存上global_token。8.4 压测时 token 刷新频繁触发登录接口被打爆这个问题的根源是 token 有效期短或者刚过期的那一刻大量线程同时触发了刷新。处理方案是加临界控制器限制同时刷新的线程数。同时检查一下刷新登录请求的响应时间是否过长如果每次刷新都要 5 秒以上说明你的登录接口本身性能有问题这不是脚本能解决的需要和开发确认。8.5 token 还没过期就提前失效了这种情况通常是服务端做了单点登录或者 token 会话管理策略导致的比如用户多端登录互踢、token 在白名单中被清除等。这类问题属于业务逻辑层面的限制脚本层只能通过缩短 token 轮换周期来规避没有太好的通用方案。8.6 响应里中文乱码很多项目的接口返回是 UTF-8 编码但如果服务端没有明确在响应头里声明 charsetJmeter 有可能用默认的 ISO-8859-1 解析导致乱码。最简单的处理方式是在 HTTP 请求的高级选项卡里把响应编码设置为 UTF-8。如果还是乱码考虑在请求后加一个 BeanShell 后置处理器做编码转换。不过这个方法只影响你看响应内容不影响 token 提取遇到乱码时优先排查是不是有特殊字符混入 token。9. 关于这套方案我再多分享一点实操心得上面这些步骤我前前后后在各种项目里配置过很多次也帮组里的新人排查过不少问题。最后再分享几个我自己的使用习惯供你参考。第一我用 BeanShell 而不是直接用更主流的 JSR223 Groovy是因为 BeanShell 是 Jmeter 内置支持的不需要额外配置 Groovy 环境对大部分测试团队来说上手门槛低很多。但如果你要拿这套方案做大规模压测还是建议把 BeanShell 换成 JSR223 Groovy运行效率会好一些脚本语法也更接近 Java 工程师习惯的写法迁移成本很小。第二token 全局化的方案并不只服务于“配置”这一个动作。当你把过期时间的判断、自动刷新逻辑做进去之后这套脚本实际上已经具备了持续集成的基本能力。配合 Jenkins 定时跑任务每天早上起来看一封测试报告邮件接口有没有问题一目了然。第三不要迷信“一个万能 token”。不同的接口服务可能使用不同的鉴权体系有的接口用 header 传 token有的用 cookie 传 session有的要求 token 放在 query 参数里。全局 token 解决的是“同一套鉴权体系”下的问题如果你的系统有多个鉴权体系建议分别建独立的线程组和 Header Manager不要混在一个全局配置里。配置全局 token 这件事本质上考验的是对 Jmeter 变量作用域和数据流理解得透不透。希望这篇文章能帮你把这条链路彻底打通后面再遇到相关的接口测试问题至少能有个明确的排查方向。