Java应用对接美团API时应对签名算法变更(如SHA256WithRSA)的可扩展设计

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长度
}

通过此设计,当美团未来升级至SM2Ed25519等算法时,仅需新增一个ApiSigner实现类并调整配置,无需修改任何业务调用代码。

本文著作权归吃喝不愁app开发者团队,转载请注明出处!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值