欢迎光临
我们一直在努力

系列13-接口自动化 Token 自动刷新:变量合并优先级、Redis 刷新锁与经执行机调试

接口回归做到几十上百条之后,鉴权往往比断言更折磨人:

  • 每条用例 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 登录 难审计、难复用、难与计划共享

平台侧目标:

  • 登录资产化:登录接口仍是 ApiDefinition,授权配置引用它
  • 缓存环境化:测试/预发各一份缓存,切 env_id 即可
  • 并发安全:多用例同时触发刷新时只打一次登录
  • 失败可诊断:last_refresh_error、调试授权不写缓存

  • 二、变量从哪来:真实合并顺序

    单条用例执行(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 降序取第一条。

    auth_type适用限制
    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)与单用例调试同源思路:

  • 用环境 Host + 接口 path 拼 URL
  • VariableResolver 替换 headers / params / body(可用环境里的账号密码)
  • httpx 发请求;status >= 400 直接失败
  • extract_variables;一个变量都抽不到则失败
  • 也就是说:授权不是「另写一套登录协议」,而是 复用接口资产 + 把提取结果放进缓存。

    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 传不下去(会被覆盖)。这是源码行为,不是文档笔误。


    七、多环境与「经执行机调试」

    环境Host授权
    测试 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}} 空 数据工厂标签;是否被同名套件/授权覆盖

    九、小结

  • 鉴权是横切能力:用 环境级授权缓存,而不是复制登录步骤。
  • 合并顺序要记牢:df < 套件 extract < Token < pre_script(同名后者赢)。
  • api_login 复用接口资产;custom_code 只做本地拼装。
  • Redis 锁保证并行计划 只刷新一次;失败写 last_refresh_error。
  • 专网调试用 经执行机发送,鉴权逻辑不变。

  • 附录 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 私信联系
    赞(0)
    未经允许不得转载:171主机测评 » 系列13-接口自动化 Token 自动刷新:变量合并优先级、Redis 刷新锁与经执行机调试
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址