编码技巧——验签&加密算法工具及支付下单签名

开发中常用的一些加密工具类,会在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);
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值