第三方平台接口的签名、缓存与降级

2262 字
11 分钟
第三方平台接口的签名、缓存与降级

接入第三方接口时,最容易完成的是“请求成功一次”,最难的是让它长期稳定地处在业务链路中。微信和大疆接口形式不同,但都需要处理密钥、超时、错误响应、字段变化和临时不可用。

1. 密钥只停留在服务端#

AppSecret、SecretKey 等凭据放在服务端配置,通过 @Value 或配置属性对象注入。前端只调用自己的业务接口,不直接获得第三方密钥,也不负责签名。

配置预览接口只能返回“是否已配置”和请求头名称,不能把真实 AccessKey 或 SecretKey返回给页面。

2. 建立统一 HTTP 客户端#

第三方每个业务方法都自己创建请求,会重复出现超时、请求头、序列化和异常判断。通用客户端应统一负责:

  • 基础地址与路径拼接;
  • 查询参数、表单和 JSON 编码;
  • 连接超时与请求超时;
  • 鉴权请求头或 Token;
  • 非 2xx 响应转换;
  • 空响应与 JSON 解析;
  • 不泄露密钥的日志。

平台 Service 再在客户端之上表达 getWayline、createTask 等业务能力。

3. 请求签名的动态字段#

大疆客户端每次请求生成时间戳与随机 nonce,再计算签名。动态字段可以降低请求被直接重放的风险:

method + timestamp + nonce
-> HMAC 签名
-> AccessKey、Timestamp、Nonce、Signature 请求头

签名原文的字段顺序、大小写和编码必须严格按平台协议,路径参数也要单独进行 URL 编码。

4. Token 缓存提前失效#

微信手机号接口依赖全局 access_token。每次请求都重新获取会增加延迟并浪费调用额度,因此可以在服务进程内缓存,并在官方过期时间前留出安全窗口。

cacheExpire = now + (expiresIn - 300) * 1000L;

本地缓存适合当前单体部署;多实例部署时,各实例独立刷新可能增加请求量,可进一步迁移到 Redis 并加互斥刷新。

5. 重试必须有限且有依据#

并不是所有错误都值得重试:

错误策略
Token 明确失效清缓存后重试一次
参数或模板错误直接失败并记录原因
网络瞬时异常对幂等查询有限重试
创建任务等非幂等操作先确认平台是否已受理,不能盲目重发

无限重试会延长接口占用,并可能在外部服务恢复时制造流量洪峰。

6. 在边界处归一化字段#

第三方响应常出现字段命名或嵌套层级差异。适配层可以使用候选字段读取并转成项目自己的模型:

longitude / lng / lon -> longitude
latitude / lat -> latitude
list / rows / records -> items

归一化应该集中在第三方边界,不要把兼容判断散到 Controller 和前端页面。

7. 按业务重要性设计降级#

订单轨迹页获取完整航线失败时,可以退回到接驳点和船舶位置,订单详情仍然可用;但创建飞行任务失败时,不能伪装成功。

可以用一个问题判断是否降级:

缺少这份第三方数据,核心业务事实是否仍然成立?

若只是展示增强,可以回退并标注数据来源;若会改变资金、订单或真实设备状态,就应明确失败并进入补偿流程。

8. 通用客户端怎样隐藏协议细节#

业务层希望调用的是:

djiOpenApiService.getWayline(waylineId, params);
djiOpenApiService.startTask(taskId, params);
wechatService.sendOrderArrivedMessage(order);

它不应该重复编写时间戳、请求头、JSON 序列化和 HTTP 状态判断。大疆客户端把这些逻辑集中到 request:

String method = rawMethod.toUpperCase(Locale.ROOT);
HttpRequest request = buildRequest(
method, path, body, contentType);
HttpResponse<String> response = httpClient.send(
request,
HttpResponse.BodyHandlers.ofString(
StandardCharsets.UTF_8));
if (response.statusCode() < 200
|| response.statusCode() >= 300) {
throw new ServiceException("第三方服务响应异常");
}
return StringUtils.isEmpty(response.body())
? Collections.emptyMap()
: objectMapper.readValue(response.body(), Object.class);

业务 Service 因而只关心平台能力和业务对象,协议变化集中修改客户端。测试时也可以替换客户端,模拟成功、超时和错误响应。

9. HMAC 签名具体在保护什么#

String timestamp = String.valueOf(
System.currentTimeMillis());
String nonce = UUID.randomUUID()
.toString().replace("-", "");
String signText = accessKey + method + timestamp + nonce;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(
secretKey.getBytes(StandardCharsets.UTF_8),
"HmacSHA256"));
String signature = toHex(mac.doFinal(
signText.getBytes(StandardCharsets.UTF_8)));

AccessKey 用于标识调用方,SecretKey 参与 HMAC 但不随请求发送。时间戳限制签名有效窗口,nonce 让同一毫秒内的两次请求也有不同签名。

