Java应用对接美团API时应对签名算法变更(如SHA256WithRSA)的可扩展设计
美团开放平台要求所有API请求必须携带数字签名,以确保数据完整性与来源可信。早期使用HmacSHA256,后续可能升级为SHA256withRSA等非对称算法。若将签名逻辑硬编码在业务代码中,一旦算法变更,将导致全量重构。本文基于baodanbao.com.cn.*包结构,采用策略模式 + 工厂模式实现签名算法的动态切换与平滑升级。
1. 定义签名算法接口
抽象出统一签名行为:
package baodanbao.com.cn.sign;
import java.util.Map;
public interface ApiSigner {
String sign(Map<String, String> params, String secretOrPrivateKey);
String getAlgorithmName();
}

2. 实现多种签名策略
HmacSHA256 签名(旧版)
package baodanbao.com.cn.sign.impl;
import baodanbao.com.cn.sign.ApiSigner;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.TreeMap;
public class HmacSha256Signer implements ApiSigner {
@Override
public String sign(Map<String, String> params, String secret) {
// 1. 参数按字典序排序
TreeMap<String, String> sorted = new TreeMap<>(params);
// 2. 拼接 key1=value1&key2=value2...
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sorted.entrySet()) {
if (sb.length() > 0) sb.append('&');
sb.append(entry.getKey()).append('=').append(entry.getValue());
}
// 3. HMAC-SHA256
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec spec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(spec);
byte[] hash = mac.doFinal(sb.toString().getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash).toUpperCase();
} catch (Exception e) {
throw new RuntimeException("HMAC-SHA256签名失败", e);
}
}
private String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
@Override
public String getAlgorithmName() {
return "HMAC-SHA256";
}
}
SHA256withRSA 签名(新版)
package baodanbao.com.cn.sign.impl;
import baodanbao.com.cn.sign.ApiSigner;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.Signature;
import java.util.Base64;
import java.util.Map;
import java.util.TreeMap;
public class RsaSha256Signer implements ApiSigner {
@Override
public String sign(Map<String, String> params, String privateKeyPem) {
TreeMap<String, String> sorted = new TreeMap<>(params);
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sorted.entrySet()) {
if (sb.length() > 0) sb.append('&');
sb.append(entry.getKey()).append('=').append(entry.getValue());
}
try {
// 解析PKCS#8私钥(需去除头尾)
String pkcs8 = privateKeyPem
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] keyBytes = Base64.getDecoder().decode(pkcs8);
PrivateKey privateKey = java.security.KeyFactory.getInstance("RSA")
.generatePrivate(new java.security.spec.PKCS8EncodedKeySpec(keyBytes));
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(sb.toString().getBytes(StandardCharsets.UTF_8));
byte[] signed = signature.sign();
return Base64.getEncoder().encodeToString(signed);
} catch (Exception e) {
throw new RuntimeException("RSA-SHA256签名失败", e);
}
}
@Override
public String getAlgorithmName() {
return "SHA256withRSA";
}
}
3. 签名工厂与配置驱动
通过配置决定当前生效的算法:
package baodanbao.com.cn.sign;
import baodanbao.com.cn.sign.impl.HmacSha256Signer;
import baodanbao.com.cn.sign.impl.RsaSha256Signer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
@Component
public class SignerFactory {
@Value("${meituan.sign.algorithm:RSA}")
private String currentAlgorithm;
private final Map<String, ApiSigner> signerMap = new HashMap<>();
@PostConstruct
public void init() {
signerMap.put("HMAC", new HmacSha256Signer());
signerMap.put("RSA", new RsaSha256Signer());
}
public ApiSigner getCurrentSigner() {
ApiSigner signer = signerMap.get(currentAlgorithm);
if (signer == null) {
throw new IllegalArgumentException("不支持的签名算法: " + currentAlgorithm);
}
return signer;
}
}
application.yml配置:
meituan:
sign:
algorithm: RSA # 可选 HMAC 或 RSA
4. API请求构建器集成
封装通用请求方法:
package baodanbao.com.cn.client;
import baodanbao.com.cn.sign.SignerFactory;
import okhttp3.HttpUrl;
import okhttp3.Request;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
public class MeituanRequestBuilder {
private final SignerFactory signerFactory;
private final String appId;
private final String appSecret; // HMAC用
private final String privateKey; // RSA用
public MeituanRequestBuilder(SignerFactory signerFactory,
@Value("${meituan.app.id}") String appId,
@Value("${meituan.app.secret}") String appSecret,
@Value("${meituan.private.key}") String privateKey) {
this.signerFactory = signerFactory;
this.appId = appId;
this.appSecret = appSecret;
this.privateKey = privateKey;
}
public Request buildSignedGet(String path, Map<String, String> queryParams) {
Map<String, String> allParams = new HashMap<>(queryParams);
allParams.put("app_id", appId);
allParams.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
ApiSigner signer = signerFactory.getCurrentSigner();
String secretOrKey = "RSA".equals(signer.getAlgorithmName()) ? privateKey : appSecret;
String sign = signer.sign(allParams, secretOrKey);
allParams.put("sign", sign);
HttpUrl.Builder urlBuilder = HttpUrl.get("https://openapi.meituan.com" + path).newBuilder();
allParams.forEach(urlBuilder::addQueryParameter);
return new Request.Builder().url(urlBuilder.build()).get().build();
}
}
5. 平滑迁移方案
- 灰度发布:先对部分接口启用新算法,验证无误后全局切换。
- 双签支持:在过渡期同时生成两种签名,由美团侧兼容校验。
- 密钥管理:私钥应从配置中心或KMS动态加载,避免硬编码。
6. 单元测试示例
@Test
void testRsaSigner() {
RsaSha256Signer signer = new RsaSha256Signer();
Map<String, String> params = Map.of("order_id", "123", "user_id", "456");
String sign = signer.sign(params, privateKeyPem);
assertNotNull(sign);
assertTrue(sign.length() > 100); // Base64长度
}
通过此设计,当美团未来升级至SM2或Ed25519等算法时,仅需新增一个ApiSigner实现类并调整配置,无需修改任何业务调用代码。
本文著作权归吃喝不愁app开发者团队,转载请注明出处!
568

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



