ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

微信小程序云函数搭建HTTP代理抓包服务,实现远程调试与网络请求分析

微信小程序云函数搭建HTTP代理抓包服务,实现远程调试与网络请求分析 在开发微信小程序时后端逻辑的快速部署与调试是提升效率的关键。你是否遇到过这样的困境云函数本地测试正常但部署到云端后行为诡异日志难以定位想抓包分析网络请求却无从下手尤其是在移动端真机调试时过程更是繁琐。本文将为你提供一套完整的解决方案利用云函数实现一个“一键部署”的HTTP代理抓包服务让你能在手机上轻松捕获和分析小程序发出的网络请求无论是开发调试还是线上问题排查都能事半功倍。本文将从零开始手把手教你搭建一个运行在云函数上的抓包服务并通过简单的配置让手机流量经过此服务实现请求的捕获、查看和重放。内容涵盖云函数开发、代理服务器原理、手机代理配置以及安全注意事项适合有一定Node.js和小程序开发基础的开发者。1. 背景与核心概念为什么需要云函数抓包在微信小程序开发中网络请求是核心交互方式。当遇到请求失败、数据异常或性能问题时查看具体的请求和响应内容是最直接的排查手段。传统抓包方式的局限性Charles/Fiddler 配置复杂需要在电脑上安装软件配置代理并在手机端安装证书步骤多且容易因证书问题失败。依赖本地网络手机和电脑必须在同一局域网限制了远程调试的可能性。云函数调试黑盒小程序云函数运行在云端其发出的对外请求如调用第三方API无法通过本地抓包工具直接捕获。生产环境调试困难线上小程序的问题复现和排查难以使用本地抓包工具介入。云函数抓包方案的优势随时随地只要云函数部署成功在任何有网络的手机上都可配置使用。中心化日志所有经过代理的请求都会在云函数日志中集中呈现方便查看和搜索。规避证书问题通过HTTP代理非HTTPS中间人解密方式可以避免复杂的HTTPS证书安装与信任问题专注于HTTP请求内容的分析。复用与共享部署一次整个团队都可以使用同一个代理地址进行调试。核心原理我们将在云函数上部署一个简单的HTTP代理服务器。将手机的HTTP代理设置为该云函数的公网访问地址。此后手机上的小程序或其他App发出的HTTP请求就会先发送到我们的云函数云函数记录下请求详情后再将请求转发给真正的目标服务器并将响应返回给手机同时记录响应详情。2. 环境准备与版本说明在开始编码前请确保你已准备好以下环境。本文示例以微信小程序云开发TCB环境为例但其原理同样适用于其他支持Node.js的云函数平台如阿里云函数计算、腾讯云SCF。基础环境操作系统Windows/macOS/Linux均可用于本地开发和部署。Node.js版本 14.x 或 16.x LTS。建议使用16.x兼容性更好。可通过node -v命令检查。npm通常随Node.js安装用于管理依赖。微信开发者工具最新稳定版并已开通云开发服务。云环境前提拥有一个已开通的微信小程序并已开通云开发。在微信开发者工具中已创建云环境并获取到云环境IDenvId。确保你的云函数有外网访问权限通常云函数的HTTP访问地址是公网可访问的。项目初始化在微信开发者工具中创建一个新的小程序项目并勾选“云开发”模板。或者在你的现有小程序项目根目录下初始化云函数目录。# 假设项目根目录为 /miniprogram # 进入云函数目录 cd miniprogram/cloudfunctions # 创建一个新的云函数命名为 proxy-capture # 在开发者工具中右键 cloudfunctions 文件夹选择“新建Node.js云函数”更便捷3. 核心原理与云函数设计我们的抓包云函数本质上是一个HTTP代理服务器。一个最简单的HTTP代理需要做两件事接收客户端请求解析客户端手机发来的HTTP请求。转发并响应将解析后的请求转发到目标服务器再将目标服务器的响应返回给客户端并在此过程中记录信息。为了避免处理复杂的HTTPS隧道CONNECT方法我们初期聚焦于拦截和记录HTTP请求。对于小程序而言其核心业务API请求大多为HTTPS但我们的代理可以处理其HTTP层面的信息如域名、路径、头部或处理一些内部调试用的HTTP请求。云函数入口设计微信小程序云函数的入口文件是index.js它导出一个主函数。我们将在这个函数里实现代理逻辑。// cloudfunctions/proxy-capture/index.js const http require(http); const https require(https); const url require(url); /** * 云函数入口 * param {Object} event - 触发事件参数包含请求信息 * param {Object} context - 上下文信息 */ exports.main async (event, context) { // 微信云函数通过HTTP访问时event 包含了请求的详细信息 // 我们需要根据 event 来构造一个对外请求 const requestUrl event.queryString || event.url; // 从查询参数或直接获取url const method event.httpMethod || GET; const headers event.headers || {}; const body event.body; if (!requestUrl) { return { statusCode: 400, body: JSON.stringify({ error: Missing target URL parameter }), }; } // 记录请求日志这里输出到云函数日志 console.log([Capture] ${new Date().toISOString()} | ${method} ${requestUrl}); console.log([Headers] , JSON.stringify(headers, null, 2)); if (body method ! GET method ! HEAD) { console.log([Body] , typeof body string ? body : JSON.stringify(body, null, 2)); } // 解析目标URL const parsedUrl url.parse(requestUrl); const isHttps parsedUrl.protocol https:; // 准备请求选项 const options { hostname: parsedUrl.hostname, port: parsedUrl.port || (isHttps ? 443 : 80), path: parsedUrl.path, method: method, headers: { ...headers }, }; // 重要移除可能引起问题的头部如 host由代理服务器自动设置 delete options.headers[host]; delete options.headers[content-length]; // 由后续请求自动计算 // 发起代理请求 return new Promise((resolve, reject) { const client isHttps ? https : http; const proxyReq client.request(options, (proxyRes) { let responseData []; proxyRes.on(data, (chunk) { responseData.push(chunk); }); proxyRes.on(end, () { const rawBody Buffer.concat(responseData); // 记录响应日志 console.log([Response] Status: ${proxyRes.statusCode}); console.log([Response Headers] , JSON.stringify(proxyRes.headers, null, 2)); // 注意二进制或过大响应体不宜直接console.log try { // 尝试以文本形式记录如果是JSON则美化输出 const bodyStr rawBody.toString(utf-8); if (proxyRes.headers[content-type]?.includes(application/json)) { console.log([Response Body] , JSON.stringify(JSON.parse(bodyStr), null, 2)); } else { // 非JSON或文本只记录长度和类型 console.log([Response Body] Type: ${proxyRes.headers[content-type]}, Length: ${rawBody.length} bytes); } } catch (e) { console.log([Response Body] Binary or invalid data, Length: ${rawBody.length} bytes); } // 构造返回给客户端的响应 resolve({ statusCode: proxyRes.statusCode, headers: proxyRes.headers, body: rawBody.toString(base64), // 云函数要求返回base64编码的二进制数据 isBase64Encoded: true, }); }); }); proxyReq.on(error, (err) { console.error([Proxy Error] , err); reject({ statusCode: 502, body: JSON.stringify({ error: Proxy request failed, detail: err.message }), }); }); // 如果有请求体则写入 if (body (method POST || method PUT || method PATCH)) { const requestBody typeof body string ? body : JSON.stringify(body); proxyReq.write(requestBody); } proxyReq.end(); }); };代码关键点解释日志记录所有关键的请求和响应信息都通过console.log输出这些日志可以在微信云开发控制台的“云函数日志”中查看。请求转发使用Node.js原生的http/https模块根据目标URL的协议发起新的请求。头部处理删除客户端原始请求头中的host字段避免转发错误content-length由Node.js自动计算。响应处理将响应体转换为Buffer然后以Base64格式返回。这是云函数HTTP触发器返回二进制数据的标准方式isBase64Encoded: true。错误处理捕获代理请求过程中的错误如网络超时、目标服务器无响应并返回502错误。4. 完整实战部署与配置抓包服务4.1 创建并部署云函数创建云函数文件在miniprogram/cloudfunctions目录下创建文件夹proxy-capture将上面的index.js代码放入。初始化package.json在proxy-capture文件夹内运行npm init -y生成package.json。由于我们使用了Node.js原生模块无需额外安装依赖。上传并部署在微信开发者工具中右键cloudfunctions目录下的proxy-capture文件夹。选择“上传并部署云端安装依赖”如果你有package.json或“上传并部署所有文件”。等待部署完成控制台会显示部署成功。4.2 获取云函数HTTP访问地址部署成功后你需要获取该云函数的HTTP触发地址。登录 微信云开发控制台 。进入你的云环境。在左侧菜单点击“云函数”找到proxy-capture函数。点击“函数配置”或“触发方式”找到“HTTP访问地址”。它通常格式为https://你的服务域名/tcb-xxxx/proxy-capture。复制这个地址这就是你的抓包代理服务器地址。4.3 在手机上配置HTTP代理现在将你的手机连接到与电脑不同的网络例如使用4G/5G移动网络以模拟真实远程调试场景。Android 手机配置以原生Android 12为例进入“设置” “网络和互联网” “高级” “代理”。选择“手动”。代理主机名填写你的云函数域名如service-xxx-xxx.gz.apigw.tencentcs.com注意不是完整的URL是域名部分。你需要从HTTP地址中提取。例如地址是https://service-xxx.gz.apigw.tencentcs.com/release/proxy-capture则主机名是service-xxx.gz.apigw.tencentcs.com。代理端口填写443因为云函数地址是HTTPS默认端口443。这是一个关键点我们实际上是将云函数的HTTPS端点作为了一个HTTP代理服务器。某些代理客户端或系统可能要求特殊的端口处理但标准HTTPS代理使用443端口。保存。iOS 手机配置连接至Wi-Fi网络即使是移动网络也需要先连上一个Wi-Fi来配置代理配置完成后可以关闭Wi-Fi使用移动网络。点击已连接Wi-Fi右侧的(i)信息图标。滑动到最底部找到“配置代理”。选择“手动”。服务器填写云函数域名同上仅域名。端口填写443。保存。重要说明这种将HTTPS端点作为HTTP代理的方式依赖于云函数平台对HTTPCONNECT方法的支持。我们上面的简单代码并未实现CONNECT方法因此它主要适用于捕获由我们自己的客户端可以改造的发起的、明确将请求发送到代理地址的流量。对于系统全局代理需要更复杂的实现来支持HTTPS隧道。作为简化方案我们可以创建一个专用的“抓包测试”小程序页面该页面内发起的网络请求手动指定代理地址。4.4 创建测试页面并发送请求在小程序项目中创建一个测试页面例如pages/proxyTest/proxyTest。proxyTest.js// pages/proxyTest/proxyTest.js Page({ data: { result: , log: [], }, onLoad() { this.addLog(页面加载准备测试代理请求...); }, // 手动发起一个经过代理的请求 makeProxyRequest() { this.addLog(开始发起代理请求...); const cloudFunctionUrl 你的云函数HTTP访问地址; // 替换为你的实际地址 const targetUrl https://httpbin.org/get; // 一个用于测试的公共API // 关键将目标URL作为参数传递给云函数 const proxyRequestUrl ${cloudFunctionUrl}?url${encodeURIComponent(targetUrl)}; wx.request({ url: proxyRequestUrl, // 请求地址是我们的云函数 method: GET, success: (res) { this.addLog(请求成功状态码${res.statusCode}); this.setData({ result: JSON.stringify(res.data, null, 2), }); // 此时云函数日志中应该已经记录了这次请求和响应的详细信息 this.addLog(请查看云开发控制台中的云函数日志查看抓包详情。); }, fail: (err) { this.addLog(请求失败${err.errMsg}); this.setData({ result: 请求失败: ${JSON.stringify(err)}, }); }, }); }, addLog(msg) { const newLog [${new Date().toLocaleTimeString()}] ${msg}; this.setData({ log: [...this.data.log, newLog], }); console.log(newLog); }, });proxyTest.wxml!-- pages/proxyTest/proxyTest.wxml -- view classcontainer button typeprimary bindtapmakeProxyRequest发起代理抓包测试请求/button view classlog-area text操作日志/text view wx:for{{log}} wx:keyindex stylefont-size: 12px; color: #666; margin: 5px 0; {{item}} /view /view view classresult-area text请求结果/text textarea value{{result}} disabled stylewidth:100%; height: 300px; background: #f5f5f5;/ /view /view4.5 查看抓包结果在微信开发者工具中将小程序切换到proxyTest页面。点击“发起代理抓包测试请求”按钮。小程序会向你的云函数发起请求云函数会去请求https://httpbin.org/get并将结果返回。打开微信云开发控制台进入“云函数” -proxy-capture- “日志”。你应该能看到类似以下的日志输出这就是抓包到的请求和响应详情[Capture] 2023-10-27T06:30:15.123Z | GET https://httpbin.org/get [Headers] { user-agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.43(0x18002b2d) NetType/WIFI Language/zh_CN, accept: */*, ... } [Response] Status: 200 [Response Headers] { content-type: application/json, date: Fri, 27 Oct 2023 06:30:15 GMT, ... } [Response Body] { args: {}, headers: { Host: httpbin.org, User-Agent: Mozilla/5.0 ..., ... }, origin: 你的云函数服务器IP, url: https://httpbin.org/get }至此你已经成功部署了一个云函数抓包服务并完成了第一次抓包测试。5. 进阶实现支持POST与复杂请求上面的基础版本仅支持GET请求和简单的转发。为了捕获小程序中常见的POST、PUT等请求我们需要改进云函数使其能正确处理请求体并支持更多的HTTP方法。改进版index.js(支持POST等)// cloudfunctions/proxy-capture/index.js const http require(http); const https require(https); const url require(url); exports.main async (event, context) { // 1. 解析事件数据 const { path, httpMethod: method, headers, queryStringParameters: query, body, isBase64Encoded } event; // 2. 从查询参数或自定义头部获取目标URL更安全的方式 let targetUrl query?.url || headers[x-target-url]; if (!targetUrl) { // 也可以尝试从请求路径中解析但这里我们要求明确传递 return { statusCode: 400, headers: { Content-Type: application/json }, body: JSON.stringify({ code: -1, message: Missing target URL. Please provide url query parameter or X-Target-URL header. }), }; } // 3. 记录请求 console.log([${new Date().toISOString()}] ${method} ${targetUrl}); console.log([Req Headers], JSON.stringify(headers, null, 2)); if (body method ! GET method ! HEAD) { let reqBody body; if (isBase64Encoded) { reqBody Buffer.from(body, base64).toString(utf-8); } console.log([Req Body], reqBody.length 500 ? reqBody.substring(0, 500) ... : reqBody); } // 4. 解析目标URL并准备代理请求选项 const parsedUrl url.parse(targetUrl); const isHttps parsedUrl.protocol https:; const options { hostname: parsedUrl.hostname, port: parsedUrl.port || (isHttps ? 443 : 80), path: parsedUrl.search ? ${parsedUrl.pathname}${parsedUrl.search} : parsedUrl.pathname, method: method, headers: { ...headers }, }; // 关键移除或覆盖一些头部 delete options.headers[host]; delete options.headers[content-length]; // 由代理请求自动设置 // 可以添加一些自定义头部用于标识代理请求 options.headers[x-forwarded-by] wx-cloud-proxy-capture; // 5. 处理请求体 let requestBody null; if (body (method POST || method PUT || method PATCH)) { requestBody isBase64Encoded ? Buffer.from(body, base64) : body; if (typeof requestBody string) { requestBody Buffer.from(requestBody); } } // 6. 发起代理请求 return new Promise((resolve, reject) { const client isHttps ? https : http; const proxyReq client.request(options, (proxyRes) { const responseBuffers []; proxyRes.on(data, (chunk) responseBuffers.push(chunk)); proxyRes.on(end, () { const rawResponse Buffer.concat(responseBuffers); // 记录响应 console.log([Resp Status] ${proxyRes.statusCode}); console.log([Resp Headers], JSON.stringify(proxyRes.headers, null, 2)); const contentType proxyRes.headers[content-type] || ; if (contentType.includes(application/json) || contentType.includes(text/)) { try { const respText rawResponse.toString(utf-8); if (contentType.includes(application/json)) { console.log([Resp Body], JSON.stringify(JSON.parse(respText), null, 2)); } else { console.log([Resp Body Preview], respText.length 1000 ? respText.substring(0, 1000) ... : respText); } } catch (e) { console.log([Resp Body] Binary or malformed data, length:, rawResponse.length); } } else { console.log([Resp Body] Binary data, Content-Type: ${contentType}, Length: ${rawResponse.length}); } // 返回给小程序客户端 resolve({ statusCode: proxyRes.statusCode, headers: proxyRes.headers, body: rawResponse.toString(base64), isBase64Encoded: true, }); }); }); proxyReq.on(error, (err) { console.error([Proxy Request Error], err); reject({ statusCode: 502, headers: { Content-Type: application/json }, body: JSON.stringify({ code: 502, message: Bad Gateway, detail: err.message }), }); }); // 写入请求体 if (requestBody) { proxyReq.write(requestBody); } proxyReq.end(); }); };对应的测试页面更新 (proxyTest.js部分代码)// 发起一个POST请求测试 makePostRequest() { this.addLog(开始发起POST代理请求...); const cloudFunctionUrl 你的云函数HTTP访问地址; const targetUrl https://httpbin.org/post; const postData { project: WX MiniProgram, test: proxy capture, timestamp: Date.now(), }; const proxyRequestUrl ${cloudFunctionUrl}?url${encodeURIComponent(targetUrl)}; wx.request({ url: proxyRequestUrl, method: POST, header: { Content-Type: application/json, // 也可以将目标URL放在头部更安全避免URL参数被日志记录 // X-Target-URL: targetUrl }, data: postData, // 小程序会自动将data序列化为JSON字符串 success: (res) { this.addLog(POST请求成功状态码${res.statusCode}); this.setData({ result: JSON.stringify(res.data, null, 2), }); this.addLog(请查看云函数日志。); }, fail: (err) { this.addLog(POST请求失败${err.errMsg}); }, }); }6. 常见问题与排查思路在部署和使用过程中你可能会遇到以下问题问题现象可能原因排查思路与解决方案云函数部署失败提示依赖错误package.json中声明的依赖与云环境不兼容或云函数根目录不正确。1. 检查package.json确保没有不必要的依赖。我们的示例无需额外依赖可以删除package.json中的dependencies或留空。2. 在微信开发者工具中右键云函数目录选择“上传并部署所有文件”。3. 检查云环境选择是否正确。手机配置代理后无法上网1. 代理服务器地址或端口错误。2. 云函数未正确响应CONNECT方法对HTTPS流量。3. 网络策略限制如公司网络。1.确认地址和端口确保手机代理配置的“主机名”是云函数域名不含https://和路径端口是443。2.本方案限制当前代码未实现完整的HTTP代理协议尤其是CONNECT方法因此不适合作为系统全局代理。请严格按照第4.4节的方法在小程序内通过代码指定代理地址发起请求。3. 使用本文的“测试页面”方法进行验证而非系统全局代理。云函数日志中看不到请求记录1. 请求未到达该云函数。2. 云函数日志有延迟。3. 请求路径或触发方式错误。1. 检查测试页面中cloudFunctionUrl是否正确。2. 在云开发控制台进入该云函数的“日志”页面确保时间范围正确并尝试刷新。3. 云函数HTTP触发地址通常包含环境名和函数名确保完整复制。请求返回400错误提示缺少URL测试代码中没有正确传递目标URL参数。检查proxyRequestUrl的拼接确保url查询参数已正确编码 (encodeURIComponent)。或者尝试使用X-Target-URL请求头的方式传递。请求返回502或超时错误1. 目标服务器不可达或拒绝连接。2. 云函数执行超时默认3秒。3. 网络问题。1. 先用浏览器或curl命令测试目标URL如https://httpbin.org/get是否可访问。2. 在云开发控制台增加云函数的超时时间例如设置为10秒。位置云函数 - 函数配置 - 执行超时时间。3. 检查云函数日志中的详细错误信息。抓包内容乱码或无法解析响应体是二进制数据如图片、文件流被错误地以文本形式记录或显示。这是正常现象。代码中已做判断对于非文本类型如image/png,application/octet-stream只记录长度和类型。如果需要查看二进制内容可以考虑将响应体以Base64形式记录到更持久的存储如云数据库但需注意日志大小限制。云函数报错Error: getaddrinfo ENOTFOUND目标URL的域名无法解析。检查目标URL的域名拼写是否正确。确保云函数的运行环境通常是腾讯云内网能够解析公网域名。7. 最佳实践与工程建议将云函数用于抓包是一个取巧的调试方法但在实际工程中需要考虑更多安全第一谨慎使用最小权限该云函数具有从内网访问公网的能力务必不要泄露其HTTP访问地址。访问控制在生产环境中应为该云函数设置访问鉴权。微信云函数的HTTP触发器可以配置“免鉴权”和“鉴权”。对于抓包函数强烈建议在云开发控制台中将其触发方式设置为**“需要鉴权”**。这样请求时必须携带云开发环境ID和自定义密钥可以在测试代码中通过wx.cloud.callFunction调用再在云函数内解析事件参数而不是直接暴露HTTP URL。内容过滤避免在日志中记录敏感信息如密码、Token、身份证号等。可以在代码中添加过滤逻辑对特定的Header如Authorization或请求体字段进行脱敏处理。性能与成本优化日志管理云函数日志有存储限制和查询成本。长期运行会产生大量日志。建议定期清理日志或只在高频调试期间开启此功能。函数超时与内存转发大文件或慢响应可能导致函数超时默认3秒。根据需要调整超时时间和内存规格但这会增加成本。异步处理对于只记录不关心响应的场景可以考虑将日志写入异步队列如云数据库、消息队列然后立即返回响应给客户端提升用户体验。功能增强方向请求/响应存储将抓包数据结构化存储到云数据库便于前端页面查询、筛选和回放。可以存储请求URL、方法、头部、体、响应状态码、响应体可截断或Hash、时间戳等。构建管理界面开发一个小程序管理页面用于查看历史抓包记录、筛选、重放请求甚至修改参数后重放。支持HTTPS隧道实现CONNECT方法处理使其能作为真正的系统级HTTP/HTTPS代理。但这涉及TCP流处理和证书动态生成复杂度极高且云函数对长连接支持有限不推荐在云函数中实现。流量过滤只捕获特定域名或包含特定关键词的请求减少噪音。替代方案考量专用代理服务器对于长期、团队的抓包需求搭建一个专用的代理服务器如使用Squid、mitmproxy部署在自有服务器是更稳定和功能强大的选择。商业抓包工具对于移动端HTTPS抓包Charles、Fiddler、Reqable等工具提供了成熟的图形化界面和强大的断点、修改功能。云函数方案更适合轻量、临时、远程或无电脑环境的特定调试场景。通过本文的指南你掌握了利用微信小程序云函数搭建一个轻量级远程抓包服务的方法。这个方案的核心价值在于其便捷性和可访问性让你能在任何地方快速启动一个调试窗口。记住它主要用于开发和测试阶段的特定问题排查切勿用于生产环境监控或处理敏感数据。
返回列表