开发中常用的一些加密工具类,会在MD5验证签名、MD5计算、AES加解密、RSA加解密,场景可能是输入参数加密、参数签名验签防止篡改、支付业务的参数加密;
本篇不讲述算法的原理,仅将开发中需要的常用加密算法代码罗列;
1. AES加密工具类
AES-128-CBC
package AA.common.utils;
import lombok.extern.slf4j.Slf4j;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* @author AA
* @description AES编解码工具类 AES 是一种可逆加密算法,对用户的敏感信息加密处理 对原始数据进行AES加密后,在进行Base64编码转化;
*/
@Slf4j
public class AesUtils {
/**
* 加密用的Key 可以用26个字母和数字组成 此处使用AES-128-CBC加密模式,key需要为16位。
*/
private static String ivParameter = "2098432527847288";
private static AesUtil instance = null;
public static AesUtil getInstance() {
if (instance == null) {
instance = new AesUtil();
}
return instance;
}
/**
* 加密
*/
public String encrypt(String sKey, String encryptStr) {
if (encryptStr == null) {
return null;
}
if (sKey == null || sKey.isEmpty()) {
return "";
}
byte[] encrypted = null;
try {
//Cipher.getInstance("AES/CBC/PKCS5Padding")存在sonar扫描出的安全漏洞
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
byte[] raw = sKey.getBytes();
SecretKeySpec sKeySpec = new SecretKeySpec(raw, "AES");
// 使用CBC模式,需要一个向量iv,可增加加密算法的强度
IvParameterSpec iv = new IvParameterSpec(ivParameter.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, sKeySpec, iv);
encrypted = cipher.doFinal(encryptStr.getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
log.error("AES encrypt failed! e:{}", e);
}
return Base64Util.encode(encrypted);
}
/**
* 解密
*/
public String decrypt(String sKey, String decryptStr) {
if (decryptStr == null) {
return null;
}
try {
byte[] raw = sKey.getBytes(StandardCharsets.US_ASCII);
SecretKeySpec sKeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(ivParameter.getBytes());
cipher.init(Cipher.DECRYPT_MODE, sKeySpec, iv);
byte[] encrypted1 = Base64Util.decode(decryptStr);
byte[] original = cipher.doFinal(encrypted1);
return new String(original, StandardCharsets.UTF_8);
} catch (Exception ex) {
return null;
}
}
@SuppressWarnings("unused")
private String encodeBytes(byte[] bytes) {
StringBuilder strBuf = new StringBuilder();
for (byte aByte : bytes) {
strBuf.append((char) (((aByte >> 4) & 0xF) + ((int) 'a')));
strBuf.append((char) (((aByte) & 0xF) + ((int) 'a')));
}
return strBuf.toString();
}
}
AES-128-GCM
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.*;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
/**
* @author AA
* @description AES对称加密工具类 AES-128-GCM
* @reference:https://blog.csdn.net/T0mato_/article/details/53160772
* @date 2022/1/12
*/
@Slf4j
public class AES128GCMUtil {
/**
* AES
*/
private static final String KEY_ALGORITHM = "AES";
/**
* 默认加密算法 使用GCM模式
*/
private static final String DEFAULT_CIPHER_ALGORITHM = "AES/GCM/PKCS5Padding";
/**
* AES 加密操作
*
* @param plainText 明文
* @param encryptKey 秘钥
* @return
*/
public static String encrypt(String plainText, String encryptKey) {
try {
Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(encryptKey));
byte[] iv = cipher.getIV();
// GCM iv字节长度为12
assert iv.length == 12;
// 加密
byte[] encryptData = cipher.doFinal(plainText.getBytes());
assert encryptData.length == plainText.getBytes().length + 16;
byte[] message = new byte[12 + plainText.getBytes().length + 16];
System.arraycopy(iv, 0, message, 0, 12);
System.arraycopy(encryptData, 0, message, 12, encryptData.length);
return Base64.encodeBase64String(message);
} catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException
| BadPaddingException e) {
log.error("AESUtil_encrypt_error. [plainText={} encryptKey={}] e:{}", plainText, encryptKey, e);
}
return null;
}
/**
* AES 解密操作
*
* @param cipherText 密文
* @param encryptKey 秘钥
* @return
*/
public static String decrypt(String cipherText, String encryptKey) {
byte[] content = Base64.decodeBase64(cipherText);
if (content.length < 12 + 16) {
throw new IllegalArgumentException();
}
GCMParameterSpec params = new GCMParameterSpec(128, content, 0, 12);
try {
Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(encryptKey), params);
byte[] decryptData = cipher.doFinal(content, 12, content.length - 12);
return new String(decryptData);
} catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException
| InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
log.error("AESUtil_decrypt_error. [cipherText={} encryptKey={}] e:{}", cipherText, encryptKey, e);
}
return null;
}
/**
* 生成加密秘钥 AES-128-GCM-ENCRYPT
*/
private static SecretKeySpec getSecretKey(String encryptPass) throws NoSuchAlgorithmException {
KeyGenerator kg = KeyGenerator.getInstance(KEY_ALGORITHM);
// 初始化密钥生成器,AES要求密钥长度为128位、192位、256位
kg.init(128, new SecureRandom(encryptPass.getBytes()));
SecretKey secretKey = kg.generateKey();
// 转换为AES专用密钥
return new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM);
}
}
2. Base64编解码工具类
package AA.common.utils;
import lombok.extern.slf4j.Slf4j;
/**
* @author AA
* @description Base64编解码工具类 Base64是常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法
*/
@Slf4j
class Base64Util {
private static final int BASELENGTH = 128;
private static final int LOOKUPLENGTH = 64;
private static final int TWENTYFOURBITGROUP = 24;
private static final int EIGHTBIT = 8;
private static final int SIXTEENBIT = 16;
private static final int FOURBYTE = 4;
private static final int SIGN = -128;
private static final char PAD = '=';
private static final boolean F_DEBUG = false;
private static final byte[] BASE_64_ALPHABET = new byte[BASELENGTH];
private static final char[] LOOK_UP_BASE_64_ALPHABET = new char[LOOKUPLENGTH];
static {
for (int i = 0; i < BASELENGTH; ++i) {
BASE_64_ALPHABET[i] = -1;
}
for (int i = 'Z'; i >= 'A'; i--) {
BASE_64_ALPHABET[i] = (byte) (i - 'A');
}
for (int i = 'z'; i >= 'a'; i--) {
BASE_64_ALPHABET[i] = (byte) (i - 'a' + 26);
}
for (int i = '9'; i >= '0'; i--) {
BASE_64_ALPHABET[i] = (byte) (i - '0' + 52);
}
BASE_64_ALPHABET['+'] = 62;
BASE_64_ALPHABET['/'] = 63;
for (int i = 0; i <= 25; i++) {
LOOK_UP_BASE_64_ALPHABET[i] = (char) ('A' + i);
}
for (int i = 26, j = 0; i <= 51; i++, j++) {
LOOK_UP_BASE_64_ALPHABET[i] = (char) ('a' + j);
}
for (int i = 52, j = 0; i <= 61; i++, j++) {
LOOK_UP_BASE_64_ALPHABET[i] = (char) ('0' + j);
}
LOOK_UP_BASE_64_ALPHABET[62] = '+';
LOOK_UP_BASE_64_ALPHABET[63] = '/';
}
private static boolean isWhiteSpace(char octect) {
return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9);
}
private static boolean isPad(char octect) {
return (octect == PAD);
}
private static boolean isData(char octect) {
return (octect < BASELENGTH && BASE_64_ALPHABET[octect] != -1);
}
/**
* Encodes hex octects into Base64
*
* @param binaryData Array containing binaryData
* @return Encoded Base64 array
*/
static String encode(byte[] binaryData) {
if (binaryData == null) {
return null;
}
int lengthDataBits = binaryData.length * EIGHTBIT;
if (lengthDataBits == 0) {
return "";
}
int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP;
int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP;
int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets;
char[] encodedData = null;
encodedData = new char[numberQuartet * 4];
byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0;
int encodedIndex = 0;
int dataIndex = 0;
if (F_DEBUG) {
log.info("number of triplets = " + numberTriplets);
}
for (int i = 0; i < numberTriplets; i++) {
b1 = binaryData[dataIndex++];
b2 = binaryData[dataIndex++];
b3 = binaryData[dataIndex++];
if (F_DEBUG) {
log.info("b1= " + b1 + ", b2= " + b2 + ", b3= " + b3);
}
l = (byte) (b2 & 0x0f);
k = (byte) (b1 & 0x03);
byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);
byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc);
if (F_DEBUG) {
log.info("val2 = " + val2);
log.info("k4 = " + (k << 4));
log.info("vak = " + ((val2 & 0xff) | (k << 4)));
}
encodedData[encodedIndex++] = LOOK_UP_BASE_64_ALPHABET[val1];
encodedData[encodedIndex++] = LOOK_UP_BASE_64_ALPHABET[(val2 & 0xff) | (k << 4)];
encodedData[encodedIndex++] = LOOK_UP_BASE_64_ALPHABET[(l << 2) | (val3 & 0xff)];
encodedData[encodedIndex++] = LOOK_UP_BASE_64_ALPHABET[b3 & 0x3f];
}
if (fewerThan24bits == EIGHTBIT) {
b1 = binaryData[dataIndex];
k = (byte) (b1 & 0x03);
if (F_DEBUG) {
log.info("b1=" + b1);
log.info("b1<<2 = " + (b1 >> 2));
}
byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
encodedData[encodedIndex++] = LOOK_UP_BASE_64_ALPHABET[val1];
encodedData[encodedIndex++] = LOOK_UP_BASE_64_ALPHABET[k << 4];
encodedData[encodedIndex++] = PAD;
encodedData[encodedIndex++] = PAD;
} else if (fewerThan24bits == SIXTEENBIT) {
b1 = binaryData[dataIndex];
b2 = binaryData[dataIndex + 1];
l = (byte) (b2 & 0x0f);
k = (byte) (b1 & 0x03);
byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);
encodedData[encodedIndex++] = LOOK_UP_BASE_64_ALPHABET[val1];
encodedData[encodedIndex++] = LOOK_UP_BASE_64_ALPHABET[(val2 & 0xff) | (k << 4)];
encodedData[encodedIndex++] = LOOK_UP_BASE_64_ALPHABET[l << 2];
encodedData[encodedIndex++] = PAD;
}
return new String(encodedData);
}
/**
* Decodes Base64 data into octects
*
* @param encoded string containing Base64 data
* @return Array containind decoded data.
*/
static byte[] decode(String encoded) {
if (encoded == null) {
return null;
}
char[] base64Data = encoded.toCharArray();
// remove white spaces
int len = removeWhiteSpace(base64Data);
if (len % FOURBYTE != 0) {
return null;
}
int numberQuadruple = (len / FOURBYTE);
if (numberQuadruple == 0) {
return new byte[0];
}
byte[] decodedData = null;
byte b1 = 0, b2 = 0, b3 = 0, b4 = 0;
char d1 = 0, d2 = 0, d3 = 0, d4 = 0;
int i = 0;
int encodedIndex = 0;
int dataIndex = 0;
decodedData = new byte[(numberQuadruple) * 3];
for (; i < numberQuadruple - 1; i++) {
if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))
|| !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++]))) {
return null;
} // if found "no data" just return null
b1 = BASE_64_ALPHABET[d1];
b2 = BASE_64_ALPHABET[d2];
b3 = BASE_64_ALPHABET[d3];
b4 = BASE_64_ALPHABET[d4];
decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
decodedData[encodedIndex++] = (byte) (b3 << 6 | (b4 & 0xff));
}
if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))) {
return null;
}
b1 = BASE_64_ALPHABET[d1];
b2 = BASE_64_ALPHABET[d2];
d3 = base64Data[dataIndex++];
d4 = base64Data[dataIndex++];
if (!isData((d3)) || !isData((d4))) {
if (isPad(d3) && isPad(d4)) {
if ((b2 & 0xf) != 0) {
return null;
}
byte[] tmp = new byte[i * 3 + 1];
System.arraycopy(decodedData, 0, tmp, 0, i * 3);
tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
return tmp;
} else if (!isPad(d3) && isPad(d4)) {
b3 = BASE_64_ALPHABET[d3];
if ((b3 & 0x3) != 0) {
return null;
}
byte[] tmp = new byte[i * 3 + 2];
System.arraycopy(decodedData, 0, tmp, 0, i * 3);
tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
return tmp;
} else {
return null;
}
} else { // No PAD e.g 3cQl
b3 = BASE_64_ALPHABET[d3];
b4 = BASE_64_ALPHABET[d4];
decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
decodedData[encodedIndex++] = (byte) (b3 << 6 | (b4 & 0xff));
}
return decodedData;
}
/**
* remove WhiteSpace from MIME containing encoded Base64 data.
*
* @param data the byte array of base64 data (with WS)
* @return the new length
*/
private static int removeWhiteSpace(char[] data) {
if (data == null) {
return 0;
}
// count characters that's not whitespace
int newSize = 0;
int len = data.length;
for (int i = 0; i < len; i++) {
if (!isWhiteSpace(data[i])) {
data[newSize++] = data[i];
}
}
return newSize;
}
}
3. RSA加解密
package AA.common.utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* @author AA
* @description RSA公私钥,加密&解密,签名&验签; 注意:加密/签名的算法要和第三方保持一致
*/
@Slf4j
public class RSAUtils {
/**
* RSA最大加密明文大小
*/
private static final int MAX_ENCRYPT_BLOCK = 1024;
/**
* RSA最大解密密文大小
*/
private static final int MAX_DECRYPT_BLOCK = 4096;
/**
* 获取密钥对
*
* @return 密钥对
*/
public static KeyPair getKeyPair() {
try {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
//2048位
generator.initialize(2048);
return generator.generateKeyPair();
} catch (Exception e) {
log.error("RSA_getKeyPair_error! e:{}", e);
throw new RuntimeException();
}
}
/**
* 获取私钥
*
* @param privateKey 私钥字符串
* @return
*/
public static PrivateKey getPrivateKey(String privateKey) {
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
byte[] decodedKey = Base64.decodeBase64(privateKey.getBytes());
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey);
return keyFactory.generatePrivate(keySpec);
} catch (Exception e) {
log.error("RSA_getPrivateKeyFromStr_error! [privateKey={}] e:{}", privateKey, e);
throw new RuntimeException();
}
}
/**
* 获取公钥
*
* @param publicKey 公钥字符串
* @return
*/
public static PublicKey getPublicKey(String publicKey) {
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
byte[] decodedKey = Base64.decodeBase64(publicKey.getBytes());
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedKey);
return keyFactory.generatePublic(keySpec);
} catch (Exception e) {
log.error("RSA_getPublicKeyFromStr_error! [publicKey={}] e:{}", publicKey, e);
throw new RuntimeException();
}
}
/**
* RSA加密
*
* @param data 待加密数据(明文)
* @param publicKey 公钥
* @return
*/
public static String encrypt(String data, PublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
int inputLen = data.getBytes().length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offset = 0;
byte[] cache;
int i = 0;
// 对数据分段加密
while (inputLen - offset > 0) {
if (inputLen - offset > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(data.getBytes(), offset, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(data.getBytes(), offset, inputLen - offset);
}
out.write(cache, 0, cache.length);
i++;
offset = i * MAX_ENCRYPT_BLOCK;
}
byte[] encryptedData = out.toByteArray();
out.close();
// 获取加密内容使用base64进行编码,并以UTF-8为标准转化成字符串
// 加密后的字符串
return Base64.encodeBase64String(encryptedData);
} catch (Exception e) {
log.error("RSA_encrypt_error! [data={} publicKey={}", data, publicKey);
throw new RuntimeException();
}
}
/**
* RSA解密
*
* @param data 待解密数据(密文)
* @param privateKey 私钥
* @return
*/
public static String decrypt(String data, PrivateKey privateKey) {
try {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] dataBytes = Base64.decodeBase64(data);
int inputLen = dataBytes.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offset = 0;
byte[] cache;
int i = 0;
// 对数据分段解密
while (inputLen - offset > 0) {
if (inputLen - offset > MAX_DECRYPT_BLOCK) {
cache = cipher.doFinal(dataBytes, offset, MAX_DECRYPT_BLOCK);
} else {
cache = cipher.doFinal(dataBytes, offset, inputLen - offset);
}
out.write(cache, 0, cache.length);
i++;
offset = i * MAX_DECRYPT_BLOCK;
}
byte[] decryptedData = out.toByteArray();
out.close();
// 解密后的内容
return new String(decryptedData, StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("RSA_decrypt_error! [data={} privateKey={}", data, privateKey);
throw new RuntimeException();
}
}
/**
* 签名
*
* @param data 待签名数据
* @param privateKey 私钥
* @return 签名
*/
public static String sign(String data, PrivateKey privateKey) {
try {
byte[] keyBytes = privateKey.getEncoded();
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey key = keyFactory.generatePrivate(keySpec);
Signature signature = Signature.getInstance("SHA256WithRSA");
signature.initSign(key);
signature.update(data.getBytes());
return new String(Base64.encodeBase64(signature.sign()));
} catch (Exception e) {
log.error("RSA_sign_error! [data={} privateKey={}]", data, privateKey);
throw new RuntimeException();
}
}
/**
* 验签
*
* @param srcData 原始字符串
* @param publicKey 公钥
* @param sign 签名
* @return 是否验签通过
*/
public static boolean verify(String srcData, PublicKey publicKey, String sign) {
try {
byte[] keyBytes = publicKey.getEncoded();
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey key = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance("SHA256WithRSA");
signature.initVerify(key);
signature.update(srcData.getBytes());
return signature.verify(Base64.decodeBase64(sign.getBytes()));
} catch (Exception e) {
log.error("RSA_verify_error! [srcData={} publicKey={} sign={}]", srcData, publicKey, sign);
throw new RuntimeException();
}
}
}
支付验签方式示例
调用支付系统的接口,一般会是HTTP接口,因为公司内服务与公司外服务(包括合作者CP、支付宝蚂蚁、腾讯支付等),对于支付系统来说都是外部调用;如果支付中台做得好,对内提供DUBBO,对外提供HTTP;如果没做到都是HTTP调用,最多会换成内网.lan域名;
调用支付接口,数据非常重要,从安全的角度,必须要对参数加密、防止参数篡改;这里给一个调用支付系统接口的加密/验签的工具类的示例;
(1)调用支付接口
支付接口公共请求参数
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* @author AA
* @description 支付接口公共请求参数
*/
@Data
public class PayCommonReqDTO {
/**
* 接口版本号
*/
@NotNull
private String version;
/**
* 接口服务名称
*/
@NotNull
private String method;
/**
* 应用编号
*/
@NotNull
private String appId;
/**
* 发送请求的时间,格式:yyyyMMddHHmmss
*/
@NotNull
private String timestamp;
/**
* 签名
*/
@NotNull
private String sign;
/**
* 签名方式,暂定RSA
*/
@NotNull
private String signType;
/**
* 请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,格式:业务参数拼接成的Json对象字符串
*/
@NotNull
private String bizContent;
}
import com.google.common.collect.Maps;
import com.xxx.pay.model.PayCommonReqDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.Map;
/**
* @author AA
* @description 调用支付系统的工具类
* @date 2020/5/12
*/
@Slf4j
public class CallPayUtils {
private static final String SIGNATURE = "sign";
private static final String SIGNATURE_TYPE = "signType";
/**
* POST请求:header设置urlencoded
*
* @return
*/
public static String sendPostUrlEncoded(String url, Map<String, String> paramMap) {
return HttpClientUtil.sendPostForm(url, paramMap);
}
/**
* 初始化通用请求参数
*
* @return
*/
public static PayCommonReqDTO initPayCommonReq() {
PayCommonReqDTO payCommonReqDTO = new PayCommonReqDTO();
payCommonReqDTO.setVersion(PayCouponConstants.VERSION);
payCommonReqDTO.setAppId(ConfigManager.getString(PayCouponConstants.APPID_KEY));
payCommonReqDTO.setTimestamp(DateUtil.toStringByFormat(new Date(), DateUtil.DATETIME_FORMAT_2));
payCommonReqDTO.setSignType(PayCouponConstants.SIGN_TYPE);
return payCommonReqDTO;
}
/**
* 请求体转Map
*
* @param requestParamsDTO
* @return
*/
public static Map<String, String> convertToMap(Object requestParamsDTO) {
Map<String, String> map = Maps.newHashMap();
try {
// 获取javaBean的BeanInfo对象
BeanInfo beanInfo = Introspector.getBeanInfo(requestParamsDTO.getClass(), Object.class);
// 获取属性描述器
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
// 获取属性名
String key = propertyDescriptor.getName();
// 获取该属性的值
Method readMethod = propertyDescriptor.getReadMethod();
String value = (String) readMethod.invoke(requestParamsDTO);
map.put(key, value);
}
} catch (Exception e) {
log.error("BeanUtils.convertToMap_error! e:{}", e);
}
return map;
}
/**
* 用私钥签名,填充sign属性
*
* @param params
* @param privateKey
* @return
*/
public static Map<String, String> signParams(Map<String, String> params, String privateKey) {
// 除去Map中的空值和签名参数
Map<String, String> params2Sign = paraFilter(params);
// 对参数签名
String signature = getPayCouponSign(params2Sign, privateKey);
// 填充签名
params.put(SIGNATURE, signature);
return params;
}
/**
* 除去请求参数中的空值和签名
*
* @param para 请求参数
* @return 去掉空值与签名参数后的请求参数
*/
private static Map<String, String> paraFilter(Map<String, String> para) {
Map<String, String> result = Maps.newHashMap();
if (MapUtils.isEmpty(para)) {
return result;
}
//过滤不需要签名的属性
for (String key : para.keySet()) {
String value = para.get(key);
if (StringUtils.isBlank(value) || key.equalsIgnoreCase(SIGNATURE) || key.equalsIgnoreCase(SIGNATURE_TYPE)) {
continue;
}
result.put(key, value);
}
return result;
}
/**
* 私钥签名
*
* @param para 参与签名的参数<key,value>
* @param privateKey RSA私钥
* @return 签名结果
*/
private static String getPayCouponSign(Map<String, String> para, String privateKey) {
// 1.得到待签名字符串,需要对map进行sort,不需要对value进行URL编码
String linkString = createLinkString(para, true, false);
// 2.RSA签名(也可以用finance-common的RSA类)
return RSAUtils.sign(linkString, RSAUtils.getPrivateKey(privateKey));
}
public static String createLinkString(Map<String, String> para, boolean sort, boolean encode) {
List<String> keys = new ArrayList(para.keySet());
if (sort) {
Collections.sort(keys);
}
StringBuilder sb = new StringBuilder();
for(int i = 0; i < keys.size(); ++i) {
String key = (String)keys.get(i);
String value = (String)para.get(key);
if (encode) {
try {
value = URLEncoder.encode(value, "utf-8");
} catch (UnsupportedEncodingException var9) {
throw new CommonException(var9);
}
}
if (i == keys.size() - 1) {
sb.append(key).append("=").append(value);
} else {
sb.append(key).append("=").append(value).append("&");
}
}
return sb.toString();
}
}
(2)支付签名工具类
package AA.common.utils;
import org.apache.commons.collections.MapUtils;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
/**
* @author AA
* @description 支付签名工具类
*/
public class PaySignUtils {
/**
* 签名key
*/
public final static String SIGNATURE = "signature";
/**
* 签名方法key
*/
public final static String SIGN_METHOD = "signMethod";
/**
* =
*/
public static final String QSTRING_EQUAL = "=";
/**
* &
*/
public static final String QSTRING_SPLIT = "&";
/**
* 拼接请求字符串
*
* @param req 请求参数
* @param key PaySystem分配给商户的密钥
* @return 请求字符串
*/
public static String buildReq(Map<String, String> req, String key) {
// 除去数组中的空值和签名参数
Map<String, String> filteredReq = paraFilter(req);
// 根据参数获取PaySystem签名
String signature = getPaySystemSign(filteredReq, key);
// 签名结果与签名方式加入请求提交参数组中
filteredReq.put(SIGNATURE, signature);
filteredReq.put(SIGN_METHOD, "MD5");
// 请求字符串,key不需要排序,value需要URL编码
return createLinkString(filteredReq, false, true);
}
/**
* 获取请求参数的paramsMap
*
* @param req 请求参数
* @param key PaySystem分配给商户的密钥
* @return 请求参数的map
*/
public static Map<String, String> buildReqParams(Map<String, String> req, String key) {
// 除去数组中的空值和签名参数
Map<String, String> filteredReq = paraFilter(req);
// 根据参数获取PaySystem签名
String signature = getPaySystemSign(filteredReq, key);
// 签名结果与签名方式加入请求提交参数组中
filteredReq.put(SIGNATURE, signature);
filteredReq.put(SIGN_METHOD, "MD5");
return filteredReq;
}
/**
* 支付通知消息验签
*
* @param req 请求参数
* @param key PaySystem分配给商户的密钥
* @return 验签结果
*/
public static boolean verifySignature(Map<String, String> req, String key) {
if (MapUtils.isEmpty(req)) {
return false;
}
// 除去数组中的空值和签名参数
Map<String, String> filteredReq = paraFilter(req);
// 根据参数获取PaySystem签名
String signature = getPaySystemSign(filteredReq, key);
// 获取参数中的签名值
String respSignature = req.get(SIGNATURE);
// 对比签名值
if (null != respSignature && respSignature.equals(signature)) {
return true;
} else {
return false;
}
}
/**
* 获取PaySystem签名
*
* @param para 参与签名的参数<key,value>
* @param key PaySystem分配给商户的密钥
* @return 签名结果
*/
public static String getPaySystemSign(Map<String, String> para, String key) {
// 除去数组中的空值和签名参数
Map<String, String> filteredReq = paraFilter(para);
// 得到待签名字符串
// 需要对map进行sort,不需要对value进行URL编码
String prestr = createLinkString(filteredReq, true, false);
prestr = prestr + QSTRING_SPLIT + md5Summary(key);
return md5Summary(prestr);
}
/**
* 除去请求参数中的空值和签名
*
* @param para 请求参数
* @return 去掉空值与签名参数后的请求参数
*/
public static Map<String, String> paraFilter(Map<String, String> para) {
Map<String, String> result = new HashMap<String, String>(16);
if (para == null || para.size() <= 0) {
return result;
}
for (String key : para.keySet()) {
String value = para.get(key);
if (value == null || value.equals("") || key.equalsIgnoreCase(SIGNATURE)
|| key.equalsIgnoreCase(SIGN_METHOD)) {
continue;
}
result.put(key, value);
}
return result;
}
/**
* 按照“参数=参数值”的模式用“&”字符拼接成字符串
*
* @param para 请求参数
* @param sort 是否需要根据key值作升序排列
* @param encode 是否需要URL编码
* @return 拼接成的字符串
*/
public static String createLinkString(Map<String, String> para, boolean sort, boolean encode) {
List<String> keys = new ArrayList<String>(para.keySet());
if (sort) {
Collections.sort(keys);
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = para.get(key);
if (encode) {
try {
value = URLEncoder.encode(value, "utf-8");
} catch (UnsupportedEncodingException e) {
}
}
// 拼接时,不包括最后一个&字符
if (i == keys.size() - 1) {
sb.append(key).append(QSTRING_EQUAL).append(value);
} else {
sb.append(key).append(QSTRING_EQUAL).append(value).append(QSTRING_SPLIT);
}
}
return sb.toString();
}
/**
* 对传入的参数进行MD5摘要
*
* @param str 需进行MD5摘要的数据
* @return MD5摘要值
*/
public static String md5Summary(String str) {
if (str == null) {
return null;
}
MessageDigest messageDigest = null;
try {
messageDigest = MessageDigest.getInstance("MD5");
messageDigest.reset();
messageDigest.update(str.getBytes("utf-8"));
} catch (NoSuchAlgorithmException e) {
return str;
} catch (UnsupportedEncodingException e) {
return str;
}
byte[] byteArray = messageDigest.digest();
StringBuffer md5StrBuff = new StringBuffer();
for (int i = 0; i < byteArray.length; i++) {
if (Integer.toHexString(0xFF & byteArray[i]).length() == 1) {
md5StrBuff.append("0").append(Integer.toHexString(0xFF & byteArray[i]));
} else {
md5StrBuff.append(Integer.toHexString(0xFF & byteArray[i]));
}
}
return md5StrBuff.toString();
}
}
先生成业务订单,再将信息填充到下单支付插件需要的参数,由插件调用支付中台;
业务订单参数:
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Date;
/**
* @author AA
* @description 业务订单下单参数
*/
@Data
public class OrderCreateReqDTO implements Serializable {
@NotNull
private String openid;
/**
* 业务单号
*/
@NotNull
private String bizNo;
/**
* 订单类型
*/
@NotNull
private Integer orderType;
/**
* 订单金额(单位分)
*/
@NotNull
private Integer orderAmount;
/**
* 产品id
*/
@NotNull
private Integer productId;
/**
* 业务类型
*/
@NotNull
private Integer bizType;
/**
* 产品订单类型
*/
@NotNull
private Integer productOrderType;
/**
* 订单来源
*/
private String source;
/**
* 产品名称
*/
private String productName;
/**
* 订单失效时间
*/
private Date expireTime;
/**
* 代金券id
*/
private String voucherId;
/**
* 代金券面额(分)
*/
private Long voucherAmount;
}
收银台/支付插件的参数:
import lombok.Data;
import java.io.Serializable;
/**
* 收银台参数
*/
@Data
public class ProductBuyRespDTO implements Serializable {
/**
* accessOpenid
*/
private String accessOpenid;
/**
* 应用申请的appId
*/
private String appId;
/**
* 订单失效时间(yyyyMMddHHmmss)
*/
private String expireTime;
/**
* 业务侧订单编号
*/
private String cpOrderNumber;
/**
* 产品价格
*/
private String orderAmount;
/**
* 产品名称
*/
private String productName;
/**
* 产品id
*/
private String productId;
/**
* 产品描述
*/
private String productDesc;
/**
* 支付回调通知URL(支付用)
*/
private String notifyUrl;
/**
* 支付回调通知URL(支付签约用)
*/
private String payNotifyUrl;
/**
* cp签约号
*/
private String cpAgreementNo;
/**
* 签约回调通知URL(支付签约用)
*/
private String signNotifyUrl;
/**
* 回调地址
*/
private String pageNotifyUrl;
}
获取下单结果Map参数(生成过程中对参数签名):
/**
* 获取下单结果参数map
*/
private Map<String, String> buildRespParams(Object requestParamsDTO) {
Map<String, String> params = BeanUtils.convertToMap(requestParamsDTO);
// 填充数字签名
String key = ConfigManager.getString(BusinessConstant.APP_KEY);
return PaySignUtils.buildReqParams(params, key);
}