接口回归做到几十上百条之后,鉴权往往比断言更折磨人:
- 每条用例 Header 写死 Bearer eyJ…,过期一次改几十处
- 套件第一条「登录 extract token」与平台授权 同名抢变量,表现为「明明登录成功后续仍 401」
- 计划并行跑时,多个协程同时发现 Token 过期,把登录接口打限流
- 平台部署在公网,被测 API 在专网——调试都发不出去
BrickCore 的解法不是「再写一个登录用例模板」,而是:按环境的 Token 授权模块 + 分层变量合并 + Redis 刷新锁 +(可选)经执行机代发。本文按实现讲透,路径对齐 CE 可读源码。
#mermaid-svg-1pH3j1txs9HJMzLO{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1pH3j1txs9HJMzLO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1pH3j1txs9HJMzLO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1pH3j1txs9HJMzLO .error-icon{fill:#552222;}#mermaid-svg-1pH3j1txs9HJMzLO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1pH3j1txs9HJMzLO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1pH3j1txs9HJMzLO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1pH3j1txs9HJMzLO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1pH3j1txs9HJMzLO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1pH3j1txs9HJMzLO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1pH3j1txs9HJMzLO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1pH3j1txs9HJMzLO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1pH3j1txs9HJMzLO .marker.cross{stroke:#333333;}#mermaid-svg-1pH3j1txs9HJMzLO svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1pH3j1txs9HJMzLO p{margin:0;}#mermaid-svg-1pH3j1txs9HJMzLO .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-1pH3j1txs9HJMzLO .cluster-label text{fill:#333;}#mermaid-svg-1pH3j1txs9HJMzLO .cluster-label span{color:#333;}#mermaid-svg-1pH3j1txs9HJMzLO .cluster-label span p{background-color:transparent;}#mermaid-svg-1pH3j1txs9HJMzLO .label text,#mermaid-svg-1pH3j1txs9HJMzLO span{fill:#333;color:#333;}#mermaid-svg-1pH3j1txs9HJMzLO .node rect,#mermaid-svg-1pH3j1txs9HJMzLO .node circle,#mermaid-svg-1pH3j1txs9HJMzLO .node ellipse,#mermaid-svg-1pH3j1txs9HJMzLO .node polygon,#mermaid-svg-1pH3j1txs9HJMzLO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1pH3j1txs9HJMzLO .rough-node .label text,#mermaid-svg-1pH3j1txs9HJMzLO .node .label text,#mermaid-svg-1pH3j1txs9HJMzLO .image-shape .label,#mermaid-svg-1pH3j1txs9HJMzLO .icon-shape .label{text-anchor:middle;}#mermaid-svg-1pH3j1txs9HJMzLO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1pH3j1txs9HJMzLO .rough-node .label,#mermaid-svg-1pH3j1txs9HJMzLO .node .label,#mermaid-svg-1pH3j1txs9HJMzLO .image-shape .label,#mermaid-svg-1pH3j1txs9HJMzLO .icon-shape .label{text-align:center;}#mermaid-svg-1pH3j1txs9HJMzLO .node.clickable{cursor:pointer;}#mermaid-svg-1pH3j1txs9HJMzLO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1pH3j1txs9HJMzLO .arrowheadPath{fill:#333333;}#mermaid-svg-1pH3j1txs9HJMzLO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1pH3j1txs9HJMzLO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1pH3j1txs9HJMzLO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1pH3j1txs9HJMzLO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1pH3j1txs9HJMzLO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1pH3j1txs9HJMzLO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1pH3j1txs9HJMzLO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1pH3j1txs9HJMzLO .cluster text{fill:#333;}#mermaid-svg-1pH3j1txs9HJMzLO .cluster span{color:#333;}#mermaid-svg-1pH3j1txs9HJMzLO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1pH3j1txs9HJMzLO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1pH3j1txs9HJMzLO rect.text{fill:none;stroke-width:0;}#mermaid-svg-1pH3j1txs9HJMzLO .icon-shape,#mermaid-svg-1pH3j1txs9HJMzLO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1pH3j1txs9HJMzLO .icon-shape p,#mermaid-svg-1pH3j1txs9HJMzLO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1pH3j1txs9HJMzLO .icon-shape .label rect,#mermaid-svg-1pH3j1txs9HJMzLO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1pH3j1txs9HJMzLO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1pH3j1txs9HJMzLO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1pH3j1txs9HJMzLO :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
执行出口
平台 Backend
inject token
变量合并项目→环境→df→套件→Token→pre_script
api_auth_service缓存 / 提前刷新
Redisrefresh_lock
平台本机 httpx
压测 Worker 代发经执行机发送
被测 API
演示与源码
| 功能演示 | http://180.76.142.97/showcase/ (文档中心「Token 授权」;平台 admin / BrickCore123456) |
| 开源仓库 | https://gitee.com/BanZhuanKeOrz/BrickCore |
线上路径:接口自动化 → Token 授权。环境变量在 项目配置 → 环境配置;项目级变量在 项目管理 编辑页。接口调试勾选 经执行机发送 在接口管理/调试页。
一、问题本质:鉴权是「横切关注点」
| Token 写在用例 Header | 过期批量改;多环境复制用例 |
| 每个套件第一条都登录 | 并行套件重复登录;与授权缓存打架 |
| 只放环境变量、从不刷新 | 适合长期固定 Token;不适合会过期的 JWT |
| 自定义脚本里直接 requests.post 登录 | 难审计、难复用、难与计划共享 |
平台侧目标:
二、变量从哪来:真实合并顺序
单条用例执行(run_single_case)里,后者覆盖前者:
#mermaid-svg-5dD7I4YXzSO6aGMe{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5dD7I4YXzSO6aGMe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5dD7I4YXzSO6aGMe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5dD7I4YXzSO6aGMe .error-icon{fill:#552222;}#mermaid-svg-5dD7I4YXzSO6aGMe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5dD7I4YXzSO6aGMe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5dD7I4YXzSO6aGMe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5dD7I4YXzSO6aGMe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5dD7I4YXzSO6aGMe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5dD7I4YXzSO6aGMe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5dD7I4YXzSO6aGMe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5dD7I4YXzSO6aGMe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5dD7I4YXzSO6aGMe .marker.cross{stroke:#333333;}#mermaid-svg-5dD7I4YXzSO6aGMe svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5dD7I4YXzSO6aGMe p{margin:0;}#mermaid-svg-5dD7I4YXzSO6aGMe .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-5dD7I4YXzSO6aGMe .cluster-label text{fill:#333;}#mermaid-svg-5dD7I4YXzSO6aGMe .cluster-label span{color:#333;}#mermaid-svg-5dD7I4YXzSO6aGMe .cluster-label span p{background-color:transparent;}#mermaid-svg-5dD7I4YXzSO6aGMe .label text,#mermaid-svg-5dD7I4YXzSO6aGMe span{fill:#333;color:#333;}#mermaid-svg-5dD7I4YXzSO6aGMe .node rect,#mermaid-svg-5dD7I4YXzSO6aGMe .node circle,#mermaid-svg-5dD7I4YXzSO6aGMe .node ellipse,#mermaid-svg-5dD7I4YXzSO6aGMe .node polygon,#mermaid-svg-5dD7I4YXzSO6aGMe .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5dD7I4YXzSO6aGMe .rough-node .label text,#mermaid-svg-5dD7I4YXzSO6aGMe .node .label text,#mermaid-svg-5dD7I4YXzSO6aGMe .image-shape .label,#mermaid-svg-5dD7I4YXzSO6aGMe .icon-shape .label{text-anchor:middle;}#mermaid-svg-5dD7I4YXzSO6aGMe .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5dD7I4YXzSO6aGMe .rough-node .label,#mermaid-svg-5dD7I4YXzSO6aGMe .node .label,#mermaid-svg-5dD7I4YXzSO6aGMe .image-shape .label,#mermaid-svg-5dD7I4YXzSO6aGMe .icon-shape .label{text-align:center;}#mermaid-svg-5dD7I4YXzSO6aGMe .node.clickable{cursor:pointer;}#mermaid-svg-5dD7I4YXzSO6aGMe .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5dD7I4YXzSO6aGMe .arrowheadPath{fill:#333333;}#mermaid-svg-5dD7I4YXzSO6aGMe .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5dD7I4YXzSO6aGMe .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5dD7I4YXzSO6aGMe .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5dD7I4YXzSO6aGMe .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5dD7I4YXzSO6aGMe .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5dD7I4YXzSO6aGMe .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5dD7I4YXzSO6aGMe .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5dD7I4YXzSO6aGMe .cluster text{fill:#333;}#mermaid-svg-5dD7I4YXzSO6aGMe .cluster span{color:#333;}#mermaid-svg-5dD7I4YXzSO6aGMe div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5dD7I4YXzSO6aGMe .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5dD7I4YXzSO6aGMe rect.text{fill:none;stroke-width:0;}#mermaid-svg-5dD7I4YXzSO6aGMe .icon-shape,#mermaid-svg-5dD7I4YXzSO6aGMe .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5dD7I4YXzSO6aGMe .icon-shape p,#mermaid-svg-5dD7I4YXzSO6aGMe .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5dD7I4YXzSO6aGMe .icon-shape .label rect,#mermaid-svg-5dD7I4YXzSO6aGMe .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5dD7I4YXzSO6aGMe .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5dD7I4YXzSO6aGMe .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5dD7I4YXzSO6aGMe :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
① 项目 global_vars
② 环境 global_vars
③ 数据工厂 ${{df:标签}}
④ 套件传入 / 前序 extract
⑤ Token 授权 inject同名盖住套件 extract
⑥ 用例 pre_script优先级最高
最终 all_variables→ URL / Header / Body 替换
文字版(便于对照源码):
项目 global_vars
↓
环境 global_vars
↓
数据工厂 ${{df:标签}} ← merge_execution_variables 内部
↓
套件传入 / 前序 extract ← 作为 extra 覆盖进合并结果
↓
Token 授权 inject ← 同名会盖住套件 extract
↓
用例 pre_script ← 可再改,优先级最高
源码链路(节选示意):
# routers/http/suites.py
all_variables = {**proj_vars, **env_vars, **(variables or {})}
all_variables = await merge_execution_variables(case.project_id, env_id, all_variables)
auth_vars, auth_err = await inject_auth_variables(case.project_id, env_id, all_variables)
if auth_err:
return ApiRunResult(..., status="failed", error=f"授权刷新失败: {auth_err}")
if auth_vars:
all_variables.update(auth_vars)
# pre_script 可继续修改 all_variables
merge_execution_variables(modules/data_tools/tag_service.py)内部约定:
项目全局 < 环境全局 < 数据工厂标签 < extra
因此:套件 extract 会盖住同名的 df 标签;随后 Token 再盖住同名套件变量。
${{dt:…}} 内联工具不在这一步算完,而在后续 VariableResolver 替换 URL/Header/Body 时计算——别和 df: 混为一谈。
2.1 实操放哪里
| 环境 Host | https://api.test.example.com | 不要写进几十条 path |
| 环境 global_vars | username / password / tenant_id | 登录接口 Body 引用 |
| 项目 global_vars | client_id | 全项目共用 |
| ${{df:标签}} | 造数、查库结果 | 合并阶段注入 |
| Token 授权 | token / admin_token | 动态、会过期 |
| 套件 extractors | order_id | 勿与授权抢同名 token |
| pre_script | 临时改写 | 最高,慎用 |
三、何时不必建 Token 授权
官方文档也强调:若 Token 长期固定 且已写在环境 global_vars,用例直接:
Authorization: Bearer ${{access_token}}
即可,不必再建授权。
授权适合:
- JWT / Session 会过期,需要提前刷新
- 全项目统一登录身份,套件不想每人写登录步
- 多环境各一套账号,切环境换缓存
四、Token 授权模块:api_login vs custom_code
路径:接口自动化 → Token 授权。每条配置绑定 一个环境(创建后一般不改环境)。
同一环境可有多条配置(不同身份),但 只有一条 is_enabled=True 会生效——get_enabled_auth_config 按 update_time 降序取第一条。
| api_login(默认) | 调已有登录 ApiDefinition + extractors | 登录接口须先能单独调试通 |
| custom_code | 从环境变量拼装 / 写固定值 | RestrictedPython,不能发 HTTP |
4.1 api_login 配置要点
| 登录接口 | login_api_id → 接口管理中的定义 |
| extractors | JSON / Header / 正则;如 $.data.access_token |
| ttl_minutes | 缓存时长(模型默认常 1440 分钟,可按 JWT 缩短) |
| refresh_before_minutes | 到期前提前刷新(默认常 5) |
| refresh_mode | 默认 on_execute(执行前检查) |
用例引用:
[{"key": "Authorization", "value": "Bearer ${{token}}"}]
注意 Bearer 与 ${{token}} 之间有空格。
4.2 登录实际怎么发请求
_execute_login_api(modules/http/api_auth_service.py)与单用例调试同源思路:
也就是说:授权不是「另写一套登录协议」,而是 复用接口资产 + 把提取结果放进缓存。
4.3 custom_code:能做什么、不能做什么
def auth(context):
variables = context.get("variables") or {}
return {
"token": variables.get("digi_token") or "",
"tenant_id": variables.get("tenant_id") or "14",
}
context 字段:environment_id / project_id / host / variables。
不要写 context.get("fixed_token") 指望顶层有该 key——预置值应放进环境变量再从 variables 取。
沙箱用 RestrictedPython:httpx / requests / 任意外发都会失败。需要调登录接口 → 改回 api_login。
五、缓存、提前刷新与 Redis 锁(核心)
实现文件:backend/app/modules/http/api_auth_service.py。
5.1 何时认为「需要刷新」
# 示意:_cache_needs_refresh
# 无 cache_data / 无 expires → 要刷新
# now >= expires_at – refresh_before_minutes → 要刷新
因此「提前 10 分钟刷新」不是等到 401 才动,而是 执行前主动换票,减少跑到一半集体翻车。
5.2 并发安全:锁 + 双重检查
常量(源码):
- 锁 TTL:REFRESH_LOCK_TTL = 60 秒
- 等待超时:REFRESH_WAIT_TIMEOUT = 30 秒
- 轮询间隔:0.3 秒
- Redis key:api_auth_refresh_lock:{config_id}
流程:
#mermaid-svg-eDU9mvWLkWBUohFS{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eDU9mvWLkWBUohFS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eDU9mvWLkWBUohFS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eDU9mvWLkWBUohFS .error-icon{fill:#552222;}#mermaid-svg-eDU9mvWLkWBUohFS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eDU9mvWLkWBUohFS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eDU9mvWLkWBUohFS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eDU9mvWLkWBUohFS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eDU9mvWLkWBUohFS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eDU9mvWLkWBUohFS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eDU9mvWLkWBUohFS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eDU9mvWLkWBUohFS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eDU9mvWLkWBUohFS .marker.cross{stroke:#333333;}#mermaid-svg-eDU9mvWLkWBUohFS svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eDU9mvWLkWBUohFS p{margin:0;}#mermaid-svg-eDU9mvWLkWBUohFS .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-eDU9mvWLkWBUohFS .cluster-label text{fill:#333;}#mermaid-svg-eDU9mvWLkWBUohFS .cluster-label span{color:#333;}#mermaid-svg-eDU9mvWLkWBUohFS .cluster-label span p{background-color:transparent;}#mermaid-svg-eDU9mvWLkWBUohFS .label text,#mermaid-svg-eDU9mvWLkWBUohFS span{fill:#333;color:#333;}#mermaid-svg-eDU9mvWLkWBUohFS .node rect,#mermaid-svg-eDU9mvWLkWBUohFS .node circle,#mermaid-svg-eDU9mvWLkWBUohFS .node ellipse,#mermaid-svg-eDU9mvWLkWBUohFS .node polygon,#mermaid-svg-eDU9mvWLkWBUohFS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eDU9mvWLkWBUohFS .rough-node .label text,#mermaid-svg-eDU9mvWLkWBUohFS .node .label text,#mermaid-svg-eDU9mvWLkWBUohFS .image-shape .label,#mermaid-svg-eDU9mvWLkWBUohFS .icon-shape .label{text-anchor:middle;}#mermaid-svg-eDU9mvWLkWBUohFS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eDU9mvWLkWBUohFS .rough-node .label,#mermaid-svg-eDU9mvWLkWBUohFS .node .label,#mermaid-svg-eDU9mvWLkWBUohFS .image-shape .label,#mermaid-svg-eDU9mvWLkWBUohFS .icon-shape .label{text-align:center;}#mermaid-svg-eDU9mvWLkWBUohFS .node.clickable{cursor:pointer;}#mermaid-svg-eDU9mvWLkWBUohFS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eDU9mvWLkWBUohFS .arrowheadPath{fill:#333333;}#mermaid-svg-eDU9mvWLkWBUohFS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eDU9mvWLkWBUohFS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eDU9mvWLkWBUohFS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eDU9mvWLkWBUohFS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eDU9mvWLkWBUohFS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eDU9mvWLkWBUohFS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eDU9mvWLkWBUohFS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eDU9mvWLkWBUohFS .cluster text{fill:#333;}#mermaid-svg-eDU9mvWLkWBUohFS .cluster span{color:#333;}#mermaid-svg-eDU9mvWLkWBUohFS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eDU9mvWLkWBUohFS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eDU9mvWLkWBUohFS rect.text{fill:none;stroke-width:0;}#mermaid-svg-eDU9mvWLkWBUohFS .icon-shape,#mermaid-svg-eDU9mvWLkWBUohFS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eDU9mvWLkWBUohFS .icon-shape p,#mermaid-svg-eDU9mvWLkWBUohFS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eDU9mvWLkWBUohFS .icon-shape .label rect,#mermaid-svg-eDU9mvWLkWBUohFS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eDU9mvWLkWBUohFS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eDU9mvWLkWBUohFS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eDU9mvWLkWBUohFS :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
否
是
抢锁成功
已有效
仍需刷新
抢锁失败
是
否
ensure_auth_cache
缓存需刷新?
直接返回 cache_data
SET NXapi_auth_refresh_lock:{id}
二次读库是否已有效?
释放锁 → 返回
refresh_auth_configapi_login / custom_code
写缓存 → 释放锁
轮询等锁消失最长 30s
再读库
缓存有效?
返回 cache_data
RuntimeError等待超时 / 仍无效
文字版:
ensure_auth_cache
├─ 未到期 → 直接返回 cache_data
├─ SET NX 抢锁成功
│ ├─ 再读库二次检查(其它人可能刚刷完)
│ ├─ refresh_auth_config(登录 / custom_code)
│ └─ 释放锁
└─ 抢锁失败 → 轮询等锁消失 → 再读库
└─ 仍无效 → RuntimeError(等待超时 / 刷新后仍无效)
inject_auth_variables 捕获异常后写入 last_refresh_error,用例侧返回 「授权刷新失败: …」,而不是静默带着空 token 去打业务接口。
5.3 调试 vs 正式刷新
| 调试授权 preview_auth_config | 否 | 核对 extractors / 账号 |
| 列表 刷新 / 执行触发 refresh_auth_config | 是 | 写入 cache_data + cache_expires_at |
建议流程:调试通过 → 保存启用 → 点一次刷新看缓存 → 再跑套件。
5.4 与计划 / 定时 / 压测的关系
- 缓存按 项目 + 环境(启用的那条配置)共享
- 同一环境下的套件、计划、定时任务 共用 这份缓存
- 压测场景构建请求时也会走变量合并;鉴权策略仍建议环境级授权,避免每个虚拟用户各自登录打爆认证服务(除非你有意压登录接口)
六、选型矩阵:授权 vs 套件登录链 vs 环境变量
| Token 会过期,全项目统一账号 | 只用 Token 授权;套件不要再登录抽同名 token |
| 套件内要走「用户 A 登录 → 用户 B 操作」 | 关闭授权,用多步 extract + 不同变量名 |
| Token 长期固定 | 环境变量即可,关闭授权 |
| 多角色同时要用 | 提取为 admin_token / user_token;注意 同时只能启用一条授权配置——多角色更稳的是套件链或分环境 |
同名冲突铁律:授权已启用且变量叫 token 时,套件前序 extract 的 token 传不下去(会被覆盖)。这是源码行为,不是文档笔误。
七、多环境与「经执行机调试」
| 测试 | https://api.test.example.com | 测开账号 api_login |
| 预发 | https://api.staging.example.com | 另建配置并启用 |
用例只写 ${{token}},执行换 env_id。
7.1 平台够不着被测 Host 时
接口调试可勾选 经执行机发送(空闲压测 Worker 代发):
#mermaid-svg-sPfh7ziibIpVwfct{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sPfh7ziibIpVwfct .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sPfh7ziibIpVwfct .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sPfh7ziibIpVwfct .error-icon{fill:#552222;}#mermaid-svg-sPfh7ziibIpVwfct .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sPfh7ziibIpVwfct .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sPfh7ziibIpVwfct .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sPfh7ziibIpVwfct .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sPfh7ziibIpVwfct .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sPfh7ziibIpVwfct .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sPfh7ziibIpVwfct .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sPfh7ziibIpVwfct .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sPfh7ziibIpVwfct .marker.cross{stroke:#333333;}#mermaid-svg-sPfh7ziibIpVwfct svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sPfh7ziibIpVwfct p{margin:0;}#mermaid-svg-sPfh7ziibIpVwfct .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-sPfh7ziibIpVwfct .cluster-label text{fill:#333;}#mermaid-svg-sPfh7ziibIpVwfct .cluster-label span{color:#333;}#mermaid-svg-sPfh7ziibIpVwfct .cluster-label span p{background-color:transparent;}#mermaid-svg-sPfh7ziibIpVwfct .label text,#mermaid-svg-sPfh7ziibIpVwfct span{fill:#333;color:#333;}#mermaid-svg-sPfh7ziibIpVwfct .node rect,#mermaid-svg-sPfh7ziibIpVwfct .node circle,#mermaid-svg-sPfh7ziibIpVwfct .node ellipse,#mermaid-svg-sPfh7ziibIpVwfct .node polygon,#mermaid-svg-sPfh7ziibIpVwfct .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sPfh7ziibIpVwfct .rough-node .label text,#mermaid-svg-sPfh7ziibIpVwfct .node .label text,#mermaid-svg-sPfh7ziibIpVwfct .image-shape .label,#mermaid-svg-sPfh7ziibIpVwfct .icon-shape .label{text-anchor:middle;}#mermaid-svg-sPfh7ziibIpVwfct .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sPfh7ziibIpVwfct .rough-node .label,#mermaid-svg-sPfh7ziibIpVwfct .node .label,#mermaid-svg-sPfh7ziibIpVwfct .image-shape .label,#mermaid-svg-sPfh7ziibIpVwfct .icon-shape .label{text-align:center;}#mermaid-svg-sPfh7ziibIpVwfct .node.clickable{cursor:pointer;}#mermaid-svg-sPfh7ziibIpVwfct .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sPfh7ziibIpVwfct .arrowheadPath{fill:#333333;}#mermaid-svg-sPfh7ziibIpVwfct .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sPfh7ziibIpVwfct .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sPfh7ziibIpVwfct .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sPfh7ziibIpVwfct .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sPfh7ziibIpVwfct .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sPfh7ziibIpVwfct .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sPfh7ziibIpVwfct .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sPfh7ziibIpVwfct .cluster text{fill:#333;}#mermaid-svg-sPfh7ziibIpVwfct .cluster span{color:#333;}#mermaid-svg-sPfh7ziibIpVwfct div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sPfh7ziibIpVwfct .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sPfh7ziibIpVwfct rect.text{fill:none;stroke-width:0;}#mermaid-svg-sPfh7ziibIpVwfct .icon-shape,#mermaid-svg-sPfh7ziibIpVwfct .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sPfh7ziibIpVwfct .icon-shape p,#mermaid-svg-sPfh7ziibIpVwfct .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sPfh7ziibIpVwfct .icon-shape .label rect,#mermaid-svg-sPfh7ziibIpVwfct .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sPfh7ziibIpVwfct .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sPfh7ziibIpVwfct .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sPfh7ziibIpVwfct :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
直连可达
勾选经执行机
调试页
Backend合并变量 + inject Token
被测 API
Perf Worker
专网 / 办公网 SUT
- Token / 变量仍在 平台侧 合并与注入
- HTTP 从 Worker 所在网络出去
- 需 Worker 在线、引擎版本满足门禁
- 带文件的 form-data 代发暂不支持(会明确报错,不静默回退本机)
适合:平台在云上、SUT 在办公网/专网。
八、排错清单(按现象)
| 授权刷新失败 | 调试授权;登录接口单独发送;JSONPath;账号是否在环境变量 |
| ${{token}} 原样发出 | 授权未启用;变量名不一致;看请求详情 |
| 部分用例 401 | Header 是否写死旧值;环境是否选错 |
| 套件登录成功后续仍无 token | 是否与授权 同名覆盖 |
| 并发偶发 401 / 登录 429 | Redis 是否通;锁等待是否超时;ttl/提前刷新是否过短 |
| custom_code 失败 | 是否试图发 HTTP;auth 是否返回非空 dict |
| Worker 代发超时 | 执行机离线;项目不一致;版本过旧 |
| ${{df:x}} 空 | 数据工厂标签;是否被同名套件/授权覆盖 |
九、小结
附录 A:源码文件索引
| 1 | modules/http/api_auth_service.py | 缓存、锁、login、custom_code、inject |
| 2 | routers/http/auth_config.py | CRUD、api_login | custom_code |
| 3 | routers/http/suites.py | run_single_case 合并与失败短路 |
| 4 | modules/data_tools/tag_service.py | merge_execution_variables |
| 5 | core/case/variable_resolver.py | ${{var}} / dt |
| 6 | routers/http/apis.py | 经 Worker 代发调试 |
| 7 | models/http.py | ApiAuthConfig 字段 |
用户文档:showcase/docs/content/api-auth.md。
支持与交流
- 演示:http://180.76.142.97/showcase/ · 源码:https://gitee.com/BanZhuanKeOrz/BrickCore
- 觉得有用欢迎 Star ⭐,问题评论区留言或 Gitee Issues
- 交流群:文末上传微信群二维码,或 CSDN 私信联系



