☰
JMeter压测如何只登录一次?Cookie与Token处理全解析
2026/10/24 4:57:28 网站建设 项目流程

做性能压测的同行应该都有过这种体验:脚本搭好了,线程数、循环次数、聚合报告都配得整整齐齐,结果一跑起来,登录接口的请求量比被测接口还多,服务端的登录模块先被压垮了。之所以会这样,多半是因为JMeter脚本里登录的逻辑没设计好。这篇我会把“登录一次,然后持续压测其他接口”的做法讲透,覆盖单用户场景与多用户并发场景,Cookie与Token两种登录态的处理方式,以及实际操作中常见的坑和排查思路。

1. 为什么“只登录一次”在压测里这么重要

1.1 你压的到底是什么

这个问题的根源,其实是很多人在设计压测脚本时,把登录接口当成了“必须每轮都跑”的部分。但实际上压测的核心目标是验证业务接口的性能指标,比如你关心的下单接口TPS、商品列表接口的响应时间、消息推送接口的吞吐量,这些才是被测对象。登录接口本身如果也是被测对象,那另说;但在大多数压测场景里,登录只是“前置条件”,它的存在是为了拿到一个合法身份去访问业务接口。

登录接口通常不轻,一次登录请求往往涉及密码加解密、用户校验、会话创建、日志埋点等多个环节,比普通的查询接口要重得多。如果脚本里让每个线程的每一次循环都先登录一次,那压测产生的请求总量里,登录占比会惊人地高。我之前接过一个电商项目,脚本没做任何登录态控制,10个线程跑10轮,登录接口的请求数是100次,而真正的业务接口下单才跑了100次,登录请求和业务请求1比1,最后瓶颈先出在登录模块上,业务接口的极限根本测不出来。

1.2 登录态的两种形态:Cookie 与 Token

登录态的传递方式决定了你在JMeter里怎么做。早期Web项目大多是Session-Cookie机制,用户登录成功后,服务端会在响应头里返回Set-Cookie,浏览器自动保存,后续请求自动携带。对应到JMeter,就是HTTP Cookie管理器这个配置元件,它负责保存响应中的Cookie,并在后续同域名的请求中自动携带。

现在更常见的API项目则是Token机制,登录接口返回一个JSON,里面带着token、access_token、或者JWT之类的字段。后续接口调用时,需要在请求头里显式加上Authorization或者自定义的token头。这种情况下Cookie管理器帮不上忙,你需要用后置处理器把token从响应里提取出来,再用HTTP信息头管理器或请求参数带过去。这两种机制的处理路径完全不同,下面我会分别展开。

1.3 先想清楚你的用户模型

在动手配置脚本之前,一定要先回答一个问题:这次的压测是单用户压场景,还是多用户并发压场景?这两种场景下,“登录一次”的含义完全不一样。

单用户场景通常用于功能验证、小流量冒烟测试,或者压测某个接口在单连接下的极限性能。这种场景整体只登录一次,所有业务请求复用同一个会话。多用户并发场景则要求每个虚拟用户各有一个登录态,各自的业务请求带上各自的凭证,模拟的是真实环境里多个用户同时在线操作为系统带来的并发压力。如果你在单用户场景里用了多用户的设计,就会平白无故增加大量登录流量;如果你在多用户场景里用了单用户的全局token,所有线程共用一个身份,轻则无法区分用户,重则触发服务端并发冲突,压测结果完全失真。所以,第一步永远是明确用户模型。

2. 单用户场景:setUp Thread Group 是最稳的解法

2.1 配置步骤:从新建脚本到业务接口跑通

如果压测目标就是“只有一个用户反复操作业务接口”,最稳妥的做法是用setUp Thread Group来承载登录逻辑。这个线程组的执行顺序在所有普通线程组之前,天然就是干初始化工作的。

具体配置流程我拆成几步:

第一步,新建测试计划,右键添加setUp Thread Group,线程数设为1,循环次数设为1。这个配置的含义就是整个压测过程中,登录逻辑只执行一次。

