配置与签名
1425字约5分钟
1. 获取商户自助平台账号
合作确认后,HaiPay 将根据《商户接入申请表》中所填写的“管理员账号信息”,创建可供商户登录商户管理自助平台的管理员账号。 届时,请注意查收 HaiPay 下发的激活邮件,接入方可根据邮件指引激活管理员账号。 首次登录需要设置密码,请确保密码安全,以防外泄。
2. 获取密钥appId和密钥
联调接入所需的商户appId,可通过商户管理平台获取。 签名使用的是SHA256WithRSA签名算法,需要商户自行生成公私钥信息,并将公钥通过商户管理平台上传,同时下载HaiPay的公钥。请妥善保管密钥信息,若不慎泄漏密钥,请及时更新密钥。
配置入口:「我的」-「业务列表」
注意
商户appId和密钥是配套的,需要区分测试环境与正式环境。
3. 公钥和私钥配置
3.1 公钥和私钥的作用
3.2 生成公钥私钥
商户公私钥可通过以下SDK生成,生成的商户私钥请商户自己妥善保管,用于请求haipay报文的加签操作,商户公钥通过平台上传给haipay用于验证商户签名防止报文在网络传输过程中被篡改,同时在平台上获取haipay公钥放到自己程序中,用于验证haipay加签报文的签名。
- 签名工具(Java)
/**
* 初始化RSA算法密钥对
*
* @param keysize RSA1024已经不安全了,建议2048
* @return 经过Base64编码后的公私钥Map, 键名分别为publicKey和privateKey
*/
public static final Charset CHARSET = StandardCharsets.UTF_8;
/**
* 密钥算法
*/
public static final String ALGORITHM_RSA = "RSA";
/**
* RSA 签名算法
*/
public static final String ALGORITHM_RSA_SIGN = "SHA256WithRSA";
public static final int ALGORITHM_RSA_PRIVATE_KEY_LENGTH = 2048;
public static Map<String, String> initRSAKey(int keysize) {
if (keysize != ALGORITHM_RSA_PRIVATE_KEY_LENGTH) {
throw new IllegalArgumentException("RSA1024已经不安全了,请使用" + ALGORITHM_RSA_PRIVATE_KEY_LENGTH + "初始化RSA密钥对");
}
//为RSA算法创建一个KeyPairGenerator对象
KeyPairGenerator kpg;
try {
kpg = KeyPairGenerator.getInstance(ALGORITHM_RSA);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("No such algorithm-->[" + ALGORITHM_RSA + "]");
}
//初始化KeyPairGenerator对象,不要被initialize()源码表面上欺骗,其实这里声明的size是生效的
kpg.initialize(ALGORITHM_RSA_PRIVATE_KEY_LENGTH);
//生成密匙对
KeyPair keyPair = kpg.generateKeyPair();
//得到公钥
Key publicKey = keyPair.getPublic();
String publicKeyStr = Base64.getEncoder().encodeToString(publicKey.getEncoded());
//得到私钥
Key privateKey = keyPair.getPrivate();
String privateKeyStr = Base64.getEncoder().encodeToString(privateKey.getEncoded());
Map<String, String> keyPairMap = new HashMap<>();
keyPairMap.put("publicKey", publicKeyStr);
keyPairMap.put("privateKey", privateKeyStr);
return keyPairMap;
}
4. 签名
类型 | 说明 |
---|---|
算法 | RSA |
签名算法 | SHA256WithRSA |
密钥长度 | 2048 |
- 签名工具(Java)
package com.example.eventdemo;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* SHA256WithRSA 非对称加密算法
* Base64编码使用 apache工具类, maven依赖 https://mvnrepository.com/artifact/commons-codec/commons-codec
*/
public class SHA256WithRSAUtils {
/**
* 密钥算法
*/
public static final String ALGORITHM_RSA = "RSA";
private SHA256WithRSAUtils() {
}
/**
* 得到私钥
*
* @param privateKey 密钥字符串(经过base64编码)
*/
public static RSAPrivateKey getPrivateKey(String privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
//通过PKCS#8编码的Key指令获得私钥对象
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_RSA);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
return (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
}
/**
* 得到公钥
*
* @param publicKey 密钥字符串(经过base64编码)
*/
public static RSAPublicKey getPublicKey(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
//通过X509编码的Key指令获得公钥对象
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_RSA);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
return (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
}
/**
* RSA算法公钥解密数据
*
* @param data 待解密的经过Base64编码的密文字符串
* @param key RSA公钥字符串
* @return RSA公钥解密后的明文字符串
*/
public static String decryptByPublicKey(String data, String key) {
try {
RSAPublicKey publicKey = getPublicKey(key);
Cipher cipher = Cipher.getInstance(ALGORITHM_RSA);
cipher.init(Cipher.DECRYPT_MODE, publicKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data), publicKey.getModulus().bitLength()), StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("decrypt [" + data + "] error", e);
}
}
/**
* RSA算法私钥加密数据
*
* @param data 待加密的明文字符串
* @param key RSA私钥字符串
* @return RSA私钥加密后的经过Base64编码的密文字符串
*/
public static String encryptByPrivateKey(String data, String key) {
try {
//通过PKCS#8编码的Key指令获得私钥对象
RSAPrivateKey privateKey = getPrivateKey(key);
Cipher cipher = Cipher.getInstance(ALGORITHM_RSA);
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return Base64.encodeBase64String(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(StandardCharsets.UTF_8), privateKey.getModulus().bitLength()));
} catch (Exception e) {
throw new RuntimeException("encrypt [" + data + "] error", e);
}
}
/**
* RSA算法分段加解密数据
*
* @param cipher 初始化了加解密工作模式后的javax.crypto.Cipher对象
* @param opmode 加解密模式,值为javax.crypto.Cipher.ENCRYPT_MODE/DECRYPT_MODE
* @return 加密或解密后得到的数据的字节数组
*/
private static byte[] rsaSplitCodec(Cipher cipher, int opmode, byte[] datas, int keySize) throws IOException {
int maxBlock = 0;
if (opmode == Cipher.DECRYPT_MODE) {
maxBlock = keySize / 8;
} else {
maxBlock = keySize / 8 - 11;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] buff;
int i = 0;
try {
while (datas.length > offSet) {
if (datas.length - offSet > maxBlock) {
buff = cipher.doFinal(datas, offSet, maxBlock);
} else {
buff = cipher.doFinal(datas, offSet, datas.length - offSet);
}
out.write(buff, 0, buff.length);
i++;
offSet = i * maxBlock;
}
} catch (Exception e) {
throw new RuntimeException("encrypt [" + maxBlock + "] error", e);
}
byte[] resultDatas = out.toByteArray();
out.close();
return resultDatas;
}
}
- 生成代签名字符串(Java)
public static String getSign(Map<String, Object> map) {
Set<String> keys = map.keySet();
List<String> list = new ArrayList<>(keys);
Collections.sort(list);
// 构造签名键值对的格式
StringBuilder sb = new StringBuilder();
// 构造签名键值对的格式
for (String key : list) {
Object val = map.get(key);
if (!("".equals(val) || val == null || "sign".equals(key))) {
sb.append(val);
}
}
return sb.toString();
}
- 签名验签测试Demo(Java)
/**
* 测试类
*
* @param args
*/
public static void main(String[] args) {
String privateKey = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBANkyOMTXOBCWBy3Ps39b/rymWWFqujOPxpdp1IAVlk8PuTxWBBscFIHLVAXdf9sCqAw+Wm89RSGDzky6TVIgRXSo6+6hk3ZSIMnu53f9cZlL2G/3+b4BRP3lKsP7nVTTwAEOH1uiJFlJbcCaRuRpGyQJ0mNg/HbCTIWxdKyelBXPAgMBAAECgYAB/eTpYTPhaw7Ly8DQpS5T2o6tRwZIHMRsdQr+1bPYK8O+GufUu9AwVIYDu8FFZ+PUoOnBZWVx5jyJFZhJ7YPVhrZSE4XqMqrtJqIER7m0VDejDSKcKIm6pnmdZHlIvQqkhoCdaCI9kFYgmS0x82SrlMqBpsA9qkSYrRJMZdDv2QJBAPiqjtR5LmfHtrgWE8JezcQqq6IQGQXzGTY73PpuBdIuRAPIvE3b8zuccyUPdozwNzxWEY429aw/XnkZQImZCHsCQQDfmhA2vmSh4eJqtpfhoQqa3IH7wQ20O6nQeIr10lwScfnj8UiKh5GuiOrus8EY0KIQUtA5DAks8RBcJfpNfYm9AkEA9n37P1swSOeLlEcuJwpa5g12PRu/8knbwArvLb9KPeJmwWmGX5ecMIcRDLebSHIGDuUyWcrZFHlsaJZDhyIPaQJBAIgxoNqXUVhA69Yv7YbivkDhOtMLHbu/84klQw7D2Izrm1e5qYOnW5bBksdd+amRuoTSzD1TFWuoUVyvTSxR4MkCQAgTzQAytBOYITp/Ks+0KOrsDPP7KquBDLKNLttayW+0L+ZLJyZvLCpY9Rj1T8lElUD2O16NBp8SafLz7XYEgLk=";
String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZMjjE1zgQlgctz7N/W/68pllharozj8aXadSAFZZPD7k8VgQbHBSBy1QF3X/bAqgMPlpvPUUhg85Muk1SIEV0qOvuoZN2UiDJ7ud3/XGZS9hv9/m+AUT95SrD+51U08ABDh9boiRZSW3AmkbkaRskCdJjYPx2wkyFsXSsnpQVzwIDAQAB";
Map<String, Object> param = new HashMap<>();
param.put("merOrderNo", "123456");
param.put("amount", 100);
String sign = SignUtils.getSign(param);
// sign = 100123456
System.out.println("sign = " + sign);
String encryptData = SHA256WithRSAUtils.encryptByPrivateKey(sign, privateKey);
String decryptData = SHA256WithRSAUtils.decryptByPublicKey(encryptData, publicKey);
//加密:bg2Q3dZY0Tv9ycQUMGKVOL7rR1sRZCUk5EnkYeOCpKaCgg1E/NRM9njtNEW5205/BABW6PHnLX9tmNR+C7n9Aj4edsXdCr1uOdgSgaki+4M/Kh2Z3vO+PrYJxBRZjolV+ue51RUqlxYQXcKrygj74QLCLsFRKzk4V2qcHVhs8e4=
System.out.println("加密:" + encryptData);
//解密:100123456
System.out.println("解密:" + decryptData);
//验签:true
System.out.println("验签:" + sign.equals(decryptData));
}
5. 示例报文
请求
POST https://uat-trade.xwinpay.tech/{接口地址}
X-MERCHANT-CODE: 商户编码
X-API-KEY: 商户后台获取apiKey
{
// 业务参数
"param": {
"accountNumber": "",
"amount": 50000,
"app": "NICE",
****,
****,
****,
},
// 根据请求参数param使用privateKey签名
"sign": "SQiqFRTmBn0***4Yz8iUCdeyWi/JObp9mXzXfNCQ+zbOCQ="
}
响应
HTTP/1.1 200 OK
Content-Type:application/json
{
"code": 200,
"message": "操作成功",
"data": {
"merchantNo": "58256833",
"merchantOrderNo": "5825683320230508569",
****,
****,
****
}
}