xiongzhu 3 năm trước cách đây
mục cha
commit
0a4521ce11

+ 174 - 0
src/main/java/cn/com/sandpay/cashier/sdk/CertUtil.java

@@ -0,0 +1,174 @@
+/**
+ *
+ * Licensed Property to Sand Co., Ltd.
+ * 
+ * (C) Copyright of Sand Co., Ltd. 2010
+ *     All Rights Reserved.
+ *
+ * 
+ * Modification History:
+ * =============================================================================
+ *   Author           Date           Description
+ *   ------------ ---------- ---------------------------------------------------
+ *   企业产品团队       2016-10-12       证书工具类.
+ * =============================================================================
+ */
+package cn.com.sandpay.cashier.sdk;
+//支付宝生活号/支付宝小程序
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Enumeration;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * @ClassName: CertUtil
+ * @Description: sdk证书工具类,主要用于对证书的加载和使用
+ * @version 2.0.0
+ */
+public class CertUtil {
+	
+	public static final String PUBLIC_KEY = "public_key";
+	public static final String PRIVATE_KEY = "private_key";
+
+	private static final Logger logger = LoggerFactory.getLogger(CertUtil.class);
+
+	private static final ConcurrentHashMap<String, Object> keys = new ConcurrentHashMap<String, Object>();
+
+	public static void init(String publicKeyPath, String privateKeyPath, String keyPassword) throws Exception {
+		// 加载私钥
+		initPulbicKey(publicKeyPath);
+		// 加载公钥
+		initPrivateKey(privateKeyPath, keyPassword);
+	}
+
+	public static PublicKey getPublicKey() {
+		return (PublicKey) keys.get(PUBLIC_KEY);
+	}
+
+	public static PrivateKey getPrivateKey() {
+		return (PrivateKey) keys.get(PRIVATE_KEY);
+	}
+
+	private static void initPulbicKey(String publicKeyPath) throws Exception {
+
+		String classpathKey = "classpath:";
+		if (publicKeyPath != null) {
+			try {
+				InputStream inputStream = null;
+				if (publicKeyPath.startsWith(classpathKey)) {
+					inputStream = CertUtil.class.getClassLoader()
+							.getResourceAsStream(publicKeyPath.substring(classpathKey.length()));
+				} else {
+					inputStream = new FileInputStream(publicKeyPath);
+				}
+				PublicKey publicKey = CertUtil.getPublicKey(inputStream);
+				keys.put(PUBLIC_KEY, publicKey);
+			} catch (Exception e) {
+				logger.error("无法加载公钥[{}]", new Object[] { publicKeyPath });
+				logger.error(e.getMessage(), e);
+				throw e;
+			}
+		}
+	}
+
+	private static void initPrivateKey(String privateKeyPath, String keyPassword) throws Exception {
+
+		String classpathKey = "classpath:";
+
+		try {
+			InputStream inputStream = null;
+			if (privateKeyPath.startsWith(classpathKey)) {
+				inputStream = CertUtil.class.getClassLoader()
+						.getResourceAsStream(privateKeyPath.substring(classpathKey.length()));
+			} else {
+				inputStream = new FileInputStream(privateKeyPath);
+			}
+			PrivateKey privateKey = CertUtil.getPrivateKey(inputStream, keyPassword);
+			keys.put(PRIVATE_KEY, privateKey);
+		} catch (Exception e) {
+			logger.error("无法加载本地私银[" + privateKeyPath + "]");
+			logger.error(e.getMessage(), e);
+			throw e;
+		}
+	}
+
+	
+	public static PublicKey getPublicKey(InputStream inputStream) throws Exception {
+		try {
+
+			CertificateFactory cf = CertificateFactory.getInstance("X.509");
+			X509Certificate oCert = (X509Certificate) cf.generateCertificate(inputStream);
+			PublicKey publicKey = oCert.getPublicKey();
+			return publicKey;
+		} catch (Exception e) {
+			e.printStackTrace();
+			throw new Exception("读取公钥异常");
+		} finally {
+			try {
+				if (inputStream != null) {
+					inputStream.close();
+				}
+			} catch (IOException e) {
+			}
+		}
+	}
+
+	
+	/**
+	 * 获取私钥对象
+	 * 
+	 * @param inputStream
+	 *            私钥输入流
+	 * @param keyAlgorithm
+	 *            密钥算法
+	 * @return 私钥对象
+	 * @throws Exception
+	 */
+	public static PrivateKey getPrivateKey(InputStream inputStream, String password) throws Exception {
+		try {
+			KeyStore ks = KeyStore.getInstance("PKCS12");
+			char[] nPassword = null;
+			if ((password == null) || password.trim().equals("")) {
+				nPassword = null;
+			} else {
+				nPassword = password.toCharArray();
+			}
+
+			ks.load(inputStream, nPassword);
+			Enumeration<String> enumas = ks.aliases();
+			String keyAlias = null;
+			if (enumas.hasMoreElements()) {
+				keyAlias = (String) enumas.nextElement();
+			}
+
+			PrivateKey privateKey = (PrivateKey) ks.getKey(keyAlias, nPassword);
+			return privateKey;
+		} catch (FileNotFoundException e) {
+			throw new Exception("私钥路径文件不存在");
+		} catch (IOException e) {
+			throw new Exception(e);
+		} catch (NoSuchAlgorithmException e) {
+			throw new Exception("生成私钥对象异常");
+		} finally {
+			try {
+				if (inputStream != null) {
+					inputStream.close();
+				}
+			} catch (IOException e) {
+			}
+		}
+	}
+
+}

+ 423 - 0
src/main/java/cn/com/sandpay/cashier/sdk/CryptoUtil.java

