API Signature Generation
# Signature Rules
All parameter names and values are included in the signature, with Key (parameter name) sorted
- Sort all parameter Keys (parameter names) according to
ASCIIorder. - Concatenate each parameter name and its value in the order of sorted Keys, then use the
RSAalgorithm to calculate the signature string from the concatenated string.
# Example Explanation
- Example Parameters
| Parameter Name | Parameter Value |
|---|---|
| merchantCode | S820190712000002 |
| orderNum | T1231511321515 |
| orderAmount | 999.56 |
| callback | https://xxx/yyy |
| timestamp | 1745377181 |
- Parameter Format
Formatting rules description:
- Extract all keys from the incoming key-value pair parameters (Map) and convert them into a string array sorted in lexicographical ascending order.
- Iterate through the sorted key array and concatenate each key and its value in the form of “key=value&” into a string sequentially.
- Remove the extra “&” at the end of the concatenated string to obtain the formatted parameter string.
- The above rules are encapsulated in TopPaySignUtil#paramFormat, which returns the following source when called:
source = callback=https://xxx/yyy&merchantCode=S820190712000002&orderAmount=999.56&orderNum=T1231511321515×tamp=1745377181
- Calculate Signature
- Use the key pair configured in your TopPay merchant backend.
- Use your Private Key and the formatted source string as inputs to call TopPaySignUtil#sign, obtaining the final signature string:
Sign = Jv0AMYiVSL/V8NxuBf8ZfHn7UyHO8TsU8Xkh2sqa0hbpKH1HSPampNXxzBn5PvJoytb8zPkHuQAMveTuBV5Ye8Qu+n8aw69 ...
# Code Example
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.RuntimeException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class TopPaySignUtil {
private static final String RSA = "RSA";
private static final int MAX_ENCRYPT_BLOCK = 245;
private static final int MAX_DECRYPT_BLOCK = 256;
private static final String RSA_ECB_PKCS1_PADDING = "RSA/ECB/PKCS1Padding";
public static void main(String[] args) throws NoSuchAlgorithmException {
Map<String,String> params = new HashMap<>() ;
params.put("merchantCode", "S820190712000002") ;
params.put("orderAmount", "999.56") ;
params.put("orderNum", "T1231511321515") ;
params.put("callback", "https://xxx/yyy") ;
params.put("timestamp", "1745377181") ;
String source = paramFormat(params);
System.out.println("source: " + source);
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(RSA);
keyGen.initialize(2048);
KeyPair pair = keyGen.generateKeyPair();
String pubKeyBase64 = Base64.getEncoder().encodeToString(pair.getPublic().getEncoded());
String priKeyBase64 = Base64.getEncoder().encodeToString(pair.getPrivate().getEncoded());
System.out.println("public key: " + pubKeyBase64);
System.out.println("private key: " + priKeyBase64);
String sign = sign(priKeyBase64, source);
System.out.println("sign: " + sign);
boolean verify = verify(pubKeyBase64, source, sign);
System.out.println("verify: " + verify);
}
/**
* This code is for reference only, and needs to be considered in actual use
* 1.HttpClient Initialize participation
* 2.HttpResponse Need to close it in time after use
*/
public static String doPost(String url, String json) throws IOException {
HttpClient client = new DefaultHttpClient();
HttpPost post = new HttpPost(url);
StringEntity s = new StringEntity(json);
s.setContentEncoding("UTF-8");
s.setContentType("application/json");
post.setEntity(s);
HttpResponse res = client.execute(post);
if (res.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
return EntityUtils.toString(res.getEntity());
}
return null;
}
//Parameter formatting
private static String paramFormat(Map<String, String> param) {
String[] keys = param.keySet().toArray(new String[0]);
Arrays.sort(keys); //Must sort first
StringBuilder builder = new StringBuilder();
for (String key : keys) {
String value = param.get(key);
if (value != null && !value.trim().isEmpty()) { //null and empty strings need to be filtered
builder.append(key).append("=").append(value).append("&");
}
}
builder.deleteCharAt(builder.length()-1) ;
return builder.toString();
}
/**
* Calculate the signature
* @param priKeyBase64 Base64 encoded private key
* @param source The source data used to calculate the signature
* @return RSA signature string
*/
public static String sign(String priKeyBase64, String source) {
return encrypt(source, stringToPrivateKey(priKeyBase64)) ;
}
/**
* Verify the signature
* @param pubKeyBase64 Base64 encoded public key
* @param source The source data used to calculate the signature
* @param sign A signature that needs to be verified
* @return RSA signature string
*/
public static boolean verify(String pubKeyBase64, String source, String sign) {
return source.equals(decrypt(sign, stringToPublicKey(pubKeyBase64)));
}
// RSA private key encryption
private static String encrypt(String plainData, PrivateKey privateKey) {
Cipher cipher = null;
try {
cipher = Cipher.getInstance(RSA_ECB_PKCS1_PADDING);
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] dataBytes = plainData.getBytes(StandardCharsets.UTF_8);
int inputLen = dataBytes.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
while (inputLen - offSet > 0) {
int blockSize = Math.min(inputLen - offSet, MAX_ENCRYPT_BLOCK);
byte[] encryptedBlock = cipher.doFinal(dataBytes, offSet, blockSize);
out.write(encryptedBlock);
offSet += blockSize;
}
return Base64.getEncoder().encodeToString(out.toByteArray());
} catch (NoSuchAlgorithmException | NoSuchPaddingException | IOException | InvalidKeyException |
IllegalBlockSizeException | BadPaddingException e) {
throw new RuntimeException("encrypt error.");
}
}
// RSA public key decryption
private static String decrypt(String encryptedData, PublicKey publicKey) {
Cipher cipher = null;
try {
cipher = Cipher.getInstance(RSA_ECB_PKCS1_PADDING);
cipher.init(Cipher.DECRYPT_MODE, publicKey);
byte[] dataBytes = Base64.getDecoder().decode(encryptedData);
int inputLen = dataBytes.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
while (inputLen - offSet > 0) {
int blockSize = Math.min(inputLen - offSet, MAX_DECRYPT_BLOCK);
byte[] decryptedBlock = cipher.doFinal(dataBytes, offSet, blockSize);
out.write(decryptedBlock);
offSet += blockSize;
}
return new String(out.toByteArray(), StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException |
IOException | InvalidKeyException e) {
throw new RuntimeException("decrypt error.");
}
}
// Recover the PublicKey from the Base64 string
public static PublicKey stringToPublicKey(String publicKeyStr) {
byte[] keyBytes = Base64.getDecoder().decode(publicKeyStr);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
try {
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
return keyFactory.generatePublic(keySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException("generatePublicKey error.");
}
}
// Recover the PrivateKey from the Base64 string
public static PrivateKey stringToPrivateKey(String privateKeyStr) {
byte[] keyBytes = Base64.getDecoder().decode(privateKeyStr);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
try {
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
return keyFactory.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException("generatePrivateKey error.");
}
}
}