用户登录

技术文章

外部链接将在新标签页打开。

小程序与 H5 调用第三方 API:密钥不上前端的中转方案

一句话回答

小程序和 H5 都不应该直连第三方 API。正确做法是把请求收敛到自有后端或云函数中转,密钥只留在服务端,域名白名单、超时重试、限流和监控都在中转层一次做完,前端只接收业务字段。

核心要点 TL;DR
  • 密钥放前端等于公开,小程序包可解、H5 源码明文
  • 中转层统一设定连接 1 秒、总超时 3 秒、重试 2 次
  • xx 不重试,只对网络错误、5xx 和 429 做指数退避
  • 限流用 QPS 加并发连接双阈值,单靠带宽阈值挡不住刷配额

结论:小程序和 H5 都不该直连第三方 API。请求走自有后端或云函数中转,密钥只留服务端,前端只收业务字段,白名单、超时、重试、限流在中转层一次做完。

用大白话说:小程序像一个只能走指定门禁的房间,门外的事它自己办不了;H5 则像一个在别人地盘上说话的人,对方不点头你就拿不到数据。第三方 API 就是门外那家店,你得派自己的服务员去跑腿,而不是把店门钥匙塞给房间里的每个人。

为什么小程序不能直接调用第三方 API?#

三个硬约束,任何一个都能让直连方案在上线当天挂掉。

约束一:合法域名(request 合法域名)。微信要求所有 wx.request 走 HTTPS、域名已备案、TLS 1.2 及以上,不支持 IP 地址,也不支持 443 以外的端口。开发者工具里那个“不校验合法域名”的勾选项只在工具里有效,真机调试和线上都不会放行。我见过最常见的翻车场景是:接口域名是别人家的,你根本没权限把它加进自己的小程序后台——因为域名归属校验要求你把校验文件放到对方服务器的根目录。

约束二:跨域预检(CORS Preflight)。H5 侧不受小程序那套限制,但要面对浏览器的同源策略。只要你在请求里带了自定义头(比如 apikey),浏览器就会先发一个 OPTIONS 预检。第三方接口几乎不会为你的域名返回 Access-Control-Allow-Origin,预检直接失败,控制台报一条红字,response 为空。

约束三:密钥等于公开。小程序的 .wxapkg 包可以解包,H5 的 JS 是明文传输。任何写在前端的 apikey,等于写在公网上。

对比表:直连与后端中转的差异

项目前端直连后端中转
密钥存放前端包内,可被解包提取服务端环境变量或配置中心
域名白名单需要第三方域名归属权只需自己的域名
CORS 预检大概率被拦不存在,服务端之间无同源限制
限流与配额保护无法做,被刷只能干看着令牌桶加并发连接双阈值
可观测性只有前端埋点,看不到上游状态码可记录上游 5xx、P95、缓存命中率
重复请求每次穿透到上游,烧配额Redis 缓存,命中即返回

有人会问:那用云函数(Cloud Function)算不算中转?算。云函数跑在服务端,密钥放在环境变量里,出口 IP 可控,只是要额外注意冷启动带来的首包延迟,通常比常驻服务多 200~800ms。

如何设计一条稳定的中转链路?#

链路只有三段:小程序 / H5 → 自有 API(Nginx 1.24 + PHP-FPM 8.2 或 Node 20)→ 上游 API.PLUS。真正决定可用性的是中间那段的时间预算和失败处理。

参数取值(我线上用的这一套)

  • 连接超时 1 秒:TCP 加 TLS 握手,超过 1 秒说明网络路径有问题,继续等没有意义。
  • 总超时 3 秒:用户在小程序里点一下,3 秒没反馈就会怀疑卡死。
  • 重试 2 次:指数退避(Exponential Backoff)100ms、300ms,叠加 0~50ms 抖动,避免同一批并发请求同时重试形成二次尖峰。
  • 熔断(Circuit Breaker):连续 20 次失败后打开 30 秒,期间直接返回降级结果。
  • 缓存:IP 归属地类结果 TTL 600 秒;随机类内容(比如壁纸)TTL 60 秒或不缓存。

