微信支付統一下單前端支付簽名與支付回調
前言
本系列主要介紹如何用原生的Feign實現微信的整個支付流程。本文先介紹支付的第一步統一下單。
難點
1.將Feign的入參的Bean自動轉化爲xml來請求。
2.同理,接收返回的xml參數
3.微信簽名流程/字段
4.不影響項目中其他Feign接口使用
依賴
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>10.7.0</version>
</dependency>
<!--Fegin請求xml編碼、解碼器-->
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-jaxb</artifactId>
<version>8.18.0</version>
</dependency>
<!--微信回調接口xml解析用-->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.8.8</version>
</dependency>
此外,lombok也是必須的,如果不使用自行轉換方法即可。
微信支付流程(常用)
具體代碼
feign接口入參bean UnifiedOrderDto.java
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
/**
* Created by zrc on 2020-05-18.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class UnifiedOrderDto {
private String appid;// 小程序ID
private String mch_id;// 商戶號
private String nonce_str;// 隨機字符串
private String sign_type;//簽名類型
private String sign;// 簽名
private String body;// 商品描述
private String out_trade_no;// 商戶訂單號
private Long total_fee;// 標價金額 ,單位爲分
private String spbill_create_ip;// 終端IP
private String notify_url;// 通知地址
private String trade_type;// 交易類型
private String openid;//用戶標識
private String time_start;
private String time_expire;
}
feign接口接收參數bean OrderReturnInfoDto.java
import lombok.Data;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
/**
* Created by zrc on 2020-05-18.
*/
@Data
@XmlRootElement(name = "xml")//必須有,沒有無法轉換
@XmlAccessorType(XmlAccessType.FIELD)
public class OrderReturnInfoDto {
private String return_code;
private String return_msg;
private String result_code;
private String appid;
private String mch_id;
private String nonce_str;
private String sign;
private String prepay_id;
private String trade_type;
}
Feign 接口 WeChatClient.java
import feign.Headers;
import feign.RequestLine;
/**
* Created by zrc on 2020-04-28.
*/
//這裏不需要@FeignClient註解,使用的原生
public interface WeChatClient {
@Headers({"Content-Type: text/xml;charset=utf-8"})
@RequestLine("POST /pay/unifiedorder")
OrderReturnInfoDto unifiedOrder(UnifiedOrderDto dto);
}
簽名用到的工具類:
SignUtil
@Slf4j
public class SignUtil {
private static final String SIGN_FIELD = "sign";
public static final String ALGORITHM = "AES/CBC/PKCS7Padding";
public static String sign(Map<String, String> param, String key) {
String prestr = SignUtil.createLinkString(param);
String sign = "";
try {
sign = byte2hex(Md5Util.encryptMD5(prestr + key));
} catch (IOException e) {
e.printStackTrace();
}
return sign;
}
public static String getMD5(Object obj) {
return getMD5(toString(obj), true);
}
/**
* 獲取MD5值
*
* @param obj
* @param is32
* @return
*/
public static String getMD5(Object obj, boolean is32) {
String result = "";
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(toString(obj).getBytes());
byte b[] = md.digest();
int i;
StringBuffer buf = new StringBuffer("");
for (int offset = 0; offset < b.length; offset++) {
i = b[offset];
if (i < 0)
i += 256;
if (i < 16)
buf.append("0");
buf.append(Integer.toHexString(i));
}
result = buf.toString();
} catch (NoSuchAlgorithmException e) {
System.out.println(e);
}
if (is32) {
return result;
} else {
return result.substring(8, 24);
}
}
/**
* 將任意對象轉換成字符串
*
* @param obj
* @return
*/
public static String toString(Object obj) {
if (obj == null || "".equals(obj)) {
return "";
} else {
return obj.toString();
}
}
public static String sign(Object o, String key) {
String sign = null;
Map<String, String> params = new HashMap<>(16);
try {
Field[] ss = o.getClass().getDeclaredFields();
for (Field s : ss) {
s.setAccessible(true);
if (s.get(o) == null || s.get(o).equals("")) {
continue;
}
if (SIGN_FIELD.equals(s.getName())) {
continue;
}
params.put(s.getName(), s.get(o).toString());
}
String prestr = SignUtil.createLinkString(params) + "&key=" + key;
System.out.println("The url is : " + prestr);
sign = getMD5(prestr).toUpperCase();
Field signField = o.getClass().getDeclaredField(SIGN_FIELD);
signField.setAccessible(true);
signField.set(o, sign);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return sign;
}
/**
* 把數組所有元素排序,並按照“參數=參數值”的模式用“&”字符拼接成字符串
*
* @param params 需要排序並參與字符拼接的參數組
* @return 拼接後字符串
*/
public static String createLinkString(Map<String, String> params) {
return createLinkString(params, true);
}
/**
* 把數組所有元素排序,並按照“參數=參數值”的模式用“&”字符拼接成字符串
*
* @param params 需要排序並參與字符拼接的參數組
* @return 拼接後字符串
*/
public static String createLinkString(Map<String, String> params, boolean sort) {
List<String> keys = new ArrayList<>(params.keySet());
if (sort) {
Collections.sort(keys);
}
String prestr = "";
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = params.get(key);
if (i == keys.size() - 1) {
prestr = prestr + key + "=" + value;
} else {
prestr = prestr + key + "=" + value + "&";
}
}
return prestr;
}
public static String byte2hex(byte[] bytes) {
StringBuilder sign = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
sign.append(String.format("%02x", bytes[i]));
}
return sign.toString();
}
/**
* XML格式字符串轉換爲Map
*
* @param strXML XML字符串
* @return XML數據轉換後的Map
* @throws Exception
*/
public static Map<String, String> xmlToMap(String strXML) throws Exception {
try {
Map<String, String> data = new HashMap<String, String>();
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
org.w3c.dom.Document doc = documentBuilder.parse(stream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for (int idx = 0; idx < nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == Node.ELEMENT_NODE) {
org.w3c.dom.Element element = (org.w3c.dom.Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
try {
stream.close();
} catch (Exception ex) {
// do nothing
}
return data;
} catch (Exception ex) {
throw ex;
}
}
/**
* 判斷簽名是否正確
*
* @param key API密鑰
* @return 簽名是否正確
* @throws Exception
*/
public static boolean isSignatureValid(Map<String, String> data, String key) throws Exception {
if (!data.containsKey("sign")) {
return false;
}
String sign = data.get("sign");
data.remove("sign");
return sign(data, key).equals(sign);
}
public static String readData(HttpServletRequest request) {
BufferedReader br = null;
try {
StringBuilder e = new StringBuilder();
br = request.getReader();
String line = null;
while ((line = br.readLine()) != null) {
e.append(line).append("\n");
}
line = e.toString();
return line;
} catch (IOException var12) {
throw new RuntimeException(var12);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException var11) {
}
}
}
}
public static String getReqInfo(String reqInfo, String key) {
String result = null;
try {
byte[] reqInfoBytes = Base64Utils.decode(reqInfo);
String keyString = Md5Util.MD5(key).toLowerCase();
byte[] keyBytes = keyString.getBytes();
result = AES.decryptData(reqInfoBytes, keyBytes);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}
MD5工具類
public class Md5Util {
public final static String MD5(String s) {
char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
try {
byte[] btInput = s.getBytes();
// 獲得MD5摘要算法的 MessageDigest 對象
MessageDigest mdInst = MessageDigest.getInstance("MD5");
// 使用指定的字節更新摘要
mdInst.update(btInput);
// 獲得密文
byte[] md = mdInst.digest();
// 把密文轉換成十六進制的字符串形式
int j = md.length;
char str[] = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
}
catch (Exception e) {
e.printStackTrace();
return null;
}
}
private static String byteArrayToHexString(byte b[]) {
StringBuffer resultSb = new StringBuffer();
for (int i = 0; i < b.length; i++)
resultSb.append(byteToHexString(b[i]));
return resultSb.toString();
}
private static String byteToHexString(byte b) {
int n = b;
if (n < 0)
n += 256;
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
public static String MD5Encode(String origin, String charsetname) {
String resultString = null;
try {
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
if (charsetname == null || "".equals(charsetname))
resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
else
resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
}
catch (Exception exception) {
}
return resultString;
}
public static byte[] encryptMD5(String data) throws IOException {
byte[] bytes;
try {
MessageDigest md = MessageDigest.getInstance("MD5");
bytes = md.digest(data.getBytes("UTF-8"));
} catch (GeneralSecurityException gse) {
throw new IOException(gse.getMessage());
}
return bytes;
}
private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
}
Base64Utils
public class Base64Utils {
private static char[] base64EncodeChars = new char[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1',
'2', '3', '4', '5', '6', '7', '8', '9', '+', '/', };
private static byte[] base64DecodeChars = new byte[] { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4,
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26,
27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1,
-1, -1, -1 };
private Base64Utils() {
}
/**
* 將字節數組編碼爲字符串
*
* @param data
*/
public static String encode(byte[] data) {
StringBuffer sb = new StringBuffer();
int len = data.length;
int i = 0;
int b1, b2, b3;
while (i < len) {
b1 = data[i++] & 0xff;
if (i == len) {
sb.append(base64EncodeChars[b1 >>> 2]);
sb.append(base64EncodeChars[(b1 & 0x3) << 4]);
sb.append("==");
break;
}
b2 = data[i++] & 0xff;
if (i == len) {
sb.append(base64EncodeChars[b1 >>> 2]);
sb.append(base64EncodeChars[((b1 & 0x03) << 4) | ((b2 & 0xf0) >>> 4)]);
sb.append(base64EncodeChars[(b2 & 0x0f) << 2]);
sb.append("=");
break;
}
b3 = data[i++] & 0xff;
sb.append(base64EncodeChars[b1 >>> 2]);
sb.append(base64EncodeChars[((b1 & 0x03) << 4) | ((b2 & 0xf0) >>> 4)]);
sb.append(base64EncodeChars[((b2 & 0x0f) << 2) | ((b3 & 0xc0) >>> 6)]);
sb.append(base64EncodeChars[b3 & 0x3f]);
}
return sb.toString();
}
/**
* 解密
*
* @param str
*/
public static byte[] decode(String str) throws Exception {
byte[] data = str.getBytes("GBK");
int len = data.length;
ByteArrayOutputStream buf = new ByteArrayOutputStream(len);
int i = 0;
int b1, b2, b3, b4;
while (i < len) {
/* b1 */
do {
b1 = base64DecodeChars[data[i++]];
} while (i < len && b1 == -1);
if (b1 == -1) {
break;
}
/* b2 */
do {
b2 = base64DecodeChars[data[i++]];
} while (i < len && b2 == -1);
if (b2 == -1) {
break;
}
buf.write((b1 << 2) | ((b2 & 0x30) >>> 4));
/* b3 */
do {
b3 = data[i++];
if (b3 == 61) {
return buf.toByteArray();
}
b3 = base64DecodeChars[b3];
} while (i < len && b3 == -1);
if (b3 == -1) {
break;
}
buf.write(((b2 & 0x0f) << 4) | ((b3 & 0x3c) >>> 2));
/* b4 */
do {
b4 = data[i++];
if (b4 == 61) {
return buf.toByteArray();
}
b4 = base64DecodeChars[b4];
} while (i < len && b4 == -1);
if (b4 == -1) {
break;
}
buf.write(((b3 & 0x03) << 6) | b4);
}
return buf.toByteArray();
}
}
微信加密信息解密(小程序敏感數據解密、微信退款回調解密)
@Component
@Slf4j
public class AES {
public static boolean initialized = false;
private static final String CHARSET_NAME = "UTF-8";
private static final String AES_NAME = "AES";
public static final String ALGORITHM = "AES/ECB/PKCS7Padding";
/**
* AES解密
*
* @param content 密文
* @return
*/
public byte[] decrypt(byte[] content, byte[] keyByte, byte[] ivByte) throws InvalidAlgorithmParameterException {
initialize();
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
Key sKeySpec = new SecretKeySpec(keyByte, "AES");
cipher.init(Cipher.DECRYPT_MODE, sKeySpec, generateIV(ivByte));// 初始化
return cipher.doFinal(content);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
e.printStackTrace();
} catch (NoSuchProviderException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
public static String decryptData(byte[] content,byte[] keyByte) {
Cipher cipher = null;
initialize();
try {
cipher = Cipher.getInstance(ALGORITHM, "BC");
SecretKeySpec secretKey = new SecretKeySpec(keyByte, AES_NAME);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
return new String(cipher.doFinal(content));
}catch (Exception ex){
log.error("數據解密失敗:{}",ex);
}
return StringUtils.EMPTY;
}
public static void initialize() {
if (initialized)//這裏進行初始化,全局一次
return;
Security.addProvider(new BouncyCastleProvider());
initialized = true;
}
// 生成iv
public static AlgorithmParameters generateIV(byte[] iv) throws Exception {
AlgorithmParameters params = AlgorithmParameters.getInstance("AES");
params.init(new IvParameterSpec(iv));
return params;
}
}
調用方法WeChatOrderServices
@Service
@Slf4j
public class WeChatOrderServices {
public OrderReturnInfoDto unifiedCreateOrder(UnifiedOrderPreDto reqData) {
Long currentTimeMillis = System.currentTimeMillis();
Date now = new Date(currentTimeMillis);
Date expire = new Date(currentTimeMillis + 86400000);
SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss");
String nowStr = formatter.format(now);
String expireStr = formatter.format(expire);
String nonce_str = RandomString.make(32);
UnifiedOrderDto unifiedOrderDto = UnifiedOrderDto.builder()
.body("店鋪名-商品類名")//reqData.getStoreName() + "-" + reqData.getProductGroup()
.mch_id("商戶號")
.appid("appid")
.nonce_str(nonce_str)
.notify_url("支付回調地址")
.spbill_create_ip("支付方IP地址")
.out_trade_no("訂單號")
.total_fee("總支付金額(單位分)")
.trade_type("JSAPI")
.openid("支付方openID")
.time_start(nowStr)
.time_expire(expireStr)
.sign_type("MD5")
.build();
SignUtil.sign(unifiedOrderDto, "商戶key");
JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder()
.build();
WeChatClient weChatClient = Feign.builder()
.decoder(new JAXBDecoder(jaxbFactory))
.encoder(new JAXBEncoder(jaxbFactory))
.contract(new feign.Contract.Default())//申明使用原生
.logger(new Slf4jLogger())
.target(WeChatClient.class, WXPayConstants.DOMAIN_API3);
return weChatClient.unifiedOrder(unifiedOrderDto);
}
}
前端支付簽名
//這裏全爲必填簽名字段,非常重要,請注意字段名稱和之前不同,且包含關鍵字package,所以只能通過map去簽名。
Map<String, String> signMap = new HashMap<>();
signMap.put("nonceStr", "隨機字符串");
signMap.put("appId", "appId");
signMap.put("package", "package");
signMap.put("signType", "signType");
signMap.put("timeStamp", "timeStamp");
String sign = SignUtil.doSign(signMap, "商戶key");
signMap.put("sign", sign);
//將signMap傳給前端,前端即可吊起支付
支付成功回調
@RestController
@RequestMapping("/wx")
public class WeChatOrderController {
@PostMapping(value = "/pay/callback")
public void wxProPayNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
String xmlMsg = SignUtil.readData(request);
Map<String, String> resultMap = SignUtil.xmlToMap(xmlMsg);
if (resultMap.get("return_code").equals("SUCCESS")) {
Boolean isSignatureValid = SignUtil.isSignatureValid(resultMap, "商戶key");
if (isSignatureValid) {//安全起見校驗
String orderNo = resultMap.get("out_trade_no");
//更新訂單信息
}
}
String result = "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
try {
response.getWriter().write(result);
} catch (IOException e) {
e.printStackTrace();
}
}
}
結語
此處是統一下單和全部的工具類,之後的內容不會附上工具類內容。統一下單算是支付流程裏面比較簡單的一環,畢竟它不需要證書。親測有效,如有問題歡迎評論,儘量24小時回覆。