LoadRunner 关联技巧与手动关联¶
关联(Correlation)本质只有一句话:把服务端这次生成的动态值取出来,替换脚本里录制时留下的上次的旧值。听着简单,实际脚本里 80% 的 401/500 都能追到关联上。环境为 LoadRunner 2023(24.1),协议 Web - HTTP/HTML。
一、三个真实报错,先把症状对上号¶
1.1 回放直接报参数未匹配¶
Action.c(38): Error -26377: No match found for the requested parameter "csrf_token".
Check whether the requested boundaries exist in the response data.
Also, if the data you want to save exceeds 256 Per-Thread-Extra-Length,
use "web_set_max_html_param_len" to increase the parameter size
这条错误只说明一件事:在被搜索的那次响应里,LB 和 RB 之间没找到内容。常见原因依次是:边界写错、搜索范围不对、响应被 gzip 压缩、注册函数放错了请求、值超过 256 字节被截断。
1.2 关联“成功”了,但订单接口 401¶
Action.c(52): Error -26612: HTTP Status-Code=401 (Unauthorized) for "http://shop.example.com/api/order"
注意这里没有 -26377。说明参数匹配到了,但值是错的——可能是取到了页面里另一个同名 token、取到了上一次会话的缓存值、或者取到的是空串。
1.3 迭代结束时提示挂起的注册¶
Action.c(41): Error -27257: Pending Web_reg_save_param/reg_find/create_html_param[_ex] request(s)
detected and reset at the end of iteration
web_reg_save_param_ex 注册的是“下一个响应的提取规则”。注册完却没有发起任何请求,规则就悬空了。典型场景:把注册函数写在 if 分支里,分支没走到;或者注册后紧跟的是 lr_think_time,迭代就结束了。
二、关联原理:什么样的值必须关联¶
判定标准很硬:同一个请求录制两次,响应里该值是否变化。变了就必须关联,没变就不要关联(过度关联会让脚本对细微改版极其脆弱)。
| 值类型 | 示例 | 处理方式 |
|---|---|---|
| 会话标识 | jsessionid、PHPSESSID |
Cookie 由 LR 自动管理;出现在 URL 里的必须手动关联 |
| CSRF / XSRF | hidden input、XSRF-TOKEN 响应头 |
手动关联,登录和每个写操作前都要刷新 |
| 一次性 token | access_token、refresh_token |
登录响应里提取 |
| 页面状态 | __VIEWSTATE、__EVENTVALIDATION |
手动关联,值通常很长,注意 256 限制 |
| 业务单号 | orderNo、payId |
后续查询/支付要用,必须关联 |
| 签名/nonce | sign、nonce、timestamp |
多数由前端算,脚本里要按同样算法重算 |
| 静态配置 | appVersion、固定商户号 |
不要关联 |
录制时这些值都被写死成常量,例如:
服务端第二次会话生成的 token 完全不同,回放自然 401。所以关联不是“让脚本跑通”的补丁,而是把脚本从“一次录像”变成“可重复执行的客户端”。
三、Correlation Studio 自动关联¶
入口:VuGen 打开脚本 → Design 选项卡 → Correlation Studio(LR 2020 之前叫 “Design Studio”)。也可以在录制选项中开启边录边关联:
操作顺序:
- 点
Scan扫描当前脚本的所有响应。 - 候选列表里按
Rule分组,常见规则包括AST Transactions、JSESSIONID、ViewState。 - 勾选候选 →
Correlate,VuGen 会自动插入web_reg_save_param_ex并替换使用点。
它的边界:
- 只认规则库里的模式。自定义
X-Sign、业务自研 token 扫不出来,必须手动。 - 会把“看起来像动态值”的静态字段也扫进来,产生大量无效关联。
- 生成的
LB/RB可能不唯一,Ordinal也不一定对,还不会帮你处理 URL 编码。
判定标准:自动关联后必须至少回放两次,两次都通过且关键参数值不同,才算真的关联成功。只跑一次通过,可能只是命中了缓存。
四、手动关联的完整步骤¶
4.1 定位动态值¶
三个入口,按可靠性排序:
Generation Log:录制时的原始请求/响应,能直接搜关键字。- Snapshot(
Snapshot=t1.inf):双击请求看 raw 响应。 - 浏览器 DevTools 的 Network → Response,确认值在 HTML、JSON 还是 Header 里。
4.2 确认“产生请求”和“消费请求”¶
一个值只能从产生它的那个响应里提取。比如 CSRF token 在 GET /login 的 HTML 里,就不能从 POST /login 的响应里找。消费点可能在 Header、Body、URL 参数三处,都要替换。
4.3 选左右边界¶
| 原则 | 好的边界 | 坏的边界 |
|---|---|---|
| 唯一 | name="csrf_token" value=" |
value=" |
| 短 | 不超过 40 字符 | 把整段 HTML 当前缀 |
| 不含动态值 | "orderNo":" |
"orderNo":"20260908001 |
| 不含换行 | 同一行内闭合 | 跨行的标签 |
| 无歧义 | AccessToken= |
Token= |
4.4 写函数并放到正确位置¶
/* 注册必须放在“产生该值的请求”之前 */
web_reg_save_param_ex(
"ParamName=csrf_token",
"LB=name=\"csrf_token\" value=\"",
"RB=\"",
"Ordinal=1",
SEARCH_FILTERS,
"Search=Body",
"NotFound=ERROR",
LAST);
web_url("login_page",
"URL=http://shop.example.com/login",
"Resource=0",
"RecContentType=text/html",
"Mode=HTML",
LAST);
4.5 参数语义表¶
| 参数 | 真实语义 | 常见错误 |
|---|---|---|
| ParamName | 变量名,引用时写 {name},不带 $ |
写成 ${name} |
| LB / RB | 左/右边界,大小写敏感 | 边界里带上了动态值 |
| LB/IC、RB/IC | 忽略大小写的边界 | 该用时没用,HTML 大小写变化就失配 |
| Ordinal | 取第几个匹配;All 取全部 |
页面有隐藏域和可见域两个同名 token |
| Search | Body、Headers、NoResource(仅 HTML 正文,不含 Header 与资源)、All |
取 Header 里的 token 却用了 Body;把 NoResource 误写成不存在的 NoBody |
| NotFound | ERROR(默认)或 WARNING |
改成 WARNING 后错误被吞,问题延后爆发 |
| SaveOffset / SaveLen | 从匹配结果中偏移截取 | 用来去掉多余的前后缀,容易截错 |
| IgnoreRedirections | 是否忽略重定向响应 | 302 响应里的 token 取不到 |
| RequestUrl | 限定只搜某个 URL 的响应 | 一个注册匹配到了多个请求 |
Correlation Studio 界面上的 Scope(Body / Headers / All)生成到脚本里就是 Search=,两者是同一个东西,手写脚本时以 Search= 为准。
4.6 引用与编码¶
web_add_header("Authorization", "Bearer {access_token}");
web_custom_request("create_order",
"URL=http://shop.example.com/api/order",
"Method=POST",
"EncType=application/json",
"Body={\"skuId\":\"{sku}\",\"token\":\"{access_token}\"}",
LAST);
URL 参数里的值如果含 +、/、=(Base64 token 常见),必须编码,否则服务端解出来是错的:
lr_save_string(lr_eval_string("{access_token}"), "token_raw");
web_convert_param("token_raw", "SourceEncoding=PLAIN", "TargetEncoding=URL", LAST);
五、JSON 与 Header 场景的两种写法¶
5.1 正则关联(通用,版本要求最低)¶
web_reg_save_param_regexp(
"ParamName=orderNo",
"RegExp=\"orderNo\"\\s*:\\s*\"(\\w+)\"",
"Group=1",
SEARCH_FILTERS,
"Search=Body",
LAST);
注意 C 字符串里反斜杠要双写,\\s 才是正则的 \s。捕获组只留值本身,不要把引号包进去;贪婪的 (.*) 在跨行响应里会一口吞到下一个字段,用 ([^"]*) 更稳。
5.2 JSON 关联(LR 12.50 及以上)¶
web_reg_save_param_json(
"ParamName=access_token",
"QueryString=$.data.token",
"SelectAll=No",
SEARCH_FILTERS,
"Search=Body",
LAST);
如果工程里提示函数未定义,说明 VuGen 版本低于 12.50(web_reg_save_param_json 自 LR 12.50 起提供),退回正则写法,不要硬编。
5.3 响应头里的 token¶
web_reg_save_param_ex(
"ParamName=xsrf_token",
"LB=XSRF-TOKEN: ",
"RB=\r\n",
SEARCH_FILTERS,
"Search=Headers",
LAST);
Header 的右边界是 CRLF,写 \r\n 而不是直接换行。
六、动态 token / CSRF 的实战组合¶
一个典型的登录 → 下单链路,需要三次关联:
| 步骤 | 请求 | 关联值 | 来源 |
|---|---|---|---|
| 1 | GET /login | csrf_token |
HTML hidden input |
| 2 | POST /login | access_token、refresh_token |
JSON body |
| 3 | GET /order/confirm | confirm_token |
JSON body |
| 4 | POST /order | — | 使用前两步的值 |
脚本骨架:
Action()
{
web_set_max_html_param_len("8192");
web_add_auto_header("Accept-Encoding", "identity");
/* 1. 取登录页 CSRF */
web_reg_save_param_ex(
"ParamName=csrf_token",
"LB=name=\"csrf_token\" value=\"",
"RB=\"",
SEARCH_FILTERS,
"Search=Body",
LAST);
web_url("login_page", "URL=http://shop.example.com/login", LAST);
/* 2. 登录并取 access_token */
web_reg_save_param_json(
"ParamName=access_token",
"QueryString=$.data.token",
SEARCH_FILTERS,
"Search=Body",
LAST);
web_submit_data("login",
"Action=http://shop.example.com/api/login",
"Method=POST",
"EncType=application/json",
ITEMDATA,
"Name=user", "Value={p_user}", ENDITEM,
"Name=pwd", "Value={p_pwd}", ENDITEM,
"Name=csrf", "Value={csrf_token}", ENDITEM,
LAST);
lr_output_message("token=[%s] len=%d",
lr_eval_string("{access_token}"),
(int)strlen(lr_eval_string("{access_token}")));
/* 3. 带 token 下单 */
web_add_header("Authorization", "Bearer {access_token}");
web_custom_request("create_order",
"URL=http://shop.example.com/api/order",
"Method=POST",
"EncType=application/json",
"Body={\"skuId\":\"{sku}\",\"qty\":1}",
LAST);
return 0;
}
web_set_max_html_param_len("8192") 必须放在最前面,它改的是每个线程的参数缓冲上限,默认 256 字节,__VIEWSTATE 这种动辄上千字符的值不放开就一定截断。
Accept-Encoding: identity 是为了让服务端返回明文。LR 虽然能解 gzip,但启用压缩后 Generation Log 里看到的是二进制,手动找边界会非常痛苦。
七、回放日志与对比调试¶
打开参数替换日志:Runtime Settings → Log → Extended log,勾选 Parameter substitution,回放时能看到每一处 {param} 被替换成了什么。
Action.c(38): Notify: Saving Parameter "csrf_token = 8f3a1c9e2b7d4a6f"
Action.c(52): Notify: Parameter Substitution: parameter "access_token" = "eyJhbGciOiJIUzI1NiJ9..."
调试三件套:
lr_output_message("token=[%s]", lr_eval_string("{token}"));
lr_log_message("token length=%d", (int)strlen(lr_eval_string("{token}")));
lr_debug_message(LR_MSG_CLASS_PARAMETERS, "token now=[%s]", lr_eval_string("{token}"));
不要写 lr_output_message(lr_eval_string("{token}"));。lr_output_message 的第一个参数是格式串,token 里一旦出现 %(Base64 不会有,但签名串可能有),轻则输出错乱,重则 VuGen 崩溃。这条在旧脚本里非常常见,审查脚本时优先改掉。
对比调试:Tools → Compare with Recording,把当前脚本的请求和录制基线逐条对比,能快速看出哪个请求的 Body 少了 token、哪个 Header 没替换。
八、常见错误对照表¶
| 现象 | 根因 | 处理 |
|---|---|---|
| -26377 | 边界不匹配、搜索范围错、响应压缩、注册位置错 | 用 Snapshot 原文核对边界,改 Search=,加 identity 头 |
| -26612 401 | 关联值为空或取到旧值 | 打开参数替换日志,打印长度 |
| -26612 500 | 关联到错误结构(如把 JSON 片段塞进单值字段) | 收紧 LB/RB,确认取的是标量 |
| -27257 | 注册后没有请求 | 把注册紧贴目标请求,或删掉多余注册 |
| 取到两个值中的第一个 | Ordinal 默认 1,页面有隐藏域 |
Ordinal=2 或改用更唯一的边界 |
| 值被截断 | 超过 256 字节 | web_set_max_html_param_len("8192") |
| 中文乱码 | 响应是 UTF-8,脚本按 GBK 解释 | 录制时设 UTF-8,或用 lr_convert_string_encoding |
| 只在第二次迭代失败 | 把注册写进了 Once 逻辑 |
注册放到每次迭代都会执行的请求前 |
| 重定向后取不到 | 302 响应被自动跟随 | IgnoreRedirections=Yes 或对 Location 单独关联 |
九、最佳实践清单¶
- 关联只做“两次录制值会变”的字段,不滥用
-
LB/RB里绝不出现动态值本身 - 每个注册都紧贴产生该值的请求,中间不插无关请求
- 关键参数都打印长度,长度异常立刻暴露
-
web_set_max_html_param_len放在 Action 开头 - Header 关联用
Search=Headers,JSON 关联优先web_reg_save_param_json - URL 中的 token 经过
web_convert_param编码 - 每个关联点至少回放两次,且参数值不同
- 脚本里没有
lr_output_message(lr_eval_string(...))这种写法 - 自动关联的结果逐条人工复核,不直接交付