可执行命令:上线前先用 curl 摸清上游延迟分布

# 运行环境:macOS / Linux 终端,curl 8.x
# 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
curl -sS -X GET \
  --connect-timeout 1 --max-time 3 \
  -H 'Accept: application/json' \
  'https://www.api.plus/api/get?apikey=YOUR_KEY&ip=8.8.8.8' \
  -w '\nHTTP=%{http_code} dns=%{time_namelookup}s connect=%{time_connect}s total=%{time_total}s\n'

参数拆解:--connect-timeout 1 只约束握手阶段,DNS 解析慢不会被它拦住;--max-time 3 是整条请求的总预算,DNS、连接、等待、下载都算在内;-sS 表示静默但保留错误输出;-w 用变量把分阶段耗时打出来,一眼能看出慢在 DNS 还是慢在上游。YOUR_KEY 换成你在服务端配置里读到的密钥,命令行里带密钥只适合本地验证,别写进 shell 历史。

返回片段(结构示意,字段名以文档为准)

{"code":200,"msg":"ok","data":{"ip":"8.8.8.8","location":"..."}}
HTTP=200 dns=0.012s connect=0.043s total=0.187s

这里的关键信息是 total=0.187s:上游在同一区域内通常 200ms 内返回。如果你的测得值是 1.2s 以上,先排查 DNS 和出口线路,别急着加超时阈值。接口字段说明见 GET 示例接口。

可执行脚本:PHP 8.2 中转函数

<?php
// 运行环境:PHP 8.2 + ext-curl
// 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
declare(strict_types=1);

function upstream_get(string $url, int $retries = 2): array
{
    $lastError = 'unknown';

    for ($i = 0; $i <= $retries; $i++) {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER     => true,
            CURLOPT_CONNECTTIMEOUT_MS  => 1000,   // 握手 1s
            CURLOPT_TIMEOUT_MS         => 3000,   // 总预算 3s
            CURLOPT_NOSIGNAL           => true,   // 毫秒级超时必需,不加会被信号打断
            CURLOPT_FOLLOWLOCATION     => false,  // 不跟随跳转,避免被带到未知主机
            CURLOPT_HTTPHEADER         => ['Accept: application/json'],
            CURLOPT_USERAGENT          => 'apiplus-relay/1.0',
        ]);

        $body  = curl_exec($ch);
        $code  = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        $errno = curl_errno($ch);
        $err   = curl_error($ch);
        curl_close($ch);

        if ($errno !== 0) {
            $lastError = "curl({$errno}): {$err}";
        } elseif ($code === 429 || $code >= 500) {
            $lastError = "http {$code}";          // 限流与服务端错误,可重试
        } elseif ($code !== 200) {
            throw new RuntimeException("upstream fatal http {$code}"); // 4xx 不重试
        } else {
            return json_decode((string) $body, true, 8, JSON_THROW_ON_ERROR);
        }

        // 100ms、300ms 退避 + 0~50ms 抖动
        usleep((100 * (3 ** $i) + random_int(0, 50)) * 1000);
    }

    throw new RuntimeException("upstream failed: {$lastError}");
}

三个细节值得单独说。CURLOPT_NOSIGNAL 是 PHP 的老坑,不加它,TIMEOUT_MS 在部分场景下会退化成秒级精度;CURLOPT_FOLLOWLOCATION => false 是防御性的,跳转目标不可控,中转层不该帮上游做这件事;4xx 直接抛异常而不是重试,因为参数错误重试三次只会把错误放大三倍,还会白白消耗配额。

可执行脚本:Python 3.12 侧的等价实现

# 运行环境:Python 3.12 + requests 2.32 + urllib3 2.x
# 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

session = requests.Session()
retry = Retry(
    total=2,
    connect=1,
    read=2,
    backoff_factor=0.3,                       # 0.3s、0.6s 递增退避
    status_forcelist=(429, 500, 502, 503, 504),
    allowed_methods=frozenset(["GET"]),       # 只重试幂等的 GET
    raise_on_status=False,
)
session.mount("https://", HTTPAdapter(max_retries=retry, pool_maxsize=32))
session.headers.update({"Accept": "application/json"})

