
上个月帮一个团队排查接口脚本他们在JMeter里明明加了请求头接口还是疯狂返回401。打开查看结果树一看我差点没绷住——Authorization的值是一个不存在的变量原样被拼到了请求头里后端当然不认。这种问题在测试交流群里几乎周周见所有人都会“添加请求头”但真正理解请求头在JMeter里怎么生效、怎么关联、怎么排查的人没那么多。所以我决定把JMeter设置请求头这件事从头到尾讲透包括常规操作、作用域规则、动态token关联、压测场景下的坑。如果你是刚接触JMeter的接口测试新手这篇文章能帮你少走很多弯路如果你已经在跑压测了重点看第3节和第6节应该能解决你一些“莫名其妙”的脚本问题。1. 为什么要手动设置请求头从一次401认证失败说起先说个很现实的场景。开发给了个接口文档你拿着Postman一点就通换到JMeter就死活调不通——返回401、403或者是400。新手最容易怀疑自己“JMeter装错了”其实多数情况下问题出在请求头上。HTTP协议在设计上把请求拆成了几个部分请求行、请求头、请求体。请求头装的是这次请求的“元数据”也就是关于请求本身的信息比如这个请求是什么格式、谁在请求、希望服务器返回什么格式。我用快递打比方请求体是你寄的包裹请求头就是快递单面上的寄件人、收件人、联系电话和“易碎品”标签。快递员看了快递单才知道怎么处理这个包裹服务器看了请求头才知道怎么处理这个请求。JMeter发HTTP请求的时候会带一些默认请求头比如Host、Content-Length这类基础信息。但真正让接口“认识你”的认证信息、内容类型声明、自定义业务字段它不会自动生成所以你必须手动设置。举几个我经常碰到的必配场景接口需要登录态很多项目的接口要求请求头带token或者Cookie。明确告诉后端Body是什么格式你发的是JSON就要声明Content-Type: application/json如果不声明部分框架会按表单格式解析直接报参数错。安全校验网关要求带签名sign、时间戳timestamp或者X-Requested-With一类的自定义头。模拟客户端环境有些接口会校验User-Agent要求必须是特定版本App或者微信浏览器。说白了请求头就是接口的“门禁卡”。你能调通Postman是因为Postman帮你在很多地方自动做了“隐性”处理而JMeter更像一个“裸工具箱”什么都要自己声明清楚。理解这一点后面所有设置请求头的操作才不算瞎填。下面这张表是我在实际项目里最常维护的请求头字段供参考请求头字段作用示例值Content-Type声明请求体的媒体类型application/json;charsetUTF-8Authorization认证凭证常用Bearer TokenBearer eyJhbGciOiJIUzI1NiIs...Accept期望服务器返回的格式application/jsonCookie携带会话标识JSESSIONIDxxx; tokenyyyUser-Agent客户端身份标识Mozilla/5.0 (Windows NT 10.0; Win64; x64)X-Requested-With标记请求为异步Ajax请求XMLHttpRequestX-Timestamp自定义时间戳常用于签名校验1715760000X-Sign自定义签名值MD5(timestampsecret)注意请求头的字段名在HTTP规范里是大小写不敏感的但很多接口的程序员用常量校验写错了照样报错。所以不能想怎么写就怎么写按接口文档来。2. JMeter里设置请求头的三种主流方式2.1 HTTP信息头管理器最常规的做法这是90%的场景都在用的方案。它的思路特别简单把请求头以“配置元件”的形式挂在测试计划里JMeter执行HTTP请求时会自动把配置好的Header拼到请求里。添加路径是右键线程组也可以是某个HTTP请求采样器→ 添加 → 配置元件 → HTTP信息头管理器。右键位置不同生效范围不同这一点后面第3节细说。打开HTTP信息头管理器界面是一个两列表格。左边Name填写请求头名称右边Value填写值。比如NameContent-TypeValueapplication/json;charsetUTF-8NameAuthorizationValueBearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.xxx这里有个经验之谈即便JMeter有时候能自动根据请求体推断Content-Type我也建议你在这里显式写出来。因为自动推断的规则在后端框架比较复杂时不一定符合预期显式声明确认后端能正确解析。2.2 JSR223预处理器动态添加签名头、时间戳的解法接口要做签名校验的情况越来越常见。请求头里的sign值是根据时间戳、请求体内容动态算出来的不可能写死。这时候就得在请求发出前用脚本动态生成Header。JMeter里实现动态Header有两种姿势我按推荐程度排序第一种在JSR223预处理器里把计算好的值存入JMeter变量界面里的HTTP信息头管理器用${变量名}引用。// JSR223预处理器语言选择 Groovy def timestamp System.currentTimeMillis() / 1000 as Long def secret 你的密钥 def sign org.apache.commons.codec.digest.DigestUtils.md5Hex(timestamp secret) vars.put(request_timestamp, timestamp.toString()) vars.put(request_sign, sign)然后在HTTP信息头管理器里这样填NameX-TimestampValue${request_timestamp}NameX-SignValue${request_sign}第二种在JSR223预处理器里直接操作当前取样器的HeaderManager对象把Header一个一个Add进去。import org.apache.jmeter.protocol.http.control.Header def timestamp System.currentTimeMillis() / 1000 as Long def secret 你的密钥 def sign org.apache.commons.codec.digest.DigestUtils.md5Hex(timestamp secret) def headerManager sampler.getHeaderManager() if (headerManager null) { headerManager new org.apache.jmeter.protocol.http.control.HeaderManager() sampler.setHeaderManager(headerManager) } headerManager.add(new Header(X-Timestamp, timestamp.toString())) headerManager.add(new Header(X-Sign, sign))两种方式哪个好如果只是动态值我更推荐第一种因为脚本里只算数据最终的Header集合还是留在可视化界面上别人接手脚本时一眼就能看到这个请求带哪些头。第二种适合“头是否存在要根据条件判断”的复杂场景脚本可控性更强。这里必须多说一句JSR223的脚本语言选Groovy不要用Beanshell。JMeter在早期版本里Beanshell用得很多但它的执行性能比Groovy差一大截压测时会导致CPU很高。从JMeter 3.x开始官方就推荐Groovy了我自己的脚本现在全部用Groovy稳定性和性能都好很多。2.3 直接编辑jmx脚本文件批量管理和协同修改有些人可能不知道JMeter的测试计划保存为jmx文件后本质上是XML格式。HTTP信息头管理器的内容也会序列化在文件里。当需要批量修改大量脚本中的请求头时用GUI一个个点效率很低直接用文本编辑器配合全局替换反而更快。jmx里对应的节点长这样HeaderManager guiclassHeaderPanel testclassHeaderManager testnameHTTP信息头管理器 enabledtrue collectionProp nameHeaderManager.headers elementProp name elementTypeHeader stringProp nameHeader.nameContent-Type/stringProp stringProp nameHeader.valueapplication/json;charsetUTF-8/stringProp /elementProp elementProp name elementTypeHeader stringProp nameHeader.nameAuthorization/stringProp stringProp nameHeader.valueBearer ${access_token}/stringProp /elementProp /collectionProp /HeaderManager比如你有20个jmx脚本都带同一个测试环境的token占位符用脚本批量替换Header.name或Header.value的值比逐个打开GUI修改高效得多。但要提醒一句编辑jmx之前一定备份如果XML格式不对JMeter打开脚本会失败到那时候恢复就麻烦了。3. Header Manager的作用域与优先级为什么设置了却不生效这是JMeter请求头问题里最隐蔽、也最让人暴躁的一类。很多人明明在HTTP信息头管理器里填了token执行的时候发现有的请求带上了有的请求没带上甚至同一个请求带了两个不同的token。原因大概率是作用域没搞明白。作用域这个概念说白了就是“配置元件在测试计划树上的位置决定了它对哪些取样器生效”。HTTP信息头管理器作为配置元件它的生效范围遵循“父子继承”原则。我把常见位置和效果整理成了一张表放置位置生效范围测试计划下对测试计划内所有线程组、所有请求生效线程组下对该线程组内所有请求生效某个HTTP请求下只对该请求生效这个规则看着简单实际项目里最容易出错的是多接口脚本有人把HTTP信息头管理器放在线程组下里面填了Authorization: ${token}本意是只给“用户信息查询”这个请求用结果发现“登录接口”这种本来不应该带token的请求也带上了。如果登录接口的后端不校验多余的Authorization头那就相安无事如果后端严格校验比如发现Authorization头的格式不对就直接拒绝登录请求就会带着一个错误的token一起去导致登录失败。再一个高频坑是Header的优先级覆盖问题。JMeter允许同一个请求的作用域里同时存在多个HTTP信息头管理器比如线程组有一个请求子级也有一个。这时候执行的规则是子级配置元件会覆盖父级同名字段异名字段合并。举一个我真实处理过的案例。场景是一个压测脚本线程组下放了公共头Content-Type: application/json但在“文件上传”这个请求的子级又放了一个HTTP信息头管理器里面写的是Content-Type: multipart/form-data。跑压测时文件上传请求使用的Content-Type确实是multipart/form-data因为子级覆盖了父级的同名配置。公共请求还是用application/json互不影响。但如果反过来你把公共头放在测试计划层把特定请求的头放在线程组层同名Header的覆盖顺序就要小心验证了。我自己总结的实操建议是同名Header只维护一份别在多层同时定义多层都要放Header Manager的话要保证不同名或者干脆把公共Header全部放线程组个别请求的特殊Header单独放请求子级并提前检查覆盖关系。还有一个值得单独拿出来说的场景是重定向。HTTP请求采样器里勾选了“跟随重定向”当服务端返回302时JMeter会按照Location跳转。但实际测试中我发现跨域跳转时原始请求的Header并不一定完全继承尤其是Authorization这类认证头很容易在跳转后的第二次请求中丢失。遇到这种场景我会先把“跟随重定向”取消手动在结果里拿到Location再用一个单独的HTTP请求去发起第二次请求并且在第二个请求里显式加上需要的Header。这样虽然脚本步骤多一些但每一跳的请求头都在掌控之中。4. 动态请求头实战token关联与参数化4.1 登录后提取token后续接口自动带上现在绝大多数项目的接口都要求先登录拿token后续请求在Header里带着这个token。JMeter里做这件事的标准流程是登录请求 → 从响应中提取token → 存成变量 → 在HTTP信息头管理器里引用变量。第一步还是先发登录请求确认返回的JSON结构。假设登录接口返回{ code: 0, message: success, data: { access_token: eyJhbGciOiJIUzI1NiJ9.xxx, expires_in: 7200 } }第二步在登录请求上右键 → 添加 → 后置处理器 → JSON提取器。配置如下Name of created variablesaccess_tokenJSON Path expressions$.data.access_tokenMatch No.1Default ValuesTOKEN_NOT_FOUND这里要解释一下Match No.如果响应里只有一个token填1就够了如果返回的是列表你得先想清楚要取值第几个再填对应下标。第三步添加一个调试取样器Debug Sampler先跑一次在查看结果树里确认access_token变量有没有正确提取出来。这一步很多新手会跳过结果等到调试的时候发现变量名写错了排查半天。我的习惯是提取变量后一定先加Debug Sampler跑一次成本极低但能节省后面大量排查时间。第四步在HTTP信息头管理器里把Authorization这一行的Value写成Bearer ${access_token}执行后续接口时JMeter会自动把变量替换成登录接口提取到的token。这里有个小细节Bearer后面有个空格漏掉或者写成Bearer${access_token}会导致token拼出来不是合法的认证头格式。这种问题Postman会自动帮你补好JMeter不会完全依赖脚本编写者细心。4.2 多用户并发场景CSV参数化token上面说的是“单用户登录一次后面带着这个token跑”的玩法。可一旦做并发压测比如模拟100个用户同时操作如果100个线程共用一个token很多人可能觉得没什么——反正接口能通。可后端如果做了用户维度限流、幂等控制、或者业务上有“同一账号不能并发登录”的逻辑你压出来的数据就全是报错根本不算数。正确的做法是准备一个CSV文件每一行放一个用户的账号、密码或者已经生成好的token然后通过CSV数据文件设置让每个线程取不同的值。CSV数据文件设置的添加路径右键线程组 → 添加 → 配置元件 → CSV数据文件设置。关键配置项文件名指向你的CSV文件路径文件编码UTF-8变量名称username,password英文逗号分隔和你CSV的列一一对应分隔符英文逗号是否允许带引号False线程共享模式每个线程有自己的CSV指针在登录接口参数里引用${username}和${password}登录成功后把token提取到变量access_token再在HTTP信息头管理器里写Authorization: Bearer ${access_token}。因为线程之间变量都是独立的每个线程拿到的token就不会互相覆盖。这里有个高频反面教材有人在用户自定义变量User Defined Variables里写死了一个token然后所有线程都在用同一个。这不叫参数化那叫“共享登录态”一旦压出问题你不知道是接口性能不行还是token被挤下线了。多用户压测一定要把用户数据分开。4.3 签名Header的Groovy实现从假值到真实签名很多后端接口在网关层会做签名校验请求头里不仅有token还要求带上时间戳和签名串。签名算法通常是把时间戳、请求体、密钥做拼接然后MD5或者SHA256加密。由于签名是动态的我一般在每个请求前挂一个JSR223预处理器用Groovy生成。import org.apache.commons.codec.digest.DigestUtils def timestamp System.currentTimeMillis() / 1000 as Long def secret 项目的密钥 def body sampler.getArguments().getArgument(0).getValue() def signContent timestamp body secret def sign DigestUtils.md5Hex(signContent) vars.put(timestamp, timestamp.toString()) vars.put(sign, sign)然后HTTP信息头管理器里加两行NameX-TimestampValue${timestamp}NameX-SignValue${sign}这里的body如果请求体里是JSON拼接顺序一定要和后端约定一致。后端拼接字段的次序跟你脚本里不一样签名永远对不上。我第一次写这种脚本就踩过坑后端Java代码里拼的是timestamp secret body我脚本里写成了timestamp body secret结果怎么算都不对。排查了半小时。所以这类脚本写完第一件事就是拿一个后端返回“签名错误”的请求日志对照拼接顺序。4.4 提取Header里的值另一种“反向关联”说完了请求头动态设置补一个大家容易混淆的需求有些场景是你需要把响应Header里的值提取出来然后再传给下一个请求。比如登录接口的token不是放在响应体里而是放在响应头的Set-Cookie或某个自定义头里。这时候用JSON提取器就不行了得换成正则表达式提取器或者用BeanShell/JSR223。以响应头X-Auth-Token: abc123为例正则表达式提取器配置Apply toMain sample and sub-samples字段响应头Response Headers正则X-Auth-Token: ([^\\s])模板$1$这个需求第一眼看会觉得“不都是取变量吗”但实际操作路径完全不同。很多人拿JSON提取器去提Header的值提半天是空的就是没搞明白数据的存放位置。5. 请求头排错的完整排查链路遇到请求头相关的报错别急着一遍遍改值重跑。我有一个固定的排查链路按这个顺序来大多数问题五分钟内能定位。查看结果树里的“请求头”数据第一个动作永远是去查看结果树里看“HTTP”这一栏里的请求头数据。操作路径添加监听器 → 查看结果树 → 执行脚本 → 选中一个取样器 → 右侧选择“HTTP”标签页 → 查看“Request Headers”。这里能直接看到JMeter实际发出的请求头比如有没有正确带上Authorizationtoken变量有没有被正常替换成真实值。需要警惕的一种情况是请求头里的值仍然是${access_token}原样输出。这说明变量没被解析。原因一般有两种一个是变量拼写和提取器里定义的名字不一致另一个是HTTP信息头管理器的位置在变量提取生效之前就被执行了。用Debug Sampler验证变量是否存在当怀疑是变量问题时在请求前面加一个调试取样器Debug Sampler运行后查看结果树里的“Response data”能看到当前线程下所有JMeter变量的值。确认access_token存的到底是什么——是没提取到显示为默认值TOKEN_NOT_FOUND还是根本没有定义。这一步能快速区分是提取器的问题还是Header配置的问题。和Postman或curl的“正常请求”对比如果JMeter里发出的请求头看起来都正常但接口还是报错就找一个能正常调通的工具做对照。Postman能通JMeter不通我见过最典型的原因有以下几点项目差异点Content-TypePostman选JSON时会默认加application/jsonJMeter不一定加AcceptPostman默认带Accept: */*JMeter可能不带User-AgentPostman带自己的UAJMeter带Apache HttpClient的UA自动计算长度Postman自动算Content-LengthJMeter也会算但格式有时不同CookiePostman有Cookie自动管理JMeter没有则默认不带把这些差异逐项补齐再测一次经常就通了。状态码与请求头问题的对应关系根据我的经验请求头配置错误和HTTP状态码之间有一些常见的对应关系可以作为排查的快速索引状态码常见请求头原因400 Bad RequestContent-Type或Header格式错误参数类型不匹配401 UnauthorizedAuthorization缺失、token为空或过期403 Forbidden签名错误、token无效、IP白名单等404 Not Found一般和请求头关系不大先查URL路径406 Not AcceptableAccept声明的响应格式与后端返回格式不匹配415 Unsupported Media TypeContent-Type声明的格式不是后端支持的格式429 Too Many Requests多用户共用了token被限流了一个真实案例Content-Type被网关拒了今年遇到一个项目测试同学说JMeter调公司内部一个接口返回415。我过去一看请求头里Content-Type: application/json;charsetUTF-8看起来没毛病。但翻接口文档发现网关层要求必须前端不能带charset只接受application/json。把charset去掉之后立刻通了。这个案例告诉我们请求头的“合法”不等于“符合后端预期”。有些后端解析器严格到连字符集声明都挑。遇到这种问题多问一句开发同学“你们的网关对Content-Type有没有特殊要求”6. 压测场景下请求头的几个隐藏坑如果你用JMeter不只是做功能接口调试而是要做性能压测请求头这块有几个隐藏坑平时不跑压测根本不会暴露。**压测时一定要关掉查看结果树。**这个监听器在调试脚本时很有用但压测时开着它会大量消耗IO和内存严重拉低TPS。更坑的是如果请求头里有敏感信息比如token压测结束后查看结果树里全是真实token脚本一旦外传就是泄漏风险。正规做法是压测时只保留聚合报告或使用命令行模式调试时再开结果树。**并发场景的token隔离看上去是请求头问题本质上是压测设计问题。**多个线程共用一个token后端如果做了登录态互踢你会发现压测一开始大量线程被踢下线报401像瀑布一样刷屏而从脚本层面看请求头里确实有token。这种情况不是Header没设置是每个线程都必须自己的token。解决办法就是我前面讲的CSV参数化一个用户一个token问题立刻消失。**动态签名Header在高并发下要小心时间戳的精度。**用System.currentTimeMillis() / 1000取的是秒级时间戳如果你的脚本里时间戳是每个请求动态生成的而同一秒内大量请求都会上报同一个timestamp这本来没问题。但如果你在JSR223里用了固定时间戳或者为了“省事”把时间戳放在用户自定义变量里那么所有线程发的请求时间戳都是同一个签名校验如果加了“时间戳不能太旧”的校验压测跑久了必然大量失败因为时间戳过期了。结论是签名相关的时间戳必须在每个请求的预处理器里动态生成不能在配置阶段写死。**请求头膨胀问题。**有些团队的脚本习惯不好一切认证信息、业务标识、调试字段全往Header里塞导致单个请求的Header体积非常大。单看一个请求没什么感觉但压测场景下每秒几百上千个请求每个请求带几个KB的Header对带宽和后端解析都是很直接的消耗。能用Cookie或Body传递的数据尽量别全堆到Header里。如果必须带压测结果出来之后要结合带宽占用一起分析避免把网络瓶颈误判成应用性能问题。**命令行压测模式下请求头错误更难发现。**很多人压测用jmeter -n -t script.jmx -l result.jtl这样的命令跑不用GUI。这时候如果脚本里请求头配置有问题比如token提取失败你看到的只是JTL报告里大面积的错误率飙升但看不到具体请求长什么样。所以我每次命令行压测前都会先用GUI跑一次单线程脚本把结果树里的请求头核对一遍确认没问题了再关掉GUI用命令行跑。这个过程多花五分钟能避免一次无效压测。最后再分享一个我个人的工作习惯每次写完或者改完一个带请求头的脚本我都会手工mock一个“故意填错token”的测试请求确认接口确实返回401再手工mock一个“正确的token”确认接口返回200。如果这两种情况的响应一致说明你的脚本可能根本没走对这个接口的鉴权逻辑。这个小实验能帮你提前发现脚本层面的致命错误比直接跑大流量再去捞日志高效得多。