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

接入第三方接口时,最容易完成的是“请求成功一次”,最难的是让它长期稳定地处在业务链路中。微信和大疆接口形式不同,但都需要处理密钥、超时、错误响应、字段变化和临时不可用。
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 -> longitudelatitude / lat -> latitudelist / 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=djioperation=getWaylinerequestId=项目侧追踪号remoteRequestId=第三方响应追踪号durationMs=183status=success/failure/degradederrorCode=平台错误码不要记录 SecretKey、完整 Token、签名和用户 openid。业务日志与敏感信息脱敏并不冲突:只要有稳定请求号、操作名、耗时和错误码,已经可以完成大部分定位。
16. 小结
稳定的第三方接入需要把凭据、协议和字段差异封装在边界内,再配合缓存、有限重试和分级降级。目标不是隐藏所有错误,而是让错误以符合业务重要性的方式发生。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!





