try:
    r = session.get(
        "https://www.api.plus/api/dongman",
        params={"type": "json"},
        timeout=(1.0, 2.0),                   # (连接超时, 读取超时) 秒
    )
    r.raise_for_status()
    payload = r.json()
except requests.Timeout:
    payload = {"code": 504, "msg": "upstream timeout", "data": None}   # 降级占位
except requests.RequestException as exc:
    payload = {"code": 502, "msg": f"upstream error: {type(exc).__name__}", "data": None}

timeout=(1.0, 2.0) 是元组,不是单个数字,写成单个数字只约束连接阶段,读取阶段会一直挂着。allowed_methods 必须显式限定为 GET,否则 POST 在连接中断时可能被重试,产生重复写入。pool_maxsize=32 和 Nginx 的 worker_connections 要对得上,不然高并发下连接池成了新瓶颈。这个接口的用途是拿一张随机图,返回体里有图片地址,缓存一秒的收益都不大,所以我用 60 秒 TTL 兜底;接口说明见 随机动漫壁纸接口。

密钥和限流怎么配合才挡得住刷量?#

密钥管理只做一件事:分级。前端一个密钥都不放,这是底线。

规范模板:密钥分级

  1. 服务端主密钥:只存在配置中心或环境变量,用于调用上游。
  2. 业务线子密钥:按业务拆成多把,方便单独吊销与配额核算。
  3. 前端凭据:只放短期令牌(Token),由后端在用户登录后签发,有效期 30 分钟,不携带上游 apikey 的任何片段。

令牌要做防重放。签名里带时间戳和随机数(Nonce),时间窗 ±300 秒,Nonce 写进 Redis 7.2 并设 300 秒过期,重复即拒。这套组合能挡住重放攻击,成本只是每次请求多一次 Redis 读写。

限流分两层,缺一层都不完整。第一层在 Nginx,按客户端 IP 做令牌桶(Token Bucket);第二层在应用侧,按令牌身份做配额。

可执行配置:Nginx 1.24 限流片段

# 运行环境:Nginx 1.24,http 块
limit_req_zone  $binary_remote_addr zone=relay:10m  rate=20r/s;
limit_conn_zone $binary_remote_addr zone=relayconn:10m;

# server / location 块
limit_req       zone=relay burst=40 nodelay;
limit_conn      relayconn 10;
limit_req_status 429;
limit_conn_status 429;

rate=20r/s 对应一个用户连点场景:中转层总超时 3 秒,用户 3 秒内最多点五六次,20r/s 已经给了很大余量。burst=40 nodelay 允许 40 个突发请求立即通过,超出直接 429。limit_conn 10 是并发连接上限,这一条比 QPS 更关键——慢连接攻击的特征就是 QPS 不高但连接数居高不下。Cloudflare 的季度报告里提到,2026 年第三季度其网络平均每小时处置 8,226 次网络层攻击,持续不足 10 分钟、带宽低于 1Gbps 的短促攻击环比增长 208%(Cloudflare DDoS 威胁报告 2026 Q3)。这类攻击打不动带宽,但足以把你的上游配额刷干净。所以阈值要按连接数和身份维度设,而不是只看流量。

上游返回 429 时要区分来源:是你自己超额,还是出口 IP 被共享调用方拖累。前者扩容,后者换独立出口或联系服务方。日志里至少记三段信息:上游状态码、本次耗时、当前并发数。

监控与告警清单#

监控表:指标、阈值与动作

指标采集方式建议阈值告警动作
上游 5xx 率中转层日志按分钟聚合> 2% 持续 5 分钟企业微信值班群,附最近 10 条错误样本
P95 总耗时curl 耗时或 OpenTelemetry> 1500ms 持续 10 分钟检查 DNS 与出口线路
超时率超时计数 / 总请求> 3%自动触发熔断,返回缓存结果
429 次数中转层按身份统计> 50 次 / 5 分钟定位到具体 token,临时封禁
缓存命中率(Cache Hit Ratio)Redis INFO stats< 70%检查 TTL 与 key 设计
密钥剩余配额定时任务探活< 20%提前通知扩容

