微信小程序与 H5 调用第三方 API:密钥该放哪、请求该走哪条链路
小程序和 H5 都不应直接持有第三方 API 密钥。可维护的做法是前端只请求自有后端,由服务端代理做端点白名单、方法限制、超时与错误归一化;小程序侧还需在小程序管理后台配置 request 合法域名,H5 侧需处理同源策略与 CORS。文中给出未执行的服务端代理与小程序调用骨架,具体平台规则以官方文档为准。
结论先说:小程序和 H5 调用第三方 API 的“正确姿势”不是在页面里写一个 fetch 或 wx.request 就完事,而是把第三方调用收到自己的服务端,前端只与自有域名通信。这样做的原因不是风格问题,而是密钥载体、平台域名规则和浏览器同源策略共同决定的。
先分清两条链路#
小程序与 H5 的约束来源不同,混着写很容易漏项。
| 维度 | 微信小程序 | H5 页面 |
|---|---|---|
| 发起方式 | wx.request 等小程序 API | fetch / XMLHttpRequest |
| 域名限制 | 需在小程序管理后台配置 request 合法域名;按微信官方文档要求,域名须使用 HTTPS,不能使用 IP 或 localhost | 受同源策略约束,跨域需第三方响应 CORS 头 |
| 密钥可见性 | 安装包可被解包,包内字符串可读 | JS 可在浏览器直接查看 |
上表关于小程序域名的要求属于常见平台规则,本文未联网核验,具体条款、端口与备案要求请以微信公众平台开发者文档当时版本为准(待核实)。
密钥不进前端,代理只开白名单#
把 AppSecret、第三方 API Key 写进小程序包或 H5 的 JS,等价于公开。可维护的边界是:密钥只存在于服务端环境变量,前端请求路径里只出现业务名,不出现上游地址。
服务端代理需要收窄四件事,否则代理本身会变成新的攻击面:
- 端点白名单:用固定映射表把内部路径映射到上游路径,不要让客户端传入完整 URL,避免服务端请求伪造(SSRF)。
- 方法与参数约束:只放行需要的 HTTP 方法;查询参数按键名过滤,避免客户端覆盖签名、分页上限等关键字段。
- 超时与中断:给上游调用设显式超时,超时后中止请求并向客户端返回可识别的错误码。
- 响应裁剪:只回传前端需要的字段,避免上游返回的调试信息、内部标识被带到客户端。
服务端代理骨架(未执行的示例)
下面的代码未实际运行,仅用于说明边界位置,不是可直接上线的实现;域名与密钥为占位。
// 未执行的示例:Node.js + Express,仅示意白名单与超时
import express from "express";
const app = express();
const ALLOWED = new Map([
["weather", "https://api.example.com/v1/weather"],
]);
app.get("/api/:name", async (req, res) => {
const target = ALLOWED.get(req.params.name);
if (!target) return res.status(404).json({ error: "endpoint_not_allowed" });
const url = new URL(target);
// 只透传允许的查询参数
const q = req.query.city;
if (typeof q === "string") url.searchParams.set("city", q);
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(), 5000);
try {
const upstream = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.UPSTREAM_KEY}` }, // 密钥仅在服务端
signal: ac.signal,
});
if (!upstream.ok) return res.status(502).json({ error: "upstream_failed" });
const data = await upstream.json();
res.json({ city: data.city, temp: data.temp }); // 裁剪字段
} catch {
res.status(504).json({ error: "upstream_timeout" });
} finally {
clearTimeout(timer);
}
});
小程序端调用骨架(未执行的示例)
小程序只请求自有域名,并把超时与错误分类写在调用处,避免把上游失败统一显示成“网络异常”。
// 未执行的示例:域名替换为已在小程序后台配置的自有域名
wx.request({
url: "https://your-domain.example.com/api/weather?city=shanghai",
timeout: 5000,
success(res) {
// 400 参数问题、502 上游失败、504 超时应分别处理
},
fail() {
// 网络不可达、域名未配置等
},
});
上线前的核对清单#
- 小程序后台的 request 合法域名是否已包含生产域名,证书是否有效且链完整(证书细节待核实)。
- H5 是否只调用自有域名;若确有直连第三方需求,第三方是否对目标源返回 CORS 头、是否支持凭证模式。
- 代理层是否做了端点白名单、参数过滤、超时与并发限制。
- 日志是否脱敏:请求头中的 Authorization、查询串中的签名参数不应落盘明文。
- 密钥是否可轮换,轮换窗口内旧密钥是否可控地失效。
- 错误码是否能区分“客户端参数错”“上游错”“超时”,便于前端展示与告警分级。
剩余的不确定项#
本文没有联网核验,也未在真机或浏览器中实际运行上述代码。以下信息需要你按当前版本自行确认:小程序的域名配置规则与数量上限、wx.request 的默认超时与可配置范围、H5 直连场景下第三方的 CORS 策略、以及所选用第三方 API 的鉴权方式与配额口径。这些都属于会随版本变化的平台约束,宜以官方文档与实测记录为准。