小程序与 H5 能不能直连第三方 API:先算清密钥、跨域与域名这三项
生产环境下默认不要让小程序或 H5 直接持有第三方 API 密钥,而应由自有服务端做一层转发:密钥留在服务端,跨域与域名问题集中到一处处理,超时、重试、限流与错误归一也有统一落点。H5 的跨域由服务端 CORS 响应头决定,小程序的网络请求受平台域名配置约束,两者不能互相替代;具体配置项与现行规则需按官方文档核对。
- 默认方案:客户端只调用自有后端,第三方密钥与上游地址都不进前端。
- H5 受同源策略约束,小程序受平台域名配置约束,同一套配置不能覆盖两端。
- 转发层至少要承担鉴权、参数白名单、超时重试、错误归一与限流计数。
结论先说:面向生产环境的调用,默认不要让小程序或 H5 直接持有第三方 API 的密钥,也不要在客户端拼接第三方请求。客户端只调用自有服务端,由服务端去调第三方。代价是多一层转发;换来的是密钥不出服务端、跨域问题集中到一处、超时与限流有统一落点。本文未联网核对任何平台现行规则,凡涉及具体配置字段的地方,按“需查官方文档”处理。
三种链路,先选定一种#
| 链路 | 密钥所在位置 | 主要问题 |
|---|---|---|
| 客户端直连第三方 | 客户端(运行时可被取出) | 密钥暴露;H5 受同源策略拦截;小程序需配置请求域名 |
| 客户端 → 自有后端 → 第三方 | 服务端 | 需自建转发层、鉴权与限流 |
| 客户端 → 自有后端 → 缓存/聚合 | 服务端 | 多一层状态,需处理缓存失效与计费口径 |
密钥进了客户端就等于公开#
小程序代码包会下发到用户设备,H5 的脚本会被浏览器完整加载。字符串混淆、分段拼接或加密常量都改变不了一个事实:运行时要拿到它,就能被取出来。因此把 AppSecret、Bearer token 放进前端,只是延迟暴露时间,不构成保护。这是设计与推理层面的判断,不依赖某家平台的具体实现细节。
跨域和域名是两套不同的约束#
H5 受浏览器同源策略约束,跨域请求是否放行由服务端返回的 CORS(Cross-Origin Resource Sharing,跨源资源共享)响应头决定;小程序的网络请求不在浏览器里执行,走的是宿主提供的能力,能否访问某个域名由平台配置决定。两者规则、配置位置和生效方式都不同:后端加了 CORS 头,不会让小程序域名自动生效;小程序里配了域名,也不会改变 H5 的跨域行为。
具体字段名、是否允许通配符、是否要求 HTTPS 或备案,本次材料未提供,待核实,应以各平台官方文档为准。可行的做法在两端是一致的:把第三方域名收进服务端,客户端只面对自己的域名。
转发层的最小职责#
- 鉴权:确认请求来自自己的用户会话。裸转发等于给陌生人开了一个带密钥的出口。
- 参数白名单:只透传必要查询参数,不让客户端改写上游路径或方法。
- 超时与重试:分别设置连接与读取超时;只对幂等请求、只对网络错误与 5xx 重试。
- 错误归一:把上游错误映射成自己的错误结构,不回吐上游原始报文与任何密钥相关头信息。
- 限流与配额:按用户或设备维度计数,计数口径先与业务对齐再上线。
一段未执行的示例#
下面的示例只说明结构,未实际运行;域名与密钥均为占位(example.com、YOUR_KEY),不指向任何真实端点。
const express = require('express');
const app = express();
const UPSTREAM = 'https://api.example.com/v1/items'; // 占位
app.get('/api/items', async (req, res) => {
// 1) 先校验自己的会话,实现省略
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const page = Number(req.query.page) || 1; // 2) 白名单化参数
const r = await fetch(`${UPSTREAM}?page=${page}`, {
headers: { Authorization: `Bearer ${process.env.UPSTREAM_KEY}` }, // YOUR_KEY
signal: controller.signal
});
if (!r.ok) return res.status(502).json({ code: 'UPSTREAM_ERROR' });
res.json({ code: 'OK', data: await r.json() }); // 3) 归一化后再返回
} catch (e) {
res.status(504).json({ code: 'UPSTREAM_TIMEOUT' });
} finally {
clearTimeout(timer);
}
});
值得提前想的边界条件
- 移动网络切换(Wi-Fi 与蜂窝网)会放大超时概率,重试需要退避,不能等间隔猛打。
- 若上游按 IP 限流,服务端集中调用会把多用户压力合并成单一出口,需提前核对配额与计费方式(待核实具体 API 的限额规则)。
- 计费或写入类接口需要幂等键,避免重试造成重复扣费或重复创建;上游是否支持幂等键同样待核实。
- 上游 4xx 通常代表请求本身有问题,重试无益,应直接暴露为业务错误。
上线前核对的四项#
- 密钥只存在于服务端环境变量,不进仓库、不进代码包。
- 日志不打密钥、不打完整上游报文;错误码保留可定位信息但不外泄细节。
- 客户端请求地址只有自有域名,第三方域名不出现在前端配置里。
- 域名与 CORS 配置以官方文档当次核对结果为准,不依赖本文推断。