阈值不是拍脑袋定的。P95 的 1500ms 来自上游实测 200ms 加两倍余量;超时率 3% 是经验值,超过这个数说明不是偶发抖动,而是链路真的有环节坏了。

风险清单#

风险表:影响与规避

风险影响规避方式
密钥写进前端包被解包提取,额度被刷空密钥只放服务端,前端用短期令牌
未设总超时PHP-FPM 进程被挂住,worker 耗尽连接 1s、总 3s,加熔断
对 4xx 做重试错误放大三倍,配额三倍消耗只对网络错误、5xx、429 重试
无缓存重复请求全部穿透,配额翻倍Redis 按业务设 60~600 秒 TTL
上游不可用无降级前端白屏或一直转圈返回上次缓存或明确的错误码
PHP 运行时未打补丁已知漏洞被利用,机器被接管跟随安全公告升级,参考 Krebs 关于 PHP 漏洞的报道

说到上游不可用,很多人把它当小概率事件。Cloudflare Radar 在 2026 年第三季度记录到 49 起互联网中断事件,其中电力故障 14 起、政府主导的关停 19 起(Cloudflare Q3 2026 断网汇总)。上游依赖一定有失效的时刻,降级路径不是可选项。中转层至少要保证一件事:即使上游全挂,前端拿到的也是一个结构合法的错误对象,而不是超时无响应。

总结#

小程序和 H5 调用第三方 API,工程上的分水岭只有一个:请求是不是从你自己的服务端发出去的。是,域名白名单、CORS、密钥、限流、缓存、监控全都有解;不是,前面几项全部无解,只剩祈祷。中转层本身不复杂,一百多行代码足够,真正花时间的是那几个阈值——1 秒连接、3 秒总超时、2 次重试、20r/s 加 10 并发。这些数不是标准答案,但都可以被观测数据推翻再调整。工程实践里更常被忽略的是错误码约定与文档沉淀,这块可以顺着 API 文章中心 里已有的调试类文章一起看。

适用范围

  • 面向 C 端的小程序、公众号 H5、移动端 WebView 页面,需要调用收费或限额的第三方接口。
  • 上游接口域名不归你所有,或者对方不提供 CORS 头。
  • 需要按用户维度做配额、审计或计费的系统。

不适用范围

  • 纯内网或本地脚本调用,没有前端,也没有跨域与域名白名单问题,直连更简单。
  • 上游接口本身由你提供,且已经内置了鉴权、限流与缓存,再加一层中转只是增加一跳延迟。
  • 对首包延迟极端敏感的场景(比如强交互的游戏内实时请求),多一次网络往返可能就是不可接受的成本,此时应优先考虑直连加域名报备,而不是中转。

本站实测数据(近 24 小时)#

以下数据来自 API.PLUS 线上调用日志(统计时间 2026-10-30 15:12,口径:仅统计已上线接口,不含扫描与无效路径),可作为容量规划与稳定性判断的现实参考。

常见问题(FAQ)

微信小程序能直接请求第三方 HTTPS 接口吗?
开发工具里勾选“不校验合法域名”可以跑通,真机不行。线上必须在 mp 后台把域名加入 request 合法域名,要求 HTTPS、已备案、TLS 1.2 以上,不支持 IP 和自定义端口。
中转层应该设多长的超时和几次重试?
连接超时 1 秒、总超时 3 秒、重试 2 次,退避 100ms 与 300ms 并加 0~50ms 抖动。只对网络错误、5xx、429 重试,4xx 直接返回,避免把参数错误放大成三倍流量。

作者:沐小悠

本文约 21 分钟读完 · 关键词:微信小程序调用第三方API、request合法域名、CORS预检、API密钥安全、后端中转限流

更多文章 GET 示例接口

「」标签下的其他文章