ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Apifox接口自动化:RSA加密登录与Token鉴权全流程实战

Apifox接口自动化:RSA加密登录与Token鉴权全流程实战 做接口自动化测试这么多年我一直觉得登录态处理是整套用例里最绕不开又最容易被忽略的一环。尤其后端把登录密码做了 RSA 加密、登录成功返回 token、后续每个接口都要在 header 里带 token 这种链路听起来不复杂但真正在 Apifox 里落地时很多人会卡在前置脚本怎么写、加密库从哪来、token 怎么共享这几个点上。这篇文章就把我实际跑通的这套方案完整拆开讲清楚从 RSA 加密密码的前置脚本到发送登录接口拿 token再到把 token 塞进后续请求 header全程基于 Apifox 最新版的脚本能力适合正在做接口测试、需要处理登录鉴权链路的同学直接参考复现。1. 场景与需求拆解为什么绕不开 RSA 加密和 token1.1 登录接口为什么要做 RSA 加密以前做接口测试登录接口大多是明文密码直接提交或者做个简单的 Base64 就算完事。现在稍微正规一点的后端密码传输基本都是 RSA 加密。RSA 是非对称加密服务端持有私钥前端或者客户端持有公钥登录时用公钥把密码加密后传给后端后端用私钥解密验证。这么做的好处是即使请求被抓包中间人拿到的也只是密文没有私钥根本还原不出原始密码。你可能会问HTTPS 本身就加密了为什么还要在业务层再做一次 RSA我见过不少项目这么干核心原因有两个一是部分老系统的 HTTPS 配置不严格存在降级风险业务层加密能多一道防线二是网关层或安全审计需要看到请求内容时密码不能被明文暴露。所以在 Apifox 里做登录接口测试第一步就是模拟前端的 RSA 加密逻辑。但这里有一个很容易踩的坑RSA 加密每次生成的密文都不一样。因为标准 RSA 加密算法里带了随机填充比如 PKCS#1 v1.5 或 OAEP同样的明文两次加密结果不同这是正常现象后端每次都能正确解密。很多人在 Apifox 里发现两次加密结果不一样以为脚本写错了其实不是。1.2 token 在接口测试里的完整链路登录成功之后后端一般会返回一个 token这个 token 可能放在响应体的 data 字段里也可能放在 header 的 Authorization 里还有可能是单独的 Set-Cookie。拿到 token 之后后续所有需要鉴权的接口都要在请求头里带上它常见格式是Authorization: Bearer token也有项目习惯用自定义 header 比如X-Token或者token字段。所以完整链路是三步第一步执行前置脚本用 RSA 公钥加密登录密码替换请求 body 里的密码字段。第二步发送登录接口请求从响应里解析出 token。第三步把 token 存入环境变量或全局变量后续接口在前置脚本或 header 模板里引用它。在 Apifox 里这三步分别对应前置脚本、断言/脚本、环境变量管理。重点是怎么让这三个环节自动串联而不是每次手动把 token 复制粘贴到下一个接口。下面我按实际操作顺序把每一步的关键代码和细节讲清楚。2. RSA 加密前置脚本从原理到 Apifox 落地2.1 Apifox 脚本环境里用什么做 RSA 加密Apifox 的脚本运行在沙箱环境里内置了一些常用的 JavaScript 库其中就包括JSEncrypt和crypto-js。JSEncrypt 是前端 RSA 加密最常用的库API 简单支持 PKCS#1 v1.5 填充适合对接大多数 Java、Go、Node.js 后端的 RSA 解密逻辑。用的时候直接在前置脚本里require(jsencrypt)引入即可不需要额外安装依赖。如果你们的后端用的是 OAEP 填充或者需要分段加密比如超长密码或敏感信息JSEncrypt 可能不够用这时候可以用 Node.js 内置的crypto模块Apifox 脚本环境也支持。我建议优先用 JSEncrypt因为大多数场景够用代码写起来最直观。这里先看一个最基础的 JSEncrypt 加密代码// 前置脚本使用 JSEncrypt 加密密码 // 获取环境变量里的公钥 const publicKey pm.environment.get(rsa_public_key); // 从请求 body 中读取原始密码 const password pm.request.body.get(password); // 创建 JSEncrypt 实例 const encryptor new JSEncrypt(); encryptor.setPublicKey(publicKey); // 加密密码 const encryptedPassword encryptor.encrypt(password); // 把加密后的密码写回请求 body pm.request.body.update({ password: encryptedPassword });这段代码看着简单但有几个细节必须注意。第一pm.request.body.get(password)只能用在 body 是application/x-www-form-urlencoded或直接能解析成对象的情况下如果你的请求体是 JSON 字符串需要先用JSON.parse解析。第二pm.environment.get(rsa_public_key)里的公钥必须是完整的 PEM 格式字符串包括-----BEGIN PUBLIC KEY-----和-----END PUBLIC KEY-----这两行少了任何一行JSEncrypt 都会报错。2.2 公钥从哪来、格式不对怎么办公钥一般由后端同学提供通常是放在一个接口里返回或者写在项目文档里。但很多同学拿到的公钥格式五花八门常见的有这两种PKCS#1 格式-----BEGIN RSA PUBLIC KEY-----PKCS#8 格式-----BEGIN PUBLIC KEY-----JSEncrypt 对 PKCS#8 格式支持比较好大多数情况下你直接把后端给的公钥粘进环境变量就能用。如果后端给的是 PKCS#1而 JSEncrypt 解析报错可以试试下面这个转换思路让后端直接给 PKCS#8 格式或者用在线工具转换。注意这个工具选择一定要用本地或者可信的转换方法不要把公钥粘贴到来路不明的网站公钥虽然是公开的但涉及安全卫生习惯还是严谨点好。还有一个高频问题公钥里带有换行符存到 Apifox 环境变量时被压缩成一行导致解析失败。解决办法很简单环境变量里可以正常保存多行文本你粘贴时保留原始换行即可。如果担心复制过程中换行丢失可以在脚本里手动拼换行符const publicKey pm.environment.get(rsa_public_key); // 如果公钥被压缩成一行手动还原 const pemHeader -----BEGIN PUBLIC KEY-----; const pemFooter -----END PUBLIC KEY-----; const base64 publicKey.replace(pemHeader, ).replace(pemFooter, ).replace(/\s/g, ); const fullPublicKey ${pemHeader}\n${base64.match(/.{1,64}/g).join(\n)}\n${pemFooter};这段代码能把连续的 Base64 字符串按 64 个字符一行重新格式化这样不管环境变量里存储时换行是否丢失都能得到一个标准 PEM 格式的公钥。我在实际项目中遇到过一次公钥从配置中心导出时被压缩成一行的情况当时就是用这个办法解决的。3. 从登录接口获取 token前置脚本里实现请求串联3.1 在登录接口的前置脚本里发送登录请求这里有一个很多人没转过弯来的点我们现在本身就在测登录接口为什么要在一个请求的前置脚本里再发一次登录请求其实这个思路不是用来测登录接口本身的而是用来给其他接口做前置鉴权的。比如你有几十个业务接口都需要登录态不想在每个接口的前置脚本里各写一套登录逻辑就可以用一个专门的“登录辅助请求”放在业务接口的前置脚本里或者把登录接口单独拿出来在测试集合级别配置“前置脚本”统一执行。Apifox 里实现这个能力的关键是apifox.sendRequest这个 API 是同步发送请求的和pm.sendRequest的异步方式不同。同步意味着脚本会一直等待响应返回再继续执行下面的代码这对“先登录、后取 token、再改请求头”这种顺序依赖的场景特别合适。我实测下来apifox.sendRequest在断言脚本和前置脚本里都能用非常稳。下面是一个完整的登录请求前置脚本写法// 前置脚本在业务接口请求前先登录获取 token // 构造登录请求 const loginRequest { url: pm.environment.get(base_url) /api/auth/login, method: POST, header: { Content-Type: application/json }, body: { mode: raw, raw: JSON.stringify({ username: pm.environment.get(login_username), password: 123456 // 这里会做 RSA 加密 }) } }; // 对密码做 RSA 加密 const publicKey pm.environment.get(rsa_public_key); const encryptor new JSEncrypt(); encryptor.setPublicKey(publicKey); const rawBody JSON.parse(loginRequest.body.raw); rawBody.password encryptor.encrypt(123456); loginRequest.body.raw JSON.stringify(rawBody); // 同步发送登录请求 const loginResponse apifox.sendRequest(loginRequest); // 解析响应提取 token const responseData loginResponse.json(); const token responseData.data.token; // 把 token 存入当前环境 pm.environment.set(auth_token, token); console.log(获取到 token, token);这段代码完整覆盖了登录到取 token 的过程。注意几点第一登录请求的 URL 我用了环境变量base_url不要把域名写死在脚本里否则换环境跑用例时你会改到怀疑人生。第二密码加密的逻辑和上一节一样先构造原始请求体再加密替换最后重新序列化。第三loginResponse.json()假设响应是 JSON 格式如果后端返回的不是标准 JSON你可以改用loginResponse.text()然后自己截取。3.2 token 解析和写入环境变量的避坑经验解析 token 是另一个容易出问题的地方。不同后端的 token 返回结构差异太大有的在data.token有的在data.access_token有的直接整个响应就是一个裸 token 字符串还有的放在响应 header 的Authorization里。我的习惯是先用console.log把整个响应体打出来确认结构后再写解析代码。比如响应长这样{ code: 0, data: { token: eyJhbGciOiJIUzI1NiIs... }, message: success }那解析代码就是const token responseData.data.token;。如果返回结构复杂比如 token 要自己拼接或者需要从响应头里取可以这样处理// 从响应 header 里取 token示例Authorization 头 const authHeader loginResponse.headers.get(Authorization); const token authHeader.replace(Bearer , ); // 或者从 Set-Cookie 里取 const setCookie loginResponse.headers.get(Set-Cookie); const token setCookie.split(;)[0].split()[1];token 入库之后建议顺手设置一个过期时间。Apifox 环境变量没有内置过期机制但你可以在脚本里记录一个时间戳下次请求时判断是否过期过期就重新登录。这个技巧对长跑任务特别有用不然 token 失效后后面的请求会全部 401你还得手动重新跑登录。// 保存 token 和获取时间 pm.environment.set(auth_token, token); pm.environment.set(auth_token_time, Date.now()); // 其他接口前置脚本里判断是否过期比如 token 有效期 2 小时 const tokenTime pm.environment.get(auth_token_time); if (!tokenTime || Date.now() - tokenTime 2 * 60 * 60 * 1000) { // 重新登录逻辑 }这里的时间判断逻辑最好抽成一个公共脚本Apifox 支持“公共脚本”功能把常用的登录、RSA 加密、token 刷新逻辑放到公共脚本里各个接口直接调用维护起来省心得多。4. 请求接口 header 里带上 token两种主流方式4.1 方式一直接在请求头里引用变量最简单的方式是手动在接口的 header 里添加一个键值对值直接引用环境变量比如KeyAuthorizationValueBearer {{auth_token}}Apifox 会自动替换{{auth_token}}为环境变量里存储的真实 token。这种方式的好处是直观、不需要写任何脚本适合接口数量少、token 过期不频繁的项目。但这种方式有个明显的局限如果 token 放在环境变量里而你的环境变量值在脚本里是被动态更新的那接口发送时会自动取最新值吗答案是会。Apifox 在发送请求时会对变量做一次渲染所以只要auth_token这个环境变量的值已经更新即使你之前手动引用过{{auth_token}}也会用最新的值。不过如果 token 不是放在环境变量里而是放在全局变量里或者你需要在请求头发送前做额外处理比如加签名、拼接多个 token手动引用就有点力不从心了。4.2 方式二前置脚本动态设置请求头更灵活的方式是在请求接口的前置脚本里动态设置 header。用pm.request.headers.upsert这个方法可以新增或覆盖一个请求头。这个方法的好处是可以在设置 header 的同时做逻辑判断比如判断 token 是否存在、是否过期如果异常就走重新登录的分支。// 前置脚本动态设置请求头 token const token pm.environment.get(auth_token); if (!token) { // 如果没有 token触发登录流程 // 可以把登录逻辑抽到这里调用这里简单打印日志 console.error(未获取到 token请先执行登录接口); } else { pm.request.headers.upsert({ key: Authorization, value: Bearer token }); }如果你的项目用的不是标准的 Bearer Token而是自定义 header 名比如X-Token、X-Access-Token或者要求同时带多个 header写法是一样的只是把 key 换成对应的名称。upsert这个方法的语义是“有则更新没有则新增”所以即使接口模板里已经手动配过这个 header也会被脚本里的值覆盖。还有一类特殊场景后端要求 header 里的 token 也做一层加密或者要求把 RSA 加密后的密码作为 header 传递。这时候前置脚本的逻辑会复杂一点但套路不变先取原始值加密再用upsert写进去。4.3 多个环境切换时 header 的隔离策略我强烈建议在 Apifox 里把不同环境的auth_token分开存储。比如dev环境和test环境的 token 不要混用否则你从 dev 切到 test环境变量自动加载的是 test 的 token但如果你不小心在全局变量里存了一份 dev 的 token接口发送时可能会因为优先级问题用到全局变量的值导致鉴权失败。Apifox 变量优先级大致是请求参数里手动写的值 环境变量 全局变量。所以当你手动在 header 里引用{{auth_token}}时如果环境变量里没有但全局变量里有会自动取全局变量的值。这个机制有时候会掩盖问题你以为用的是环境变量实际用的全局变量。排查 header 鉴权失败时第一件事就是分别检查环境变量和全局变量里有没有同名变量。我在一次联调中踩过这个坑环境变量里设置了auth_token全局变量里也残留了一个旧的auth_token后端的 token 校验逻辑能看到两个值吗看不到Apifox 只会选一个。当时我百思不得其解为什么明明登录成功了接口还是 401后来把全局变量清空一切正常。所以建议在项目里规定token 这类敏感动态数据只放环境变量全局变量只放真正的公共静态数据比如固定的 app_id。5. 常见问题与排查技巧实录5.1 RSA 加密报错、token 解析失败的几个高频问题我把这段时间后台收到的和身边同事问过的问题汇总一下做成一个速查表方便你们对号入座。问题现象可能原因解决办法encrypt返回 false 或空字符串公钥格式不对或公钥包含不可见字符检查公钥是否为标准 PEM 格式去掉多余空格和换行或用手动换行代码格式化提示jsencrypt is not defined脚本环境里没有正确引入库确认使用require(jsencrypt)Apifox 内置支持不需要安装 npm 包每次请求第一次能成功第二次就 401token 存到了全局变量被旧值覆盖检查全局变量和环境变量是否有同名 token统一用环境变量登录响应解析出来 token 是 undefined返回结构和你写的不一致先用console.log(JSON.stringify(responseData))打印真实结构再定位字段请求头发送了但后端说没收到 tokenheader 名不对或大小写问题确认后端要求的 header 名有的框架大小写敏感提示apifox.sendRequest is not defined在旧版 Apifox 或非脚本上下文使用升级 Apifox 到最新版确认在脚本编辑区域调用5.2 一次真实的 token 失效排查过程讲一个我印象很深的排查案例。当时我在做一个用户中心接口的回归测试用例跑着跑着突然从第 40 个接口开始全部返回 401。第一反应是 token 过期了但我设置的 token 有效期是 24 小时不应该这么快过期。重新跑登录接口token 也正常。后来我在前置脚本里加了日志发现一个现象前面 39 个接口用的是新 token第 40 个接口开始环境变量里的 token 变成了另一个值。顺着这个线索继续查发现项目里存在两个环境dev和test。我当时用的是dev环境跑用例但某个前置脚本里写死了pm.environment.set(auth_token, xxx)这个pm.environment只操作当前环境。问题出在另一个接口的断言脚本里有人写了pm.globals.set(auth_token, oldToken)。由于环境变量和全局变量同名的优先级问题第 40 个接口发送时Apifox 优先取了环境变量按理说应该没问题。但诡异的是那个接口的请求头里没有写{{auth_token}}而是写死了token字段导致它永远用的是全局变量里的旧值。最终结论是不要在请求头里写死 token也不要混用全局变量。统一用环境变量 动态脚本注入问题就消失了。这个案例给我的教训是接口测试的鉴权链路看起来代码量不大但变量作用域一乱排查起来非常耗时。5.3 批量接口循环调用时的 token 与 header 注意事项如果你的集合里做了数据驱动需要循环调用同一批接口每个循环都带同样的 token那要注意一个隐藏问题前置脚本会在接口发送前执行如果循环次数很多登录逻辑也会跟着执行多次这会平白增加登录接口的压力。更合理的做法是只让第一个接口前置脚本去取 token或者在公共脚本里加一个判断如果环境变量里已有未过期的 token就直接复用不重复登录。我之前做过一个 500 次循环的性能测试为了模拟真实用户需要每个循环都用同一个 token 但不同参数。如果每个循环都重新登录登录接口会成为瓶颈测试结果完全失真。后来我在前置脚本里加了 token 存在性判断只有第一次循环才会真正发登录请求后面都直接复用。这个优化对压测场景很有用。// 循环调用的前置脚本token 存在就直接用不存在才登录 if (pm.environment.get(auth_token)) { pm.request.headers.upsert({ key: Authorization, value: Bearer pm.environment.get(auth_token) }); return; } // 不存在则走登录逻辑这里省略登录代码 // ...还有一点如果接口响应里出现了request header is too large的报错别光想着是不是 token 太长了。JWT 类型的 token 确实会比其他 token 长不少但一般也没到撑爆 header 的程度。优先检查是不是某个前置脚本多次调用了pm.request.headers.add导致同一个 header 被添加了 N 遍。add方法不做去重upsert才会。所以统一用upsert能避免这类重复 header 的问题。5.4 不同后端框架对 header 命名的兼容性最后聊一个偏门但实用的点后端框架对 header 的解析策略不一样。比如 Spring Cloud Gateway、Nginx、Node.js 的http-proxy这些组件有的会统一把 header 名转为小写有的则保留原样。如果你在 Apifox 里配的 header 是Authorization抓包时发现变成authorization不要慌这是正常的兼容处理后端能正确识别。但如果后端是自定义网关要求 header 名必须精确匹配那你必须严格按后端文档写。我见过一个案例后端要求x-auth-token测试人员在 Apifox 里写成了x-auth-Token后端识别不了一直报签名错误。这种问题用肉眼很难发现建议在后端文档里直接复制 header 名。另外如果你在 header 里传递的是带特殊字符的值比如加密后的密码或 token 中含、、/这类字符Apifox 默认不会对 header 值做 URL 编码这是对的。千万别手动编码否则后端解出来的值和原始值不一致。我有一次在别的工具里踩过这种坑Apifox 在这块的处理我觉得反而更符合预期。6. 最后再分享一个我常用的调试技巧前置脚本调试最怕的就是“看不到过程”。我的习惯是在脚本关键位置加console.logApifox 的响应面板里有一个“控制台”可以查看这些日志。比如 RSA 加密完成后先打一条日志看看密文长度是否正常一般 RSA 加密后的 Base64 字符串长度在 170~300 字符之间如果长度异常大概率是公钥格式问题。登录响应解析前先打一条响应全文确认字段路径不要盲写解析代码。等脚本稳定之后再把这些调试日志注掉或者在关键位置保留一条简短的日志方便以后排查。我自己会在正式用例里保留 token 获取成功和 token 失效这两条日志遇到问题打开控制台一眼就能看到是哪一步出了问题。如果你们项目里有同事刚接触 Apifox我建议把上面这套 RSA 加密登录、token 获取、header 注入的逻辑整理成一个公共脚本模板直接放到团队的知识库里大家照着改就行能少走不少弯路。
返回列表