签名能证明请求由持有 SecretKey 的一方生成,但是否真正防重放还取决于平台是否校验时间窗口并保存近期 nonce。客户端不能只生成字段就假设服务端一定执行了这些规则。

10. access_token 缓存的并发细节#

private synchronized String getAccessToken() {
long now = System.currentTimeMillis();
if (StringUtils.isNotEmpty(cachedAccessToken)
&& cachedAccessTokenExpireTime > now) {
return cachedAccessToken;
}
JsonNode tokenResult = requestAccessToken();
int expiresIn = tokenResult.path("expires_in")
.asInt(7200);
cachedAccessToken = tokenResult.path("access_token")
.asText();
cachedAccessTokenExpireTime = now
+ Math.max(60, expiresIn - 300) * 1000L;
return cachedAccessToken;
}

volatile 保证其他线程能看到更新后的 Token 与时间,synchronized 保证只有一个线程进入刷新过程。由于同步范围只在缓存失效时执行,大部分命中请求不会访问第三方。

多实例部署后每个实例仍会各自刷新。可以把 Token 放入 Redis,并使用短期分布式锁控制刷新;读取缓存时还要处理锁持有者崩溃和 Token 即将过期的情况。

11. 重试前先判断请求是否幂等#

查询航线失败后重试通常不会产生新业务数据;创建任务失败后直接重试则可能创建两份任务。可以先为方法分类:

boolean retryable = switch (operation) {
case LIST_DEVICE, GET_WAYLINE, QUERY_TASK -> true;
case CREATE_TASK, START_TASK, SEND_MESSAGE -> false;
};

真正实现时还要结合错误类型:连接尚未建立通常可以重试,读取响应时断开则无法确定第三方是否已处理。非幂等动作需要业务请求号或状态查询来确认。

退避时间可以采用指数增长并加入随机抖动:

第 1 次失败:约 200ms 后重试
第 2 次失败:约 500ms 后重试
第 3 次失败:约 1000ms 后停止

随机抖动避免大量请求同时恢复后再次形成流量尖峰。

12. 字段归一化怎样限制变化范围#

private Object pickFirst(
Map<String, Object> source,
String... keys) {
for (String key : keys) {
Object value = source.get(key);
if (value != null
&& !String.valueOf(value).trim().isEmpty()) {
return value;
}
}
return null;
}

调用时把 longitude、lng 和 lon 归一为项目字段 longitude。如果平台把列表从 list 改为 records,只需扩展适配层候选项。

不过兼容不能无限增加。字段语义改变时应建立新版本适配并记录告警,否则“能取到值”不代表值仍代表同一含义。

13. 降级结果也要说明数据来源#

订单轨迹从大疆获取成功时返回:

{
"routeCoordinateSystem": "WGS84",
"waylinePoints": [
{ "longitude": 120.1, "latitude": 30.2, "height": 80 }
]
}

降级到订单起终点时返回 GCJ02 和两个基础点。坐标系字段告诉前端两种结果不能直接按相同方式绘制。

还可以增加 routeSource: DJI | ORDER_FALLBACK 与 routeDegraded: true,让页面决定是否显示“当前展示预计路线”,也方便日志和监控统计降级比例。

14. 熔断和限流适合解决什么#

当第三方持续超时时,继续让每个订单详情等待 8 秒会耗尽 Web 线程。熔断器会在连续失败达到阈值后暂时快速失败,等待恢复窗口再放少量探测请求。

限流则控制单位时间内主动发出的请求量,避免超过平台配额。两者解决的问题不同:

机制目标
超时限制单个请求等待时间
重试恢复短暂且可安全重试的失败
熔断第三方持续故障时保护自身资源
限流控制调用速率与平台配额
降级在缺少增强数据时维持核心功能

这些机制应按接口单独配置,不能给查询航线和启动真实任务使用完全相同的策略。

15. 可观测性至少记录什么#

第三方调用日志建议包含:

provider=dji
operation=getWayline
requestId=项目侧追踪号
remoteRequestId=第三方响应追踪号
durationMs=183
status=success/failure/degraded
errorCode=平台错误码

不要记录 SecretKey、完整 Token、签名和用户 openid。业务日志与敏感信息脱敏并不冲突:只要有稳定请求号、操作名、耗时和错误码,已经可以完成大部分定位。

16. 小结#

稳定的第三方接入需要把凭据、协议和字段差异封装在边界内,再配合缓存、有限重试和分级降级。目标不是隐藏所有错误,而是让错误以符合业务重要性的方式发生。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
第三方平台接口的签名、缓存与降级
https://firefly.cuteleaf.cn/posts/third-party-api-sign-cache-fallback/
作者
Yore
发布于
2026-07-31
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Yore
Hello, I'm Yore.
公告
欢迎来到Yore的博客!本站基于原创作者夏叶的博客网站模版创作。
分类
标签
最新动态
站点统计
文章
51
分类
2
标签
124
总字数
94,115
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
EdgeOne Pages
博客版本
Firefly v6.15.0
文章许可
CC BY-NC-SA 4.0