Java后端对接美团外卖霸王餐回调:基于HTTPS双向认证与证书轮转
一、场景痛点:双向认证+证书90天过期
美团霸王餐回调接口 https://open-api.meituan.com/bawangcan/callback 强制 HTTPS双向认证。平台方提供:
server.crt有效期90天- 我方需上传
client.crt+client.key - 过期未更新直接报
SSLHandshakeException
要求:零停机 证书自动轮转,且兼容老证书在途请求。
二、总体架构:SpringBoot + Undertow + 动态SSLContext
┌--------------┐ ┌--------------┐
│ 美团网关 │-----▶│ 负载均衡 │-----▶│ 应用集群 │
└--------------┘ └--------------┘ └--------------┘
双向TLS 四层转发 动态SSLContext
- 证书统一由 ACM 拉取,本地加密缓存
- 每12h检测一次,新证书落地即 热替换
- 老证书持有
CloseableHttpClient用完即弃,无连接撕裂
三、证书仓库:本地JKS + KMS解密
// juwatech.cn.ssl.CertHolder
@Data
public class CertHolder {
private X509Certificate clientCert;
private PrivateKey privateKey;
private X509Certificate[] serverCa;
private Instant expireTime;
}
KMS解密后写内存,不落磁盘:
byte[] encryptedP12 = Files.readAllBytes(Paths.get("/secrets/client.p12.enc"));
byte[] plainP12 = KMSUtil.decrypt(encryptedP12);
KeyStore store = KeyStore.getInstance("PKCS12");
store.load(new ByteArrayInputStream(plainP12), pwd);

四、动态SSLContext工厂
package juwatech.cn.ssl;
public final class DynamicSslContextFactory {
private static final Map<String, SSLContext> CACHE = new ConcurrentHashMap<>();
public static SSLContext get(String version) {
return CACHE.computeIfAbsent(version, v -> build(v));
}
private static SSLContext build(String v) {
CertHolder holder = CertLoader.load(v);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(KeyStoreUtil.createKeyStore(holder), pwd);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(KeyStoreUtil.createTrustStore(holder));
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
return ctx;
}
public static void reload(String version) {
CACHE.put(version, build(version));
}
}
五、Undertow热替换Connector
SpringBoot 默认 Tomcat 不支持运行时替换 SSLHostConfig,改用 Undertow:
@Bean
public UndertowServletWebServerFactory undertowFactory() {
UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();
factory.addBuilderCustomizers(builder -> {
SSLContext newCtx = DynamicSslContextFactory.get("v2");
builder.addHttpsListener(8443, "0.0.0.0", newCtx);
});
return factory;
}
证书更新后,向容器发 SIGUSR1 触发重新绑定:
kill -USR1 $PID
六、HttpClient双向认证调用
// juwatech.cn.open.HttpClientFactory
public static CloseableHttpClient createTwoWayClient(String certVersion) {
SSLContext ctx = DynamicSslContextFactory.get(certVersion);
SSLConnectionSocketFactory ssl = new SSLConnectionSocketFactory(
ctx, NoopHostnameVerifier.INSTANCE);
Registry<ConnectionSocketFactory> reg = RegistryBuilder.<ConnectionSocketFactory>create()
.register("https", ssl)
.register("http", PlainConnectionSocketFactory.INSTANCE)
.build();
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(reg);
return HttpClients.custom().setConnectionManager(cm).build();
}
七、证书轮转定时任务
@Component
public class CertRotateJob {
@Scheduled(fixedDelay = 12 * 3600 * 1000)
public void rotate() {
String newVer = ACMClient.getLatestVersion();
if (newVer.equals(CurrentVersion.get())) return;
// 1. 下载并解密
CertLoader.download(newVer);
// 2. 预热SSLContext
DynamicSslContextFactory.reload(newVer);
// 3. 切换标识
CurrentVersion.set(newVer);
// 4. 老客户端优雅关闭
HttpClientFactory.closeIdle(newVer);
}
}
八、回调接口样例
@RestController
@RequestMapping("/meituan")
@Slf4j
public class CallbackController {
@PostMapping("/callback")
public String callback(HttpServletRequest req, @RequestBody String body) {
// 验签
boolean ok = SignUtil.verify(req, body);
if (!ok) return "sign_error";
// 业务处理
霸王餐Service.handle(body);
return "ok";
}
}
九、灰度验证
- 利用
X-Client-Cert-Versionheader区分 - 老证书请求继续由原容器处理,新证书流量逐步切到
v2 - 观察 24h 无异常后下线老证书
十、完整配置速贴
server:
port: 8443
ssl:
enabled: false # 由Undertow接管
undertow:
https:
key-alias: client
key-store-type: PKCS12
key-store-provider: SUN
本文著作权归吃喝不愁app开发者团队,转载请注明出处!

被折叠的 条评论
为什么被折叠?



