跳转至

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、固定商户号 不要关联

录制时这些值都被写死成常量,例如:

"Name=csrf_token", "Value=8f3a1c9e2b7d4a6f", ENDITEM,

服务端第二次会话生成的 token 完全不同,回放自然 401。所以关联不是“让脚本跑通”的补丁,而是把脚本从“一次录像”变成“可重复执行的客户端”。

三、Correlation Studio 自动关联

入口:VuGen 打开脚本 → Design 选项卡 → Correlation Studio(LR 2020 之前叫 “Design Studio”)。也可以在录制选项中开启边录边关联:

Recording Options → Correlation → Enable correlation during recording

操作顺序:

  1. 点 Scan 扫描当前脚本的所有响应。
  2. 候选列表里按 Rule 分组,常见规则包括 AST Transactions、JSESSIONID、ViewState。
  3. 勾选候选 → Correlate,VuGen 会自动插入 web_reg_save_param_ex 并替换使用点。

它的边界:

  • 只认规则库里的模式。自定义 X-Sign、业务自研 token 扫不出来,必须手动。
  • 会把“看起来像动态值”的静态字段也扫进来,产生大量无效关联。
  • 生成的 LB/RB 可能不唯一,Ordinal 也不一定对,还不会帮你处理 URL 编码。

判定标准:自动关联后必须至少回放两次,两次都通过且关键参数值不同,才算真的关联成功。只跑一次通过,可能只是命中了缓存。

四、手动关联的完整步骤

4.1 定位动态值

三个入口,按可靠性排序:

  1. Generation Log:录制时的原始请求/响应,能直接搜关键字。
  2. Snapshot(Snapshot=t1.inf):双击请求看 raw 响应。
  3. 浏览器 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(...)) 这种写法
  • 自动关联的结果逐条人工复核,不直接交付

相关阅读