@@ -0,0 +1,423 @@
+/**
+ *
+ * Licensed Property to Sand Co., Ltd.
+ * 
+ * (C) Copyright of Sand Co., Ltd. 2010
+ *     All Rights Reserved.
+ *
+ * 
+ * Modification History:
+ * =============================================================================
+ *   Author           Date           Description
+ *   ------------ ---------- ---------------------------------------------------
+ *   企业产品团队       2016-10-12       加解密工具类.
+ * =============================================================================
+ */
+package cn.com.sandpay.cashier.sdk;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.X509Certificate;
+import java.security.spec.RSAPrivateKeySpec;
+import java.security.spec.RSAPublicKeySpec;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * @ClassName: CryptoUtil
+ * @Description: sdk加解密工具类,主要用于签名、验证、RSA加解密等
+ * @version 2.0.0
+ */
+
+public class CryptoUtil {
+
+	public static  Logger logger = LoggerFactory.getLogger(CryptoUtil.class);
+	
+	/**
+	 * 数字签名函数入口
+	 * 
+	 * @param plainBytes
+	 *            待签名明文字节数组
+	 * @param privateKey
+	 *            签名使用私钥
+	 * @param signAlgorithm
+	 *            签名算法
+	 * @return 签名后的字节数组
+	 * @throws Exception
+	 */
+	public static byte[] digitalSign(byte[] plainBytes, PrivateKey privateKey, String signAlgorithm) throws Exception {
+		try {
+			Signature signature = Signature.getInstance(signAlgorithm);
+			signature.initSign(privateKey);
+			signature.update(plainBytes);
+			byte[] signBytes = signature.sign();
+
+			return signBytes;
+		} catch (NoSuchAlgorithmException e) {
+			throw new Exception(String.format("数字签名时没有[%s]此类算法", signAlgorithm));
+		} catch (InvalidKeyException e) {
+			throw new Exception("数字签名时私钥无效", e);
+		} catch (SignatureException e) {
+			throw new Exception("数字签名时出现异常", e);
+		}
+	}
+
+	/**
+	 * 验证数字签名函数入口
+	 * 
+	 * @param plainBytes
+	 *            待验签明文字节数组
+	 * @param signBytes
+	 *            待验签签名后字节数组
+	 * @param publicKey
+	 *            验签使用公钥
+	 * @param signAlgorithm
+	 *            签名算法
+	 * @return 验签是否通过
+	 * @throws Exception
+	 */
+	public static boolean verifyDigitalSign(byte[] plainBytes, byte[] signBytes, PublicKey publicKey, String signAlgorithm) throws Exception {
+		boolean isValid = false;
+		try {
+			Signature signature = Signature.getInstance(signAlgorithm);
+			signature.initVerify(publicKey);
+			signature.update(plainBytes);
+			isValid = signature.verify(signBytes);
+			return isValid;
+		} catch (NoSuchAlgorithmException e) {
+			throw new Exception(String.format("验证数字签名时没有[%s]此类算法", signAlgorithm), e);
+		} catch (InvalidKeyException e) {
+			throw new Exception("验证数字签名时公钥无效", e);
+		} catch (SignatureException e) {
+			throw new Exception("验证数字签名时出现异常", e);
+		}
+	}
+
+	/**
+	 * 验证数字签名函数入口
+	 * 
+	 * @param plainBytes
+	 *            待验签明文字节数组
+	 * @param signBytes
+	 *            待验签签名后字节数组
+	 * @param publicKey
+	 *            验签使用公钥
+	 * @param signAlgorithm
+	 *            签名算法
+	 * @return 验签是否通过
+	 * @throws Exception
+	 */
+	public static boolean verifyDigitalSign(byte[] plainBytes, byte[] signBytes, X509Certificate cert, String signAlgorithm) throws Exception {
+		boolean isValid = false;
+		try {
+			Signature signature = Signature.getInstance(signAlgorithm);
+			signature.initVerify(cert);
+			signature.update(plainBytes);
+			isValid = signature.verify(signBytes);
+			return isValid;
+		} catch (NoSuchAlgorithmException e) {
+			throw new Exception(String.format("验证数字签名时没有[%s]此类算法", signAlgorithm));
+		} catch (InvalidKeyException e) {
+			throw new Exception("验证数字签名时公钥无效", e);
+		} catch (SignatureException e) {
+			throw new Exception("验证数字签名时出现异常", e);
+		}
+	}
+
+	/**
+	 * RSA加密
+	 * 
+	 * @param plainBytes
+	 *            明文字节数组
+	 * @param publicKey
+	 *            公钥
+	 * @param keyLength
+	 *            密钥bit长度
+	 * @param reserveSize
+	 *            padding填充字节数,预留11字节
+	 * @param cipherAlgorithm
+	 *            加解密算法,一般为RSA/ECB/PKCS1Padding
+	 * @return 加密后字节数组,不经base64编码
+	 * @throws Exception
+	 */
+	public static byte[] RSAEncrypt(byte[] plainBytes, PublicKey publicKey, int keyLength, int reserveSize, String cipherAlgorithm)
+			throws Exception {
+		int keyByteSize = keyLength / 8; // 密钥字节数
+		int encryptBlockSize = keyByteSize - reserveSize; // 加密块大小=密钥字节数-padding填充字节数
+		int nBlock = plainBytes.length / encryptBlockSize;// 计算分段加密的block数,向上取整
+		if ((plainBytes.length % encryptBlockSize) != 0) { // 余数非0,block数再加1
+			nBlock += 1;
+		}
+
+		try {
+			Cipher cipher = Cipher.getInstance(cipherAlgorithm);
+			cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+
+			// 输出buffer,大小为nBlock个keyByteSize
+			ByteArrayOutputStream outbuf = new ByteArrayOutputStream(nBlock * keyByteSize);
+			// 分段加密
+			for (int offset = 0; offset < plainBytes.length; offset += encryptBlockSize) {
+				int inputLen = plainBytes.length - offset;
+				if (inputLen > encryptBlockSize) {
+					inputLen = encryptBlockSize;
+				}
+
+				// 得到分段加密结果
+				byte[] encryptedBlock = cipher.doFinal(plainBytes, offset, inputLen);
+				// 追加结果到输出buffer中
+				outbuf.write(encryptedBlock);
+			}
+
+			outbuf.flush();
+			outbuf.close();
+			return outbuf.toByteArray();
+		} catch (NoSuchAlgorithmException e) {
+			throw new Exception(String.format("没有[%s]此类加密算法", cipherAlgorithm));
+		} catch (NoSuchPaddingException e) {
+			throw new Exception(String.format("没有[%s]此类填充模式", cipherAlgorithm));
+		} catch (InvalidKeyException e) {
+			throw new Exception("无效密钥", e);
+		} catch (IllegalBlockSizeException e) {
+			throw new Exception("加密块大小不合法", e);
+		} catch (BadPaddingException e) {
+			throw new Exception("错误填充模式", e);
+		} catch (IOException e) {
+			throw new Exception("字节输出流异常", e);
+		}
+	}
+
+	/**
+	 * RSA解密
+	 * 
+	 * @param encryptedBytes
+	 *            加密后字节数组
+	 * @param privateKey
+	 *            私钥
+	 * @param keyLength
+	 *            密钥bit长度
+	 * @param reserveSize
+	 *            padding填充字节数,预留11字节
+	 * @param cipherAlgorithm
+	 *            加解密算法,一般为RSA/ECB/PKCS1Padding
+	 * @return 解密后字节数组,不经base64编码
+	 * @throws Exception
+	 */
+	public static byte[] RSADecrypt(byte[] encryptedBytes, PrivateKey privateKey, int keyLength, int reserveSize, String cipherAlgorithm)
+			throws Exception {
+		int keyByteSize = keyLength / 8; // 密钥字节数
+		int decryptBlockSize = keyByteSize - reserveSize; // 解密块大小=密钥字节数-padding填充字节数
+		int nBlock = encryptedBytes.length / keyByteSize;// 计算分段解密的block数,理论上能整除
+
+		try {
+			Cipher cipher = Cipher.getInstance(cipherAlgorithm);
+			cipher.init(Cipher.DECRYPT_MODE, privateKey);
+
+			// 输出buffer,大小为nBlock个decryptBlockSize
+			ByteArrayOutputStream outbuf = new ByteArrayOutputStream(nBlock * decryptBlockSize);
+			// 分段解密
+			for (int offset = 0; offset < encryptedBytes.length; offset += keyByteSize) {
+				// block大小: decryptBlock 或 剩余字节数
+				int inputLen = encryptedBytes.length - offset;
+				if (inputLen > keyByteSize) {
+					inputLen = keyByteSize;
+				}
+
+				// 得到分段解密结果
+				byte[] decryptedBlock = cipher.doFinal(encryptedBytes, offset, inputLen);
+				// 追加结果到输出buffer中
+				outbuf.write(decryptedBlock);
+			}
+
+			outbuf.flush();
+			outbuf.close();
+			return outbuf.toByteArray();
+		} catch (NoSuchAlgorithmException e) {
+			throw new Exception(String.format("没有[%s]此类解密算法", cipherAlgorithm));
+		} catch (NoSuchPaddingException e) {
+			throw new Exception(String.format("没有[%s]此类填充模式", cipherAlgorithm));
+		} catch (InvalidKeyException e) {
+			throw new Exception("无效密钥", e);
+		} catch (IllegalBlockSizeException e) {
+			throw new Exception("解密块大小不合法", e);
+		} catch (BadPaddingException e) {
+			throw new Exception("错误填充模式", e);
+		} catch (IOException e) {
+			throw new Exception("字节输出流异常", e);
+		}
+	}
+	
+	public static PublicKey toPublicKey(BigInteger exponent,BigInteger modulus) throws Exception {
+		
+		KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+		RSAPublicKeySpec pubSpec = new RSAPublicKeySpec(modulus, exponent);
+		PublicKey key = keyFactory.generatePublic(pubSpec);
+		return key;
+	}
+	
+	public static PrivateKey toPrivateKey(BigInteger exponent,BigInteger modulus) throws Exception {
+		
+		KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+		RSAPrivateKeySpec prispec = new RSAPrivateKeySpec(modulus, exponent);
+		PrivateKey key = keyFactory.generatePrivate(prispec);
+		return key;
+	}
+
+	
+	/**
+	 * AES加密
+	 * 
+	 * @param plainBytes
+	 *            明文字节数组
+	 * @param keyBytes
+	 *            密钥字节数组
+	 * @param keyAlgorithm
+	 *            密钥算法
+	 * @param cipherAlgorithm
+	 *            加解密算法
+	 * @param IV
+	 *            随机向量
+	 * @return 加密后字节数组,不经base64编码
+	 * @throws Exception
+	 */
+	public static byte[] AESEncrypt(byte[] plainBytes, byte[] keyBytes, String keyAlgorithm, String cipherAlgorithm, String IV)
+			throws Exception {
+		try {
+			// AES密钥长度为128bit、192bit、256bit,默认为128bit
+			if (keyBytes.length % 8 != 0 || keyBytes.length < 16 || keyBytes.length > 32) {
+				throw new Exception("AES密钥长度不合法");
+			}
+
+			Cipher cipher = Cipher.getInstance(cipherAlgorithm);
+			SecretKey secretKey = new SecretKeySpec(keyBytes, keyAlgorithm);
+			if (StringUtils.trimToNull(IV) != null) {
+				IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes());
+				cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivspec);
+			} else {
+				cipher.init(Cipher.ENCRYPT_MODE, secretKey);
+			}
+
+			byte[] encryptedBytes = cipher.doFinal(plainBytes);
+
+			return encryptedBytes;
+		} catch (NoSuchAlgorithmException e) {
+			throw new Exception(String.format("没有[%s]此类加密算法", cipherAlgorithm));
+		} catch (NoSuchPaddingException e) {
+			throw new Exception(String.format("没有[%s]此类填充模式", cipherAlgorithm));
+		} catch (InvalidKeyException e) {
+			throw new Exception("无效密钥", e);
+		} catch (InvalidAlgorithmParameterException e) {
+			throw new Exception("无效密钥参数", e);
+		} catch (BadPaddingException e) {
+			throw new Exception("错误填充模式", e);
+		} catch (IllegalBlockSizeException e) {
+			throw new Exception("加密块大小不合法", e);
+		}
+	}
+
+	/**
+	 * AES解密
+	 * 
+	 * @param encryptedBytes
+	 *            密文字节数组,不经base64编码
+	 * @param keyBytes
+	 *            密钥字节数组
+	 * @param keyAlgorithm
+	 *            密钥算法
+	 * @param cipherAlgorithm
+	 *            加解密算法
+	 * @param IV
+	 *            随机向量
+	 * @return 解密后字节数组
+	 * @throws Exception
+	 */
+	public static byte[] AESDecrypt(byte[] encryptedBytes, byte[] keyBytes, String keyAlgorithm, String cipherAlgorithm, String IV)
+			throws Exception {
+		try {
+			// AES密钥长度为128bit、192bit、256bit,默认为128bit
+			if (keyBytes.length % 8 != 0 || keyBytes.length < 16 || keyBytes.length > 32) {
+				throw new Exception("AES密钥长度不合法");
+			}
+
+			Cipher cipher = Cipher.getInstance(cipherAlgorithm);
+			SecretKey secretKey = new SecretKeySpec(keyBytes, keyAlgorithm);
+			if (IV != null && StringUtils.trimToNull(IV) != null) {
+				IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes());
+				cipher.init(Cipher.DECRYPT_MODE, secretKey, ivspec);
+			} else {
+				cipher.init(Cipher.DECRYPT_MODE, secretKey);
+			}
+
+			byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
+
+			return decryptedBytes;
+		} catch (NoSuchAlgorithmException e) {
+			throw new Exception(String.format("没有[%s]此类加密算法", cipherAlgorithm));
+		} catch (NoSuchPaddingException e) {
+			throw new Exception(String.format("没有[%s]此类填充模式", cipherAlgorithm));
+		} catch (InvalidKeyException e) {
+			throw new Exception("无效密钥", e);
+		} catch (InvalidAlgorithmParameterException e) {
+			throw new Exception("无效密钥参数", e);
+		} catch (BadPaddingException e) {
+			throw new Exception("错误填充模式", e);
+		} catch (IllegalBlockSizeException e) {
+			throw new Exception("解密块大小不合法", e);
+		}
+	}
+	
+	public static  byte[] hexString2ByteArr(String hexStr) {
+		return new BigInteger(hexStr, 16).toByteArray();
+	}
+	public static final byte[] hexStrToBytes(String s) {
+		byte[] bytes; 
+		bytes = new byte[s.length() / 2];
+		for (int i = 0; i < bytes.length; i++) { 
+			bytes[i] = (byte) Integer.parseInt(s.substring(2 * i, 2 * 		i + 2), 16);
+		} 
+		return bytes;
+	}
+	/**
+	 * 字符数组16进制字符
+	 * 
+	 * @param bytes
+	 * @return
+	 */
+	public static String bytes2string(byte[] bytes, int radix) {
+		int size = 2;
+		if (radix == 2) {
+			size = 8;
+		}
+		StringBuilder sb = new StringBuilder(bytes.length * size);
+		for (int i = 0; i < bytes.length; i++) {
+			int integer = bytes[i];
+			while (integer < 0) {
+				integer = integer + 256;
+			}
+			String str = Integer.toString(integer, radix);
+			sb.append(StringUtils.leftPad(str.toUpperCase(), size, "0"));
+		}
+		return sb.toString();
+	}
+	
+	
+}