第二步,在setUp Thread Group下添加登录请求的Sampler,填写协议、域名、端口、路径和登录参数。参数从哪来?如果登录接口是JSON格式,用HTTP Header Manager加一个Content-Type: application/json,然后在Body Data里写JSON体。

第三步,添加普通线程组,线程数和循环次数按你的压测目标配置,比如100线程循环30次。业务请求的Sampler放在这里。

第四步,在普通线程组下面添加聚合报告、查看结果树等监听器,跑完直接看数据。

提示:setUp Thread Group里的线程数和循环次数请务必都设为1。有人在这个线程组里配了多线程或多循环,登录就会执行多次,那“只登录一次”的目标直接失效。

这个结构跑起来后,请求顺序一定是:登录请求先执行,而且只执行一次,然后业务请求反复执行。你在结果树里可以直观地看到登录请求只有一条,业务请求有一大堆。

2.2 Cookie管理器在这里扮演的角色

如果是Session-Cookie机制的登录,上面的步骤基本就能跑通了,但需要搭配HTTP Cookie管理器。这里有一个特别容易踩的坑:Cookie管理器放置的位置决定了它的作用域。

如果你把Cookie管理器放在setUp Thread Group下面,那它只对setUp线程组里的请求生效,登录成功后在setUp线程内保存的Cookie,普通线程组的请求是拿不到的。因为JMeter里Cookie的存储跟线程相关,跨线程组不会自动共享。正确的做法是把HTTP Cookie管理器放在测试计划(Test Plan)这一层,这样它才对整个测试计划的所有线程组生效,Cookie才能从setUp线程传递到普通线程。

实际操作中,我在测试计划级别放一个HTTP Cookie管理器,里面默认的“User-Defined Cookies”留空即可,JMeter会自动从登录响应里解析Set-Cookie并保存。需要注意不要勾选“Clear cookies each iteration”,这个选项一旦勾上,每一轮循环都会清空Cookie,那你的登录态就没了,业务接口马上全部401。默认是不勾的,但很多人改过配置忘了改回来。

2.3 别忘了给登录请求加断言

登录请求这种前置步骤,很多人觉得“能通就行”,其实不然。压测时如果登录失败,业务请求会跟着全部失败,但如果你只看聚合报告的总数据,很容易被表面上的大量请求误导,以为只是业务接口性能差,实际上根因是登录态没拿到。

给登录请求加断言的目的是把“登录失败”这件事尽早暴露出来。右键登录Sampler,添加响应断言(Response Assertion),判断响应文本里是否包含某个登录成功的标识字段,比如"code":0或“登录成功”,或者用响应状态码断言,状态码为200。再配一个断言结果监听器,压测过程中如果登录失败,断言结果会立刻标红,日志也会打印错误,不用等到看完整报告才发现。

另外,很多系统的登录接口会有验证码、风控、IP限制等机制。压测脚本如果遇到登录接口返回验证码要求,一定要先确认测试环境是否放开了这些限制,或者准备一个可重复使用的测试账号,否则登录断言会给你报一片红。

3. Token 接口:从登录响应里提取凭证并全局复用

3.1 正则表达式提取器 vs JSON提取器

Token型接口和Cookie型接口的处理方式差别很大。Token不在Cookie里,而是在登录接口的响应体里,通常是JSON格式,要么是{"token":"eyJhbGci..."},要么是{"data":{"accessToken":"xxx"}}。你需要先从响应里把这个字段抠出来,再注入到后续请求头中。

JMeter里做“抠字段”常用的有两把工具:正则表达式提取器和JSON提取器。早期版本没有JSON提取器,大家用的都是正则表达式提取器,比如响应是{"token":"abc123"},用正则表达式提取器,配置查询字段为响应体,正则写成"token":"([^"]*)",模板$1$,匹配序号1。这样就能把abc123提出来。缺点是正则表达式写起来容易出错,遇到JSON里有转义字符、嵌套结构时很容易匹配错。

