feat(pay): 新增收货地址DTO和签名服务实现
- 新增ShippingDTO用于收货地址信息传输 - 新增SignatureService接口定义签名生成与验证方法 - 实现SignatureServiceImpl支持PingPong支付签名逻辑 - 支持MD5和SHA256两种签名算法 - 添加签名参数过滤和排序功能 - 集成PingPong配置属性进行签名处理
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<String, Object> params);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成签名
|
||||||
|
*
|
||||||
|
* @param params 待签名参数Map
|
||||||
|
* @param secret 签名密钥
|
||||||
|
* @param signType 签名类型(MD5或SHA256)
|
||||||
|
* @return 签名(大写)
|
||||||
|
*/
|
||||||
|
String generateSign(Map<String, Object> params, String secret, String signType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证签名
|
||||||
|
*
|
||||||
|
* @param params 包含sign和signType的参数Map
|
||||||
|
* @return 验证结果
|
||||||
|
*/
|
||||||
|
boolean verifySign(Map<String, Object> params);
|
||||||
|
}
|
||||||
@@ -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<String> SIGN_SCOPE = Set.of(
|
||||||
|
"clientId",
|
||||||
|
"accId",
|
||||||
|
"amount",
|
||||||
|
"currency",
|
||||||
|
"cardNum",
|
||||||
|
"transactionId",
|
||||||
|
"merchantTransactionId",
|
||||||
|
"requestId",
|
||||||
|
"signType",
|
||||||
|
"notificationUrl",
|
||||||
|
"shopperResultUrl"
|
||||||
|
);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateSign(Map<String, Object> params) {
|
||||||
|
return generateSign(params, pingPongProperties.getSecret(), pingPongProperties.getSignType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateSign(Map<String, Object> params, String secret, String signType) {
|
||||||
|
// 1. 筛选出需要参与签名的参数
|
||||||
|
Map<String, Object> signParams = filterSignParams(params);
|
||||||
|
|
||||||
|
// 2. 按照字典序排序
|
||||||
|
Map<String, Object> sortedParams = new TreeMap<>(signParams);
|
||||||
|
|
||||||
|
// 3. 构建签名串:{secret}key1=val1&key2=val2...
|
||||||
|
StringBuilder signStr = new StringBuilder(secret);
|
||||||
|
for (Map.Entry<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> filterSignParams(Map<String, Object> params) {
|
||||||
|
Map<String, Object> signParams = new HashMap<>();
|
||||||
|
|
||||||
|
for (Map.Entry<String, Object> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user