+ 336 - 0
src/main/java/cn/com/sandpay/cashier/sdk/HttpClient.java

@@ -0,0 +1,336 @@
+/**
+ *
+ * Licensed Property to Sand Co., Ltd.
+ * 
+ * (C) Copyright of Sand Co., Ltd. 2010
+ *     All Rights Reserved.
+ *
+ * 
+ * Modification History:
+ * =============================================================================
+ *   Author           Date           Description
+ *   ------------ ---------- ---------------------------------------------------
+ *   企业产品团队       2016-10-12       Http通讯工具类.
+ * =============================================================================
+ */
+package cn.com.sandpay.cashier.sdk;
+
+import java.io.IOException;
+import java.net.URL;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * @ClassName: HttpClient
+ * @Description: 主要用于Http通讯
+ * @version 2.0.0
+ */
+public class HttpClient {
+
+	private static Logger logger = LoggerFactory.getLogger(HttpClient.class);
+
+	private static final String DEFAULT_CHARSET = "UTF-8";
+
+	private static SSLContext sslcontext;
+
+	private static SSLConnectionSocketFactory sslsf;
+
+	public static String doPost(String url, Map<String, String> params, int connectTimeout, int readTimeout)
+			throws IOException {
+		return doPost(url, params, DEFAULT_CHARSET, connectTimeout, readTimeout);
+	}
+
+	public static String doPost(String url, Map<String, String> params, String charset, int connectTimeout,
+			int readTimeout) throws IOException {
+		Map<String, String> headers = new HashMap<String, String>();
+		headers.put("Content-type", "application/x-www-form-urlencoded;charset=" + charset);
+		return doPost(url, headers, params, charset, connectTimeout, readTimeout);
+	}
+
+	public static String doPost(String url, Map<String, String> headers, Map<String, String> params,
+			final String charset, int connectTimeout, int readTimeout) throws IOException {
+
+		URL targetUrl = new URL(url);
+		HttpHost httpHost = new HttpHost(targetUrl.getHost(), targetUrl.getPort(), targetUrl.getProtocol());
+		logger.info("host:" + targetUrl.getHost() + ",port:" + targetUrl.getPort() + ",protocol:"
+				+ targetUrl.getProtocol() + ",path:" + targetUrl.getPath());
+
+		CloseableHttpClient httpclient = getHttpClient(targetUrl);
+
+		try {
+			HttpPost httpPost = getHttpPost(targetUrl, headers, params, charset, connectTimeout, readTimeout);
+
+			String resp = httpclient.execute(httpHost, httpPost, new ResponseHandler<String>() {
+				public String handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
+
+					int status = response.getStatusLine().getStatusCode();
+
+					logger.info("status:["+status+"]");
+					if (status == 200) {
+						return EntityUtils.toString(response.getEntity(), charset);
+					} else {
+						return "";
+					}
+				}
+			});
+			return resp;
+		} finally {
+			httpclient.close();
+		}
+
+	}
+
+	/**
+	 * 执行HTTP GET请求。
+	 * 
+	 * @param url
+	 *            请求地址
+	 * @param params
+	 *            请求参数
+	 * @return 响应字符串
+	 * @throws IOException
+	 */
+	public static String doGet(String url, Map<String, String> params) throws IOException {
+		return doGet(url, params, DEFAULT_CHARSET);
+	}
+
+	/**
+	 * 执行HTTP GET请求。
+	 * 
+	 * @param url
+	 *            请求地址
+	 * @param params
+	 *            请求参数
+	 * @param charset
+	 *            字符集,如UTF-8, GBK, GB2312
+	 * @return 响应字符串
+	 * @throws IOException
+	 */
+	public static String doGet(String url, Map<String, String> params, String charset) throws IOException {
+
+		Map<String, String> headers = new HashMap<String, String>();
+		headers.put("Content-type", "application/x-www-form-urlencoded;charset=" + charset);
+		return doGet(url, headers, params, charset);
+
+	}
+
+	/**
+	 * 
+	 * @param url
+	 * @param headers
+	 * @param params
+	 * @param charset
+	 * @return
+	 * @throws IOException
+	 * @throws ClientProtocolException
+	 */
+	public static String doGet(String url, Map<String, String> headers, Map<String, String> params,
+			final String charset) throws IOException {
+
+		URL targetUrl = new URL(url);
+		CloseableHttpClient httpclient = getHttpClient(targetUrl);
+
+		try {
+
+			HttpGet httpGet = getHttpGet(url, headers, params, charset);
+
+			String resp = httpclient.execute(httpGet, new ResponseHandler<String>() {
+				public String handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
+					int status = response.getStatusLine().getStatusCode();
+
+					logger.info("status:[{}]", new Object[] { status });
+					if (status == 200) {
+						return EntityUtils.toString(response.getEntity(), charset);
+					} else {
+						return "";
+					}
+				}
+			});
+			return resp;
+
+		} finally {
+			httpclient.close();
+		}
+	}
+
+	/**
+	 * 
+	 * @param targetUrl
+	 * @param headers
+	 * @param params
+	 * @param charset
+	 * @param isProxy
+	 * @return
+	 * @throws IOException
+	 */
+	private static HttpGet getHttpGet(String url, Map<String, String> headers, Map<String, String> params,
+			String charset) throws IOException {
+
+		URL targetUrl = buildGetUrl(url, buildQuery(params, charset));
+		HttpGet httpGet = new HttpGet(targetUrl.toString());
+
+		Iterator<Entry<String, String>> iterator = headers.entrySet().iterator();
+		while (iterator.hasNext()) {
+			Entry<String, String> entry = iterator.next();
+			httpGet.setHeader(entry.getKey(), entry.getValue());
+		}
+
+		return httpGet;
+
+	}
+
+	/**
+	 * @param isProxy
+	 * 
+	 * @param targetUrl @param headers @param params @param charset @param
+	 * connectTimeout @param readTimeout @return @throws IOException @throws
+	 */
+	private static HttpPost getHttpPost(URL targetUrl, Map<String, String> headers, Map<String, String> params,
+			String charset, int connectTimeout, int readTimeout) throws IOException {
+
+		HttpPost httpPost = new HttpPost(targetUrl.getPath());
+
+		Iterator<Entry<String, String>> iterator = headers.entrySet().iterator();
+		while (iterator.hasNext()) {
+			Entry<String, String> entry = iterator.next();
+			httpPost.setHeader(entry.getKey(), entry.getValue());
+		}
+
+		RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(readTimeout)
+				.setConnectTimeout(connectTimeout) // Connection timeout is the timeout until a connection with the
+													// server is established.
+				.build();
+		httpPost.setConfig(requestConfig);
+
+		StringEntity entity = new StringEntity(buildQuery(params, charset), charset);
+		httpPost.setEntity(entity);
+
+		return httpPost;
+	}
+
+	/**
+	 * 
+	 * @param targetUrl
+	 * @return
+	 */
+	private static CloseableHttpClient getHttpClient(URL targetUrl) {
+
+		CloseableHttpClient httpClient = null;
+		if ("https".equals(targetUrl.getProtocol())) {
+
+			httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
+		} else {
+			httpClient = HttpClients.createDefault();
+		}
+		return httpClient;
+	}
+
+	private static class DefaultTrustManager implements X509TrustManager {
+		public X509Certificate[] getAcceptedIssuers() {
+			return null;
+		}
+
+		public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+		}
+
+		public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+		}
+	}
+
+	static {
+		try {
+			sslcontext = SSLContext.getInstance("TLS");
+			sslcontext.init(new KeyManager[0], new TrustManager[] { new DefaultTrustManager() }, new SecureRandom());
+
+			// Allow TLSv1 protocol only
+			sslsf = new SSLConnectionSocketFactory(sslcontext, new String[] { "TLSv1" }, null, new HostnameVerifier() {
+				public boolean verify(String hostname, SSLSession session) {
+					return true;// 默认认证不通过,进行证书校验。
+				}
+			});
+
+			// javax.net.ssl.SSLPeerUnverifiedException: Host name '192.168.92.124' does not
+			// match the certificate subject provided by the peer
+			// (EMAILADDRESS=lsq1015@qq.com, CN=ipay, OU=CMBC, O=XMCMBC, L=Xiamen,
+			// ST=Fujian, C=CN)
+			// at
+			// org.apache.http.conn.ssl.SSLConnectionSocketFactory.verifyHostname(SSLConnectionSocketFactory.java:394)
+
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+
+	}
+	
+	public static String buildQuery(Map<String, String> params, String charset) throws IOException {
+		 
+		List<NameValuePair> nvps = new LinkedList<NameValuePair>();
+		
+		Set<Entry<String, String>> entries = params.entrySet();
+		for (Entry<String, String> entry : entries) {
+			nvps.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
+		}
+		String str = URLEncodedUtils.format(nvps, charset);
+		
+		return str;
+	 }
+	
+	public static URL buildGetUrl(String strUrl, String query) throws IOException {
+        URL url = new URL(strUrl);
+        if (StringUtils.isEmpty(query)) {
+            return url;
+        }
+
+        if (StringUtils.isEmpty(url.getQuery())) {
+            if (strUrl.endsWith("?")) {
+                strUrl = strUrl + query;
+            } else {
+                strUrl = strUrl + "?" + query;
+            }
+        } else {
+            if (strUrl.endsWith("&")) {
+                strUrl = strUrl + query;
+            } else {
+                strUrl = strUrl + "&" + query;
+            }
+        }
+
+        return new URL(strUrl);
+    }
+
+}

+ 39 - 0
src/main/java/cn/com/sandpay/cashier/sdk/RandomStringGenerator.java

@@ -0,0 +1,39 @@
+/**
+ * Copyright : http://www.sandpay.com.cn , 2011-2014
+ * Project : sandpay-qr-demo
+ * $Id$
+ * $Revision$
+ * Last Changed by pxl at 2018-4-25 下午3:50:48
+ * $URL$
+ * 
+ * Change Log
+ * Author      Change Date    Comments
+ *-------------------------------------------------------------
+ * pxl         2018-4-25        Initailized
+ */
+package cn.com.sandpay.cashier.sdk;
+
+import java.util.Random;
+
+/**
+ *
+ * @ClassName :RandomStringGenerator
+ * @author : pxl
+ * @Date : 2018-4-25 下午3:50:48
+ * @version 2.0.0
+ *
+ */
+public class RandomStringGenerator {
+
+	public static String getRandomStringByLength(int length)
+	  {
+	    String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+	    Random random = new Random();
+	    StringBuffer sb = new StringBuffer();
+	    for (int i = 0; i < length; i++) {
+	      int number = random.nextInt(base.length());
+	      sb.append(base.charAt(number));
+	    }
+	    return sb.toString();
+	  }
+}

+ 282 - 0
src/main/java/cn/com/sandpay/cashier/sdk/SDKConfig.java

@@ -0,0 +1,282 @@
+/**
+ * Licensed Property to Sand Co., Ltd.
+ * <p>
+ * (C) Copyright of Sand Co., Ltd. 2010
+ * All Rights Reserved.
+ * <p>
+ * <p>
+ * Modification History:
+ * =============================================================================
+ * Author           Date           Description
+ * ------------ ---------- ---------------------------------------------------
+ * 企业产品团队       2016-10-12       基本参数工具类.
+ * =============================================================================
+ */
+package cn.com.sandpay.cashier.sdk;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * @version 2.0.0
+ * @ClassName: SDKConfig
+ * @Description:
+ */
+public class SDKConfig {
+
+    public static Logger logger = LoggerFactory.getLogger(SDKConfig.class);
+
+    public static final String FILE_NAME = "sand_sdk.properties";
+
+    /**
+     * 通讯地址.
+     */
+    private String url;
+    /**
+     * 商户号.
+     */
+    private String mid;
+    /**
+     * 平台商户号.
+     */
+    private String plMid;
+    /**
+     * 商户私钥证书路径.
+     */
+    private String signCertPath;
+    /**
+     * 商户私钥证书密码.
+     */
+    private String signCertPwd;
+    /**
+     * 杉德证书路径.
+     */
+    private String sandCertPath;
+
+    /**
+     * 配置文件中的通讯地址常量.
+     */
+    public static final String SDK_URL            = "sandsdk.url";
+    /**
+     * 配置文件中的商户号常量.
+     */
+    public static final String SDK_MID            = "sandsdk.mid";
+    /**
+     * 配置文件中的平台商户号常量.
+     */
+    public static final String SDK_PL_MID         = "sandsdk.plMid";
+    /**
+     * 配置文件中的商户私钥证书路径常量.
+     */
+    public static final String SDK_SIGN_CERT_PATH = "sandsdk.signCert.path";
+    /**
+     * 配置文件中的商户私钥证书密码常量.
+     */
+    public static final String SDK_SIGN_CERT_PWD  = "sandsdk.signCert.pwd";
+    /**
+     * 配置文件中的杉德证书路径常量.
+     */
+    public static final String SDK_SNAD_CERT_PATH = "sandsdk.sandCert.path";
+
+    /**
+     * 操作对象.
+     */
+    private static SDKConfig  config = new SDKConfig();
+    /**
+     * 属性文件对象.
+     */
+    private        Properties properties;
+
+    private SDKConfig() {
+        super();
+    }
+
+    /**
+     * 获取config对象.
+     *
+     * @return
+     */
+    public static SDKConfig getConfig() {
+        return config;
+    }
+
+    /**
+     * 从properties文件加载
+     *
+     * @param rootPath 不包含文件名的目录.
+     */
+    public void loadPropertiesFromPath(String rootPath) {
+        if (StringUtils.isNotBlank(rootPath)) {
+            logger.info("从路径读取配置文件: " + rootPath + File.separator + FILE_NAME);
+            File file = new File(rootPath + File.separator + FILE_NAME);
+            InputStream in = null;
+            if (file.exists()) {
+                try {
+                    in = new FileInputStream(file);
+                    properties = new Properties();
+                    properties.load(in);
+                    loadProperties(properties);
+                } catch (FileNotFoundException e) {
+                    logger.error(e.getMessage(), e);
+                } catch (IOException e) {
+                    logger.error(e.getMessage(), e);
+                } finally {
+                    if (null != in) {
+                        try {
+                            in.close();
+                        } catch (IOException e) {
+                            logger.error(e.getMessage(), e);
+                        }
+                    }
+                }
+            } else {
+                // 由于此时可能还没有完成LOG的加载,因此采用标准输出来打印日志信息
+                logger.error(rootPath + FILE_NAME + "不存在,加载参数失败");
+            }
+        } else {
+            loadPropertiesFromSrc();
+        }
+
+    }
+
+    /**
+     * 从classpath路径下加载配置参数
+     */
+    public void loadPropertiesFromSrc() {
+        InputStream in = null;
+        try {
+            logger.info("从classpath: " + SDKConfig.class.getClassLoader().getResource("").getPath() + " 获取属性文件" + FILE_NAME);
+            in = SDKConfig.class.getClassLoader().getResourceAsStream(FILE_NAME);
+            if (null != in) {
+                properties = new Properties();
+                try {
+                    properties.load(in);
+                } catch (IOException e) {
+                    throw e;
+                }
+            } else {
+                logger.error(FILE_NAME + "属性文件未能在classpath指定的目录下 " + SDKConfig.class.getClassLoader().getResource("").getPath() + " 找到!");
+                return;
+            }
+            loadProperties(properties);
+        } catch (IOException e) {
+            logger.error(e.getMessage(), e);
+        } finally {
+            if (null != in) {
+                try {
+                    in.close();
+                } catch (IOException e) {
+                    logger.error(e.getMessage(), e);
+                }
+            }
+        }
+    }
+
+    /**
+     * 根据传入的 {@link #load(java.util.Properties)}对象设置配置参数
+     *
+     * @param pro
+     */
+    public void loadProperties(Properties pro) {
+        logger.info("开始从属性文件中加载配置项");
+        String value = null;
+
+        value = pro.getProperty(SDK_URL);
+        if (!StringUtils.isEmpty(value)) {
+            this.url = value.trim();
+            logger.info("配置项:通讯地址==>" + SDK_URL + "==>" + value + " 已加载");
+        }
+        value = pro.getProperty(SDK_MID);
+        if (!StringUtils.isEmpty(value)) {
+            this.mid = value.trim();
+            logger.info("配置项:商户号==>" + SDK_MID + "==>" + value + " 已加载");
+        }
+        value = pro.getProperty(SDK_PL_MID);
+        if (!StringUtils.isEmpty(value)) {
+            this.plMid = value.trim();
+            logger.info("配置项:平台商户号==>" + SDK_PL_MID + "==>" + value + " 已加载");
+        }
+        value = pro.getProperty(SDK_SIGN_CERT_PATH);
+        if (!StringUtils.isEmpty(value)) {
+            this.signCertPath = value.trim();
+            logger.info("配置项:商户私钥证书路径==>" + SDK_SIGN_CERT_PATH + "==>" + value + " 已加载");
+        }
+        value = pro.getProperty(SDK_SIGN_CERT_PWD);
+        if (!StringUtils.isEmpty(value)) {
+            this.signCertPwd = value.trim();
+            logger.info("配置项:商户私钥证书密码==>" + SDK_SIGN_CERT_PWD + "==>" + value + " 已加载");
+        }
+        value = pro.getProperty(SDK_SNAD_CERT_PATH);
+        if (!StringUtils.isEmpty(value)) {
+            this.sandCertPath = value.trim();
+            logger.info("配置项:杉德公钥证书路径==>" + SDK_SNAD_CERT_PATH + "==>" + value + " 已加载");
+        }
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public String getMid() {
+        return mid;
+    }
+
+    public void setMid(String mid) {
+        this.mid = mid;
+    }
+
+    public String getPlMid() {
+        return plMid;
+    }
+
+    public void setPlMid(String plMid) {
+        this.plMid = plMid;
+    }
+
+    public String getSignCertPath() {
+        return signCertPath;
+    }
+
+    public void setSignCertPath(String signCertPath) {
+        this.signCertPath = signCertPath;
+    }
+
+    public String getSignCertPwd() {
+        return signCertPwd;
+    }
+
+    public void setSignCertPwd(String signCertPwd) {
+        this.signCertPwd = signCertPwd;
+    }
+
+    public String getSandCertPath() {
+        return sandCertPath;
+    }
+
+    public void setSandCertPath(String sandCertPath) {
+        this.sandCertPath = sandCertPath;
+    }
+
+    public Properties getProperties() {
+        return properties;
+    }
+
+    public void setProperties(Properties properties) {
+        this.properties = properties;
+    }
+
+
+}

+ 132 - 0
src/main/java/cn/com/sandpay/cashier/sdk/SDKUtil.java

@@ -0,0 +1,132 @@
+/**
+ *
+ * Licensed Property to Sand Co., Ltd.
+ * 
+ * (C) Copyright of Sand Co., Ltd. 2010
+ *     All Rights Reserved.
+ *
+ * 
+ * Modification History:
+ * =============================================================================
+ *   Author           Date           Description
+ *   ------------ ---------- ---------------------------------------------------
+ *   企业产品团队       2016-10-12       SDK工具类.
+ * =============================================================================
+ */
+package cn.com.sandpay.cashier.sdk;
+
+import java.io.UnsupportedEncodingException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * @ClassName: SDKUtil
+ * @Description: 
+ * @version 2.0.0
+ */
+public class SDKUtil {
+
+	/**
+	 * 将形如key=value&key=value的字符串转换为相应的Map对象
+	 * 
+	 * @param result
+	 * @return
+	 */
+	public static Map<String, String> convertResultStringToMap(String result) {
+		Map<String, String> map = null;
+		try {
+
+			if (StringUtils.isNotBlank(result)) {
+				if (result.startsWith("{") && result.endsWith("}")) {
+					System.out.println(result.length());
+					result = result.substring(1, result.length() - 1);
+				}
+				map = parseQString(result);
+			}
+
+		} catch (UnsupportedEncodingException e) {
+			e.printStackTrace();
+		}
+		return map;
+	}
+
+	/**
+	 * 解析应答字符串,生成应答要素
+	 * 
+	 * @param str
+	 *            需要解析的字符串
+	 * @return 解析的结果map
+	 * @throws UnsupportedEncodingException
+	 */
+	public static Map<String, String> parseQString(String str) throws UnsupportedEncodingException {
+
+		Map<String, String> map = new HashMap<String, String>();
+		int len = str.length();
+		StringBuilder temp = new StringBuilder();
+		char curChar;
+		String key = null;
+		boolean isKey = true;
+		boolean isOpen = false;// 值里有嵌套
+		char openName = 0;
+		if (len > 0) {
+			for (int i = 0; i < len; i++) {// 遍历整个带解析的字符串
+				curChar = str.charAt(i);// 取当前字符
+				if (isKey) {// 如果当前生成的是key
+
+					if (curChar == '=') {// 如果读取到=分隔符
+						key = temp.toString();
+						temp.setLength(0);
+						isKey = false;
+					} else {
+						temp.append(curChar);
+					}
+				} else {// 如果当前生成的是value
+					if (isOpen) {
+						if (curChar == openName) {
+							isOpen = false;
+						}
+
+					} else {// 如果没开启嵌套
+						if (curChar == '{') {// 如果碰到,就开启嵌套
+							isOpen = true;
+							openName = '}';
+						}
+						if (curChar == '[') {
+							isOpen = true;
+							openName = ']';
+						}
+					}
+					if (curChar == '&' && !isOpen) {// 如果读取到&分割符,同时这个分割符不是值域,这时将map里添加
+						putKeyValueToMap(temp, isKey, key, map);
+						temp.setLength(0);
+						isKey = true;
+					} else {
+						temp.append(curChar);
+					}
+				}
+
+			}
+			putKeyValueToMap(temp, isKey, key, map);
+		}
+		return map;
+	}
+
+	private static void putKeyValueToMap(StringBuilder temp, boolean isKey, String key, Map<String, String> map)
+			throws UnsupportedEncodingException {
+		if (isKey) {
+			key = temp.toString();
+			if (key.length() == 0) {
+				throw new RuntimeException("QString format illegal");
+			}
+			map.put(key, "");
+		} else {
+			if (key.length() == 0) {
+				throw new RuntimeException("QString format illegal");
+			}
+			map.put(key, temp.toString());
+		}
+	}
+
+}

+ 25 - 0
src/main/java/com/izouma/nineth/config/SandPayConfig.java

@@ -0,0 +1,25 @@
+package com.izouma.nineth.config;
+
+import cn.com.sandpay.cashier.sdk.CertUtil;
+import lombok.AllArgsConstructor;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import javax.annotation.PostConstruct;
+
+@Configuration
+@AllArgsConstructor
+@EnableConfigurationProperties({SandPayProperties.class})
+public class SandPayConfig {
+
+    private final SandPayProperties sandPayProperties;
+
+    @PostConstruct
+    public void init() {
+        try {
+            CertUtil.init(sandPayProperties.getSandCertPath(), sandPayProperties.getSignCertPath(), sandPayProperties.getSignCertPwd());
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+}

+ 16 - 0
src/main/java/com/izouma/nineth/config/SandPayProperties.java

@@ -0,0 +1,16 @@
+package com.izouma.nineth.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@Data
+@ConfigurationProperties(prefix = "sandpay")
+public class SandPayProperties {
+    private String url = "https://cashier.sandpay.com.cn/qr/api";
+    private String mid;
+    private String plMid;
+    private String signCertPath;
+    private String signCertPwd;
+    private String sandCertPath;
+    private String notifyUrl;
+}

+ 1 - 0
src/main/java/com/izouma/nineth/security/WebSecurityConfig.java

@@ -117,6 +117,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
                 .antMatchers("/showroom/get/**").permitAll()
                 .antMatchers("/testClass/**").permitAll()
                 .antMatchers("/appVersion/**").permitAll()
+                .antMatchers("/sandpay/**").permitAll()
                 // all other requests need to be authenticated
                 .anyRequest().authenticated().and()
                 // make sure we use stateless session; session won't be used to

+ 162 - 0
src/main/java/com/izouma/nineth/service/SandPayService.java

@@ -0,0 +1,162 @@
+package com.izouma.nineth.service;
+
+import cn.com.sandpay.cashier.sdk.*;
+import com.alibaba.fastjson.JSONObject;
+import com.izouma.nineth.config.SandPayProperties;
+import com.izouma.nineth.domain.Order;
+import com.izouma.nineth.enums.OrderStatus;
+import com.izouma.nineth.exception.BusinessException;
+import com.izouma.nineth.repo.OrderRepo;
+import com.izouma.nineth.utils.DateTimeUtils;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+@Service
+@AllArgsConstructor
+@Slf4j
+public class SandPayService {
+    private final OrderRepo         orderRepo;
+    private final SandPayProperties sandPayProperties;
+
+    public JSONObject requestServer(JSONObject header, JSONObject body, String reqAddr) {
+
+        Map<String, String> reqMap = new HashMap<String, String>();
+        JSONObject reqJson = new JSONObject();
+        reqJson.put("head", header);
+        reqJson.put("body", body);
+        String reqStr = reqJson.toJSONString();
+        String reqSign;
+        // 签名
+        try {
+            reqSign = new String(Base64.encodeBase64(CryptoUtil.digitalSign(reqStr.getBytes(StandardCharsets.UTF_8), CertUtil.getPrivateKey(), "SHA1WithRSA")));
+        } catch (Exception e) {
+            log.error(e.getMessage());
+            return null;
+        }
+        //整体报文格式
+        reqMap.put("charset", "UTF-8");
+        reqMap.put("data", reqStr);
+        reqMap.put("signType", "01");
+        reqMap.put("sign", reqSign);
+        reqMap.put("extend", "");
+
+        String result;
+        try {
+            log.info("请求报文:\n" + JSONObject.toJSONString(reqJson, true));
+            result = HttpClient.doPost(reqAddr, reqMap, 30000, 30000);
+            result = URLDecoder.decode(result, StandardCharsets.UTF_8);
+        } catch (IOException e) {
+            log.error(e.getMessage());
+            return null;
+        }
+
+        Map<String, String> respMap = SDKUtil.convertResultStringToMap(result);
+        String respData = respMap.get("data");
+        String respSign = respMap.get("sign");
+
+        // 验证签名
+        boolean valid;
+        try {
+            valid = CryptoUtil.verifyDigitalSign(respData.getBytes(StandardCharsets.UTF_8), Base64.decodeBase64(respSign), CertUtil.getPublicKey(), "SHA1WithRSA");
+            if (!valid) {
+                log.error("verify sign fail.");
+                return null;
+            }
+            log.info("verify sign success");
+            JSONObject respJson = JSONObject.parseObject(respData);
+            if (respJson != null) {
+                log.info("响应码:[" + respJson.getJSONObject("head").getString("respCode") + "]");
+                log.info("响应描述:[" + respJson.getJSONObject("head").getString("respMsg") + "]");
+                log.info("响应报文:\n" + JSONObject.toJSONString(respJson, true));
+            } else {
+                log.error("服务器请求异常!!!");
+            }
+            return respJson;
+
+        } catch (Exception e) {
+            log.error(e.getMessage());
+            return null;
+        }
+    }
+
+    public JSONObject requestAlipay(String orderId, BigDecimal amount, String subject, String desc,
+                                    LocalDateTime timeout, String extend) {
+
+        JSONObject header = new JSONObject();
+        header.put("version", "1.0");            //版本号
+        header.put("method", "sandpay.trade.precreate");   //接口名称:统一下单并支付
+        header.put("productId", "00000006");     //产品编码
+        header.put("mid", sandPayProperties.getMid());     //商户号
+        header.put("accessType", "1");           //接入类型设置为普通商户接入
+        header.put("channelType", "07");         //渠道类型:07-互联网   08-移动端
+        header.put("reqTime", new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()));  //请求时间
+
+        DecimalFormat df = new DecimalFormat("000000000000", DecimalFormatSymbols.getInstance(Locale.US));
+
+        JSONObject body = new JSONObject();
+        body.put("payTool", "0401");             //支付工具: 固定填写0401
+        body.put("orderCode", orderId);          //商户订单号
+        body.put("totalAmount", df.format(amount.multiply(new BigDecimal("100"))));  //订单金额 12位长度,精确到分
+        //body.put("limitPay", "5");             //限定支付方式 送1-限定不能使用贷记卡	送4-限定不能使用花呗	送5-限定不能使用贷记卡+花呗
+        body.put("subject", subject);            //订单标题
+        body.put("body", desc);                  //订单描述
+        body.put("txnTimeOut", DateTimeUtils.format(timeout, "yyyyMMddHHmmss"));    //订单超时时间
+        body.put("notifyUrl", sandPayProperties.getNotifyUrl());                         //异步通知地址
+        body.put("bizExtendParams", "");         //业务扩展参数
+        body.put("merchExtendParams", "");       //商户扩展参数
+        body.put("extend", extend);              //扩展域
+
+        return requestServer(header, body, "https://cashier.sandpay.com.cn/qr/api/order/create");
+    }
+
+    public JSONObject query(String orderId) {
+        JSONObject header = new JSONObject();
+        header.put("version", "1.0");                     //版本号
+        header.put("method", "sandpay.trade.query");      //接口名称:订单查询
+        header.put("productId", "00000006");              //产品编码
+        header.put("mid", sandPayProperties.getMid());    //商户号
+        header.put("accessType", "1");                    //接入类型设置为普通商户接入
+        header.put("channelType", "07");                  //渠道类型:07-互联网   08-移动端
+        header.put("reqTime", new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()));    //请求时间		
+
+        JSONObject body = new JSONObject();
+        body.put("orderCode", orderId);
+        body.put("extend", "");                            //扩展域
+
+        return requestServer(header, body, "https://cashier.sandpay.com.cn/qr/api/order/query");
+    }
+
+
+    public String payOrder(Long orderId) {
+        Order order = orderRepo.findById(orderId).orElseThrow(new BusinessException("订单不存在"));
+        if (order.getStatus() != OrderStatus.NOT_PAID) {
+            throw new BusinessException("订单状态错误");
+        }
+        JSONObject extend = new JSONObject();
+        extend.put("type", "order");
+        extend.put("id", orderId);
+
+        JSONObject res = requestAlipay(orderId.toString(), order.getTotalPrice(), order.getName(), order.getName(),
+                order.getCreatedAt().plusMinutes(3), extend.toJSONString());
+        if (res == null)
+            throw new BusinessException("下单失败,请稍后再试");
+
+        return res.getJSONObject("body").getString("qrCode");
+    }
+}

+ 16 - 7
src/main/java/com/izouma/nineth/web/OrderPayController.java

@@ -9,10 +9,7 @@ import com.izouma.nineth.domain.Order;
 import com.izouma.nineth.enums.OrderStatus;
 import com.izouma.nineth.exception.BusinessException;
 import com.izouma.nineth.repo.OrderRepo;
-import com.izouma.nineth.service.AssetService;
-import com.izouma.nineth.service.GiftOrderService;
-import com.izouma.nineth.service.MintOrderService;
-import com.izouma.nineth.service.OrderService;
+import com.izouma.nineth.service.*;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import me.chanjar.weixin.common.error.WxErrorException;
@@ -37,20 +34,32 @@ public class OrderPayController {
     private final GiftOrderService giftOrderService;
     private final OrderRepo        orderRepo;
     private final MintOrderService mintOrderService;
+    private final SandPayService   sandPayService;
 
     @RequestMapping(value = "/alipay_h5", method = RequestMethod.GET)
     @ResponseBody
     public String payOrderAlipayH5(Long id, Model model) throws BaseAdaPayException {
-        return (String) orderService.payAdapay(id, "alipay_wap", null);
+//        return (String) orderService.payAdapay(id, "alipay_wap", null);
+        return sandPayService.payOrder(id);
     }
 
     @RequestMapping(value = "/alipay_wx", method = RequestMethod.GET)
     public String payOrderAlipayWx(Long id, Model model) throws BaseAdaPayException {
+//        Order order = orderRepo.findById(id).orElseThrow(new BusinessException("订单不存在"));
+//        if (order.getStatus() != OrderStatus.NOT_PAID) {
+//            return "redirect:/9th/store";
+//        }
+//        String payUrl = (String) orderService.payAdapay(id, "alipay_wap", null);
+//        model.addAttribute("payUrl", payUrl);
+//        model.addAttribute("orderId", id);
+//        return "AlipayHtml";
+
+
         Order order = orderRepo.findById(id).orElseThrow(new BusinessException("订单不存在"));
         if (order.getStatus() != OrderStatus.NOT_PAID) {
             return "redirect:/9th/store";
         }
-        String payUrl = (String) orderService.payAdapay(id, "alipay_wap", null);
+        String payUrl = sandPayService.payOrder(id);
         model.addAttribute("payUrl", payUrl);
         model.addAttribute("orderId", id);
         return "AlipayHtml";
@@ -65,7 +74,7 @@ public class OrderPayController {
     @RequestMapping(value = "/alipay_app", method = RequestMethod.GET)
     @ResponseBody
     public String payOrderAlipayApp(Long id, Model model) throws BaseAdaPayException {
-        return (String) orderService.payAdapay(id, "alipay", null);
+        return sandPayService.payOrder(id);
     }
 
     @RequestMapping(value = "/weixin_h5")

+ 93 - 0
src/main/java/com/izouma/nineth/web/SandPayController.java

@@ -0,0 +1,93 @@
+package com.izouma.nineth.web;
+
+import cn.com.sandpay.cashier.sdk.CertUtil;
+import cn.com.sandpay.cashier.sdk.CryptoUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.izouma.nineth.config.GeneralProperties;
+import com.izouma.nineth.enums.PayMethod;
+import com.izouma.nineth.event.OrderNotifyEvent;
+import com.izouma.nineth.service.OrderService;
+import com.izouma.nineth.service.SandPayService;
+import com.izouma.nineth.utils.SnowflakeIdWorker;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+
+@RestController
+@RequestMapping("/sandpay")
+@Slf4j
+@AllArgsConstructor
+public class SandPayController {
+
+    private SandPayService    sandPayService;
+    private SnowflakeIdWorker snowflakeIdWorker;
+    private GeneralProperties generalProperties;
+    private RocketMQTemplate  rocketMQTemplate;
+
+    @GetMapping(value = "/testpay", produces = "text/html")
+    private String testpay() {
+        JSONObject extend = new JSONObject();
+        extend.put("type", "order");
+        extend.put("id", 1);
+        JSONObject res = sandPayService.requestAlipay(snowflakeIdWorker.nextId() + "", new BigDecimal("0.01"),
+                "话费充值", "话费充值", LocalDateTime.now().plusMinutes(3), extend.toJSONString());
+        String qrCode = res.getJSONObject("body").getString("qrCode");
+        return "<html><body><a href=\"" + qrCode + "\">" + qrCode + "</a></body></html>";
+    }
+
+    @PostMapping("/notify")
+    public Object notifyOrder(HttpServletRequest req, HttpServletResponse resp) {
+        String data = req.getParameter("data");
+        String sign = req.getParameter("sign");
+        // 验证签名
+        boolean valid;
+        try {
+            valid = CryptoUtil.verifyDigitalSign(data.getBytes(StandardCharsets.UTF_8), Base64.decodeBase64(sign),
+                    CertUtil.getPublicKey(), "SHA1WithRSA");
+            if (!valid) {
+                log.error("verify sign fail.");
+                log.error("签名字符串(data)为:" + data);
+                log.error("签名值(sign)为:" + sign);
+            } else {
+                log.info("verify sign success");
+                JSONObject dataJson = JSONObject.parseObject(data);
+                if (dataJson != null) {
+                    log.info("通知业务数据为:" + JSONObject.toJSONString(dataJson, true));
+                    if ("000000".equals(dataJson.getJSONObject("head").getString("respCode"))) {
+                        JSONObject body = dataJson.getJSONObject("body");
+                        JSONObject extend = body.getJSONObject("extend");
+                        String type = extend.getString("type");
+                        Long id = extend.getLong("id");
+                        String payOrderCode = body.getString("payOrderCode");
+                        String orderCode = body.getString("orderCode");
+                        String bankserial = body.getString("bankserial");
+
+                        switch (type) {
+                            case "order":
+                                rocketMQTemplate.syncSend(generalProperties.getOrderNotifyTopic(),
+                                        new OrderNotifyEvent(id, PayMethod.ALIPAY, payOrderCode, System.currentTimeMillis()));
+                                break;
+                        }
+                    }
+                    return "respCode=000000";
+                } else {
+                    log.error("通知数据异常!!!");
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+}

+ 8 - 0
src/main/resources/application.yaml

@@ -174,6 +174,7 @@ general:
   broadcast-event-topic: broadcast-event-topic-dev
   register-group: register-group-dev
   register-topic: register-topic-dev
+  notify-server: true
 mychain:
   rest:
     bizid: a00e36c5
@@ -224,6 +225,13 @@ rocketmq:
   producer:
     group: my-producer-dev
     send-message-timeout: 30000
+sandpay:
+  url: https://cashier.sandpay.com.cn/qr/api
+  mid: 6888806043057
+  sign-cert-path: classpath:cert/6888806043057.pfx
+  sign-cert-pwd: 3edc#EDC
+  sand-cert-path: classpath:cert/sand.cer
+  notify-url: http://xiongzhu.frp.izouma.com/sandpay/notify
 ---
 
 spring:

BIN
src/main/resources/cert/6888806043057.cer


BIN
src/main/resources/cert/6888806043057.pfx


BIN
src/main/resources/cert/sand.cer


+ 9 - 0
src/test/java/com/izouma/nineth/CommonTest.java

@@ -66,6 +66,9 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.security.NoSuchAlgorithmException;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
 import java.util.List;
 import java.util.*;
 import java.util.concurrent.ExecutorService;
@@ -555,4 +558,10 @@ public class CommonTest {
         System.out.println(Integer.parseInt(str.toString()));
 
     }
+
+    @Test
+    public void testNumberFormat() {
+        DecimalFormat df = new DecimalFormat("000000", DecimalFormatSymbols.getInstance(Locale.US));
+        System.out.println(df.format(new BigDecimal("199.11")));       // prints: 001.0
+    }
 }

+ 31 - 0
src/test/java/com/izouma/nineth/service/SandPayServiceTest.java

@@ -0,0 +1,31 @@
+package com.izouma.nineth.service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.izouma.nineth.ApplicationTests;
+import com.izouma.nineth.utils.SnowflakeIdWorker;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class SandPayServiceTest extends ApplicationTests {
+    @Autowired
+    private SandPayService    sandPayService;
+    @Autowired
+    private SnowflakeIdWorker snowflakeIdWorker;
+
+    @Test
+    void requestAlipay() {
+        JSONObject jsonObject = sandPayService.requestAlipay(snowflakeIdWorker.nextId() + "", new BigDecimal("0.01"),
+                "话费充值", "话费充值", LocalDateTime.now().plusMinutes(2), "");
+        System.out.println(JSONObject.toJSONString(jsonObject, true));
+    }
+
+    @Test
+    public void testQuery() {
+        JSONObject jsonObject = sandPayService.query("312022040222001494481423679292");
+    }
+}