JSON提取器就友好得多,它用JSONPath表达式,比如$.token或$.data.accessToken,尤其适合嵌套深的响应结构。还有个好处是JSON提取器可以提取多个字段,用分号分隔,变量名也对应分号分隔,适合一次性提取token和用户ID等。我的建议是:响应是JSON就用JSON提取器,响应不是JSON(比如HTML页面、纯文本)才用正则提取器。现在网上许多老旧教程还在教正则写法,但新项目用JSON提取器显然更省心。

3.2 把Token存进JMeter属性的方法

提取到token之后,下一个问题是怎么让所有业务请求都用上它。这里要先分清两个概念:JMeter变量(vars)和JMeter属性(props)。

变量是线程内的,每个线程有自己的变量副本,同一个线程组里,其他线程看不到这个线程的变量。属性是全局的,所有线程、所有线程组都能通过${__P(propName,)}引用。在setUp Thread Group里登录成功后,如果你把token存在变量里,普通线程组根本读不到,因为setUp线程和普通线程是不同线程。所以跨线程组传递token,必须存到属性里。

具体做法是:在登录请求下添加后置处理器,先用JSON提取器把token提取到一个临时变量,比如loginToken;再添加一个JSR223 PostProcessor,写一行Groovy代码:

props.put("globalToken", vars.get("loginToken"));

这样token就进全局属性了。业务请求需要带的Header,比如Authorization: Bearer ${__P(globalToken,)},在HTTP信息头管理器里引用这个属性就行。

如果不想用JSR223,也可以直接用${__setProperty(globalToken, ${loginToken},)}函数来设置属性,但涉及特殊字符时有转义风险,Groovy方式更稳。

3.3 方案对比:vars、props、CSV预置

我整理了一下三种常见的token传递方式,各有适用场景:

方式存储位置作用域适用场景
JMeter变量vars当前线程单线程内同一个线程内登录后提取token,后续同线程接口用
JMeter属性props全局所有线程共享setUp线程登录一次,普通线程组所有线程复用token
CSV预置token文件按行分配给线程压测前已有token,直接批量调度

变量方式适合在同一线程组里,登录Sampler提取token后,后续的另一个Sampler用同一个变量引用,这样的场景不需要setUp线程组。属性方式适合单用户或者少量用户全局复用一个登录态的简化场景,也是最常见的“只登录一次”的实现方式。CSV预置方式适合token有效期长、测试环境会提前生成批量token的情况,这种方式完全不依赖登录接口,压测脚本不发起登录请求,压力最干净。

选择的关键在于“登录态复用的粒度和跨线程需求”。我见过不少新手把变量当属性用,脚本明明在setUp线程里登录了,普通线程组里引用${loginToken},结果永远是空值,就是因为没搞清这两个存储的作用域。

4. 多用户并发:每个用户登录一次,但整体不是一次

4.1 Once Only Controller 的真实语义

很多人在网上搜“JMeter只登录一次”,搜到的答案里出现最多的是“仅一次控制器(Once Only Controller)”。但这里要特别提醒:Once Only Controller并不是“整个测试计划只执行一次”,它的真实语义是“每个线程在循环中只执行一次”。线程组里配置了100个线程,那Once Only Controller里的内容会被执行100次,每个线程各一次。

这个性质在多用户并发场景里恰好有用:每个虚拟用户用自己的账号登录一次,然后进入业务接口的循环,这样可以模拟真实的并发用户行为。但如果你误把它用在单用户全局复用的场景里,就会得到你不想看到的结果——比如一个线程组1个线程,循环1000次,登录逻辑放在Once Only Controller里,那登录确实只执行1次,这没问题;但如果线程组是10个线程,循环次1000次,登录会执行10次,每次都是独立登录。所以在用Once Only Controller之前,先要确认你的用户模型是“每用户登录一次”还是“全局登录一次”,两者完全不是一回事。

还有一个细节:Once Only Controller只在线程组的循环迭代中保证一次,如果你把登录Sampler直接放在线程组下、业务采样器再放在循环控制器里,每次线程启动时登录采样器也会执行。所以要配合循环控制器:登录放在Once Only Controller里,业务请求放在循环控制器或普通采样器下,这样每个线程启动后先登录,再循环跑业务。

