diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/dto/risk/ShippingDTO.java b/mt-pay/src/main/java/com/mtkj/mtpay/dto/risk/ShippingDTO.java new file mode 100644 index 0000000..8702039 --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/dto/risk/ShippingDTO.java @@ -0,0 +1,35 @@ +package com.mtkj.mtpay.dto.risk; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 收货地址DTO + */ +@Data +public class ShippingDTO implements Serializable { + + private String firstName; + + private String lastName; + + private String phone; + + private String email; + + private String street; + + private String postcode; + + private String city; + + private String state; + + private String country; + + private String lastModifierStreetTime; + + private String lastModifierPhoneTime; +} + diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/service/SignatureService.java b/mt-pay/src/main/java/com/mtkj/mtpay/service/SignatureService.java new file mode 100644 index 0000000..60b455d --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/service/SignatureService.java @@ -0,0 +1,36 @@ +package com.mtkj.mtpay.service; + +import java.util.Map; + +/** + * 签名服务接口 + * 负责生成和验证PingPong支付接口的签名 + */ +public interface SignatureService { + + /** + * 生成签名 + * + * @param params 待签名参数Map + * @return 签名(大写MD5) + */ + String generateSign(Map params); + + /** + * 生成签名 + * + * @param params 待签名参数Map + * @param secret 签名密钥 + * @param signType 签名类型(MD5或SHA256) + * @return 签名(大写) + */ + String generateSign(Map params, String secret, String signType); + + /** + * 验证签名 + * + * @param params 包含sign和signType的参数Map + * @return 验证结果 + */ + boolean verifySign(Map params); +} diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/service/impl/SignatureServiceImpl.java b/mt-pay/src/main/java/com/mtkj/mtpay/service/impl/SignatureServiceImpl.java new file mode 100644 index 0000000..79d144e --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/service/impl/SignatureServiceImpl.java @@ -0,0 +1,155 @@ +package com.mtkj.mtpay.service.impl; + +import com.mtkj.mtpay.config.PingPongProperties; +import com.mtkj.mtpay.service.SignatureService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.*; + +/** + * 签名服务实现类 + * 负责生成和验证PingPong支付接口的签名 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SignatureServiceImpl implements SignatureService { + + private final PingPongProperties pingPongProperties; + + /** + * 加签参数列表(根据文档定义) + */ + private static final Set SIGN_SCOPE = Set.of( + "clientId", + "accId", + "amount", + "currency", + "cardNum", + "transactionId", + "merchantTransactionId", + "requestId", + "signType", + "notificationUrl", + "shopperResultUrl" + ); + + @Override + public String generateSign(Map params) { + return generateSign(params, pingPongProperties.getSecret(), pingPongProperties.getSignType()); + } + + @Override + public String generateSign(Map params, String secret, String signType) { + // 1. 筛选出需要参与签名的参数 + Map signParams = filterSignParams(params); + + // 2. 按照字典序排序 + Map sortedParams = new TreeMap<>(signParams); + + // 3. 构建签名串:{secret}key1=val1&key2=val2... + StringBuilder signStr = new StringBuilder(secret); + for (Map.Entry entry : sortedParams.entrySet()) { + String value = entry.getValue() != null ? entry.getValue().toString().trim() : ""; + signStr.append(entry.getKey()).append("=").append(value).append("&"); + } + + // 移除最后一个& + if (signStr.length() > 0 && signStr.charAt(signStr.length() - 1) == '&') { + signStr.setLength(signStr.length() - 1); + } + + String signContent = signStr.toString(); + log.debug("签名串: {}", signContent); + + // 4. 进行MD5或SHA256运算并转大写 + String sign = hash(signContent, signType).toUpperCase(); + log.debug("生成的签名: {}", sign); + + return sign; + } + + @Override + public boolean verifySign(Map params) { + String receivedSign = (String) params.get("sign"); + if (receivedSign == null || receivedSign.isEmpty()) { + log.warn("签名参数为空"); + return false; + } + + String signType = (String) params.getOrDefault("signType", pingPongProperties.getSignType()); + + // 移除sign参数后计算签名 + Map paramsWithoutSign = new HashMap<>(params); + paramsWithoutSign.remove("sign"); + + String calculatedSign = generateSign(paramsWithoutSign, pingPongProperties.getSecret(), signType); + + boolean isValid = receivedSign.equalsIgnoreCase(calculatedSign); + if (!isValid) { + log.warn("签名验证失败,接收到的签名: {}, 计算出的签名: {}", receivedSign, calculatedSign); + } + + return isValid; + } + + /** + * 筛选出需要参与签名的参数 + * + * @param params 所有参数 + * @return 需要签名的参数 + */ + private Map filterSignParams(Map params) { + Map signParams = new HashMap<>(); + + for (Map.Entry entry : params.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + // 跳过sign参数本身 + if ("sign".equals(key)) { + continue; + } + + // 只保留在加签参数列表中的参数,且值不为空 + if (SIGN_SCOPE.contains(key) && value != null && !value.toString().trim().isEmpty()) { + signParams.put(key, value); + } + } + + return signParams; + } + + /** + * 哈希运算 + * + * @param content 待哈希内容 + * @param algorithm 算法类型(MD5或SHA256) + * @return 哈希值(小写) + */ + private String hash(String content, String algorithm) { + try { + MessageDigest digest = MessageDigest.getInstance(algorithm); + byte[] hashBytes = digest.digest(content.getBytes(StandardCharsets.UTF_8)); + + StringBuilder hexString = new StringBuilder(); + for (byte b : hashBytes) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + + return hexString.toString(); + } catch (Exception e) { + log.error("哈希运算失败", e); + throw new RuntimeException("哈希运算失败: " + e.getMessage(), e); + } + } +} +