[Spring boot]原生Feign完成微信支付全流程(一)統一下單支付與支付回調



前言

本系列主要介紹如何用原生的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小時回覆。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章