4.2 CSV参数化让每个线程有独立身份

多用户并发压测里,每用户一个登录态的前提是每用户一个账号。账号从哪里来?最常用的是CSV Data Set Config,也就是通过CSV文件批量参数化用户名和密码。

配置步骤是:准备一个CSV文件,第一行是字段名,比如username,password,后面每行一个用户,然后在测试计划里添加CSV Data Set Config,配置文件名、变量名(如userName,passWord)、分隔符、是否允许带引号等。关键设置是“线程间共享”,默认的All threads是指所有线程按行读取,每个线程拿到不同行,这对多用户并发刚好合适。注意CSV文件在服务器端时要写绝对路径,分布式压测时每台机器上都要有一份同样的CSV文件。

参数化之后,登录请求里用户名密码分别填${userName}和${passWord}。这样每个线程登录时用的就是独立账号,每个线程获取到属于自己的cookie或token,互不干扰。如果你偷懒所有线程都用同一个账号去登录,服务端可能因为同账号并发登录产生会话互踢,或者触发风控,压测过程中登录成功率会惨不忍睹。所以CSV参数化这一步不能省。

4.3 全链路压测常见的“登录N次”模型设计

真实业务场景中的登录模型不是“0次”也不是“无限次”,而是“有比例的N次”。比如一个在线商城,用户平均使用时长半小时,登录一次后会浏览、下单、支付多个操作,所以登录请求和业务请求的比例可能是1比30或1比50。

要在JMeter里实现这种带登录的混合压力模型,通常用两个普通线程组:一个线程组专门跑登录请求,另一个线程组专门跑业务请求,通过配置线程数和循环次数来控制比例。这个方案不如setUp Thread Group干净,但它符合“登录也是真实压力的一部分”的混合场景压测需求。如果登录完全不是压测目标,那就用setUp一次搞定;如果登录也是被测范围的一部分,就把它单独作为一个线程组,按比例分配权重。

还有一种常见做法是加一个吞吐量控制器(Throughput Controller),在同一个线程组里控制登录请求占用的执行百分比。不过吞吐量控制器调的百分比是“迭代次数占比”,在并发场景下更细致,适合登录和业务请求混在一个线程组里的情况。这个模型设计的关键是:无论你用什么工具控制比例,都要确保登录请求的执行次数符合预期,并且不影响业务接口的压力计量。

5. 常见问题与排查技巧实录

5.1 业务接口 401/403 的排查路径

这是压测群里问得最多的问题:“我setUp登录成功了,业务接口还是401。”原因通常有几种:token没提取到、token没传对Header、token过期、多用户共用了一个token但服务端把它踢了。

排查路径我一般这样走:第一步,打开查看结果树,找到登录请求的响应,确认登录确实成功,并且响应里有token字段。第二步,找到业务请求的采样器,查看它的“请求头”实际发出去的Cookie或Authorization是什么。很多人在这一步就发现问题了——Header里根本没有token,或者token值是空。第三步,如果token值显示为${__P(globalToken,)}这种字面量,说明属性引用没生效,检查属性名拼写和是否在HTTP Header Manager的“名称”和“值”两栏填反了。第四步,如果token是正确的但还是401,那就要看服务端的token有效期设置,测试环境经常把token过期时间设得很短,比如10分钟,压测跑到后面就全员过期了。解决办法是调长测试环境的token有效期,或者定期重新登录。

5.2 Cookie 没有生效的原因

Cookie场景下最常见的坑是:登录成功了,但业务请求居然没有携带Cookie。我遇到过几种典型原因。

第一种,Cookie管理器的作用域不对,就像前面说的,放在setUp Thread Group下面,那普通线程组根本拿不到Cookie。第二种,登录请求和业务请求的域名不一致,Cookie是按域存储的,如果登录用的是testapi.example.com,业务请求用的是api.example.com,Cookie不会带到另一个域。第三种,服务端设置了HttpOnly或其他特殊属性,虽然JMeter一般会正常保存,但某些自定义的Cookie管理器配置或代理设置会影响解析。第四种,响应里的Set-Cookie字段被重定向丢弃了,如果你登录请求默认跟随重定向,而第一次响应是302,中间一次Set-Cookie没有保存到。

排查时,看查看结果树里的登录响应,确认响应头里有没有Set-Cookie字段;再看业务请求的“请求体/请求头”,里面有没有自动带上cookie。如果Set-Cookie存在但业务请求没带,就检查Cookie管理器的位置和作用域。如果Cookie根本没有在响应中出现,那就是测试环境或登录逻辑的问题,需要开发配合处理。

5.3 token 被并发覆盖的经典场景

如果你的脚本是每个线程登录后把token存到全局属性props里,并发跑的时候会出现一个经典问题:100个线程同时启动,每个线程都登录,每个线程都把token写到同一个props里,后写的把先写的覆盖了。结果就是所有线程都拿着最后一个登录用户的token去请求业务接口,如果服务端发现token和用户不匹配,或者新token把旧token踢下线,业务接口就会大量报错。

这说明一个设计原则:全局props适合存“全局共享”的登录态,比如单用户压测的token;不适合存“各线程独立”的登录态。多用户并发压测时,正确的做法是让token存在于线程级别,也就是每个线程登录后把token存在变量里,后续请求用它自己的变量。这样就不会互相覆盖。

那如果业务请求和登录请求在一个线程组里,登录后token存在变量里,后续业务请求直接用${token},这没问题。如果登录在setUp里,业务在另一个线程组里,又想每个线程有独立token,那就不适合用setUp,而是把登录放在普通线程组里配合Once Only Controller。这里要再次强调:工具没有绝对的对错,关键是匹配你的用户模型。

5.4 登录接口耗时被算进业务压测结果

有些小伙伴把登录和业务请求放在同一个线程组里,登录在上、业务在下,一个循环里登录一次再业务几次。虽然执行上满足“每轮只登录一次”,但聚合报告会把登录请求的TPS、响应时间、错误率全部混在测试结果里。如果你的目标是只评估业务接口的性能指标,这个方案就污染了数据。

解决办法是用监听器的过滤器,或者在分析报告时只关注业务接口对应的Sampler行。聚合报告本身是逐个Sampler统计的,所以你可以只看业务接口的那一行数据。如果不想让登录请求出现在报表里,还可以考虑对登录请求用“禁用”,或者把登录做成setUp线程组里的逻辑,那它默认就不会和普通线程组混在一起看。更彻底的做法是设计脚本时就把登录和业务分到不同的线程组,结果天然分开。压测报告里也建议同时记录登录接口的请求量,说明它对整体压力的贡献比例。

还有一个相关的问题是:登录接口如果被设置为不采样,那它消耗的资源并不会消失,它还是真实跑了。所以在设计压测方案时,要明确登录压力是“有效压力”还是“无效消耗”。如果登录接口会给服务端造成缓存失效、会话表暴涨等副作用,仍然会影响业务接口的表现,这点需要在结果分析时留意。

5.5 提取器不生效:执行顺序问题

提取器是后置处理器,它的执行顺序是:先执行Sampler请求,然后执行该Sampler下的所有后置处理器,最后才能让后续请求引用提取到的变量。如果你把提取器放在了线程组级别或者放在了错误的Sampler下面,或者在一个Sampler完成后立刻用另一个Sampler去引用变量,那么后者读取时变量还没创建,值就是空的。解决方法是把提取器紧跟在提取来源的Sampler下面,并且在后续使用变量的Sampler前确认前一个采样器已经执行完成。

还有一个顺序坑:如果你在setUp线程组里登录,提取token存到props,然后业务线程组里通过${__P()}引用,JMeter的脚本执行顺序能保证setUp先于普通线程组,但如果你在普通线程组里同时有多个线程,某些线程可能早于另一些线程启动,

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询