Java 接入APIV3 微信支付 (一) JSAPI下单

Java接入微信支付APIV3

1.简介

为了在保证支付安全的前提下,带给商户简单、一致且易用的开发体验,推出了全新的微信支付API v3。
相较于之前的微信支付API,主要区别是:

  • 遵循统一的REST 的设计风格
  • 使用JSON作为数据交互的格式,不再使用XML
  • 使用基于非对称密钥的SHA256-RSA的数字签名算法,不再使用MD5或HMAC-SHA256
  • 不再要求HTTPS客户端证书
  • 使用AES-256-GCM,对回调中的关键信息进行加密保护

在接口规则中,你将了解到微信支付API v3的基础约定,如数据格式,参数兼容性,错误处理等。随后我们重点介绍了微信支付API v3新的认证机制。你可以跟随着签名指南,使用命令行或者你熟悉的编程语言,一步一步实践如何签名和验签。在最后的常见问题中,我们总结了商户接入过程中的各种常见和不常见的问题。
我们提供了API v3的Postman调试工具和某些开发语言的库。你可以通过我们的Github获取。
如果你有任何问题,欢迎访问我们的开发者社区

以上是微信支付官方的说明,APIV3版本的微信支付相较于之前版本的不同以及优化之处。

前言:

最近也是新增了一个微信支付的需求,之前写过微信支付的Native支付也就是扫码支付,本次接入的是JSAPI支付/小程序支付,接入的也是最新版的APIV3的版本,在接入过程中也是遇到了不少的坑,新的APIV3的接口文档在很多地方描述的也是不够清晰,也阅读过多篇CSDN大多也都是七零八碎的,特此编写了这篇博客,希望各位小伙伴少踩坑,也让自己不再重复踩坑,如果有描述不当错误之处,恳请各位大佬指正!
微信支付APIV3官方文档

2.JSAPI下单

注: 商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后再按Native、JSAPI、APP等不同场景生成交易串调起支付。

想要调用微信支付JSAPI下单,必须先设置HTTP头。微信支付商户API v3要求请求通过HTTP Authorization头来传递签名。
话不多说,贴代码:

1.这是请求JSAPI必须的参数,我写了一个属性类。
/**
 * Created by HCH on 2021/12/14.
 */
public class CreateOrderRequestVo {

    @ApiModelProperty(value = "appId,公众号名称,由商户传入", required = true)
    private String appid;

    @ApiModelProperty(value = "直连商户的商户号,由微信支付生成并下发", required = true)
    private String mchid;

    @ApiModelProperty(value = "商品描述", required = true)
    private String description;

    @ApiModelProperty(value = "商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一", required = true)
    private String out_trade_no;
    
    @ApiModelProperty(value = "异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 公网域名必须为https,如果是走专线接入,使用专线NAT IP或者私有回调域名可使用http", required = true)
    private String notify_url;
	
	@ApiModelProperty(value = "订单金额信息", required = true)
    private HashMap<String,Object> amount;

    @ApiModelProperty(value = "支付者信息", required = true)
    private HashMap<String,Object> payer;
}
2.通过调用下面wxDoPostJson(String url, String json)方法,访问JSAPI。

其中两个参数:
url(微信官方JSAPI_url):https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
json(将属性类CreateOrderRequestVo,转化成json格式的字符串):

贴一个我用来转json字符串格式的代码:

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
     * javaBean,list,array convert to json string
     *
     * @param obj javaBean,list,array
     * @return json字符串
     * @throws Exception 异常
     */
    public static String obj2json(Object obj) throws Exception {
        return objectMapper.writeValueAsString(obj);
    }
3.接下来就是创建Httpclient对象,来访问微信支付_JSAPI下单的请求了

看代码:

import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.util.*;
/**
 * Created by HCH on 2021/12/14.
 */
/**
     * 发送post请求,携带json类型数据
     * 如:{"name":"jok","age":"10"}
     *
     * @param url  请求地址
     * @param json json格式参数
     * @return
     */
    public static String wxDoPostJson(String url, String json) {
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        String resultString = "";
        try {
            // 创建Http Post请求
            HttpPost httpPost = new HttpPost(url);

            //获取签名请求头
            HashMap<String, String> heads = null;
            try {
                heads = WxSignV3Utils.getSignMap("POST", url, json);
            } catch (InvalidKeySpecException e) {
                e.printStackTrace();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            } catch (InvalidKeyException e) {
                e.printStackTrace();
            } catch (SignatureException e) {
                e.printStackTrace();
            }

            httpPost.addHeader("Authorization", heads.get("Authorization"));
            httpPost.addHeader("Accept", heads.get("Accept"));
            httpPost.addHeader("Content-Type",heads.get("Content-Type"));
            // 创建请求内容
            StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
            httpPost.setEntity(entity);
            // 执行http请求
            response = httpClient.execute(httpPost);
            resultString = EntityUtils.toString(response.getEntity(), getDefaultCharSet());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                response.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        return resultString;
    }

其中最关键的是设置HTTP Header 的设置。
微信官方的要求: 请求的签名信息通过HTTP头Authorization 传递。没有携带签名或者签名验证不通过的请求,都不会被执行,并返回401 Unauthorized 。

OK,我们继续,生成签名:

4.生成签名

注意,上面用到了 WxSignV3Utils.getSignMap(“POST”, url, json); 那我先贴出我的 WxSignV3Utils 工具类:

/**
 * Created by HCH on 2021/12/14.
 */
@Component
public class WxSignV3Utils {
//V3主商户ID
    private static String merchantId;

    //微信商户平台APIv3证书序列号
    private static String certificateSerialNo;

    //私钥(不要把私钥文件暴露在公共场合,如上传到Github,写在客户端代码等。)
    //cert_p12_Path
    private static String certP12Path;

	// 自己配置文件的mchid_路径
	@Value("${wx.mchid}")
    public void setMerchantId(String merchantId) {
        WxSignV3Utils.merchantId = merchantId;
    }

    @Value("${wx.payment.v3.certificate_Serial_No}")
    public void setCertificateSerialNo(String certificateSerialNo) {
        WxSignV3Utils.certificateSerialNo = certificateSerialNo;
    }
	
	@Value("${wx.payment.v3.cert_p12_Path}")
    public void setCertP12Path(String certP12Path) {
        WxSignV3Utils.certP12Path = certP12Path;
    }

/**
     * 使用方法
     * @param method 请求方法
     * @param url 请求url
     * @param body 请求内容
     * @return
     */
    public static HashMap<String, String> getSignMap(String method, String url, String body) throws InvalidKeySpecException, NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException, SignatureException {
        String authorization = getSign(method, url, body);

        HashMap<String, String> headsMap = new HashMap<>();
        headsMap.put("Authorization", authorization);
        headsMap.put("Content-Type", "application/json");
        headsMap.put("Accept", "application/json");

        return headsMap;
    }
	
	public static String getSign(String method, String url, String body) throws NoSuchAlgorithmException, SignatureException, InvalidKeySpecException, InvalidKeyException, UnsupportedEncodingException {
        return "WECHATPAY2-SHA256-RSA2048 " + getToken(method, HttpUrl.parse(url), body);
    }
    
	public static String getToken(String method, HttpUrl url, String body) throws UnsupportedEncodingException, SignatureException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
        String nonceStr = nonceString();
        long timestamp = System.currentTimeMillis() / 1000;
        String message = buildMessage(method, url, timestamp, nonceStr, body);
        String signature = sign(message.getBytes("utf-8"));
        return "mchid=\"" + merchantId + "\","
                + "nonce_str=\"" + nonceStr + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + certificateSerialNo + "\","
                + "signature=\"" + signature + "\"";
    }
	
	public static String sign(byte[] message) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, SignatureException {
        Signature sign = Signature.getInstance("SHA256withRSA");
        //sign.initSign(getPKCS8PrivateKey(privateKey));
        KeyPair pkcs12 = new KeyPairFactory().createPKCS12(certP12Path,merchantId);
        PrivateKey aPrivate = pkcs12.getPrivate();
        sign.initSign(aPrivate);

        sign.update(message);

        return Base64.getEncoder().encodeToString(sign.sign());
    }
	
	public static String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
        String canonicalUrl = url.encodedPath();
        if (url.encodedQuery() != null) {
            canonicalUrl += "?" + url.encodedQuery();
        }

        return method + "\n"
                + canonicalUrl + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + body + "\n";
    }
    
    public static String nonceString() {

        String currTime = String.format("%d", (long) System.currentTimeMillis() / 1000);

        String strTime = currTime.substring(8, currTime.length());

        Random random = new Random();
        int num = (int) (random.nextDouble() * (1000000 - 100000) + 100000);
        String code = String.format("%06d", num);

        String nonce_str = currTime.substring(2) + code;
        return nonce_str;

    }
}
/**
 * Created by HCH on 2021/12/14.
 */
public class KeyPairFactory {

    private KeyStore store;

    private final Object lock = new Object();

    /**
     * 获取公私钥.
     *
     * @param keyPath  the key path
     * @param keyPass  password
     * @return the key pair
     */
    public KeyPair createPKCS12(String keyPath,String keyPass) {
        ClassPathResource resource = new ClassPathResource(keyPath);
        char[] pem = keyPass.toCharArray();
        try {
            synchronized (lock) {
                if (store == null) {
                    synchronized (lock) {
                        store = KeyStore.getInstance("PKCS12");
                        store.load(resource.getInputStream(), pem);
                    }
                }
            }
            X509Certificate certificate = (X509Certificate) store.getCertificate("Tenpay Certificate");
            certificate.checkValidity();
            // 证书的序列号
            String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase();
            // 证书的公钥
            PublicKey publicKey = certificate.getPublicKey();
            // 证书的私钥
            PrivateKey storeKey = (PrivateKey) store.getKey("Tenpay Certificate", pem);

            return new KeyPair(publicKey, storeKey);

        } catch (Exception e) {
            throw new IllegalStateException("Cannot load keys from store: " + resource, e);
        }
    }

注意里面有两个属性,都是在商户申请微信支付成功以后会给你的。
获取步骤:登录微信支付商户平台,进入【账户中心】->【账户设置】->【API安全】
certificateSerialNo:微信商户平台APIv3证书序列号。
certP12Path:证书 apiclient_cert.p12 的路径。
什么是商户API证书?如何获取商户API证书?

当我们配置好这些需要的参数,和证书的时候就可以访问,微信支付_JSAPI下单了。

还是贴一下postman的接口测试格式吧
测试数据,仅供参考

4.生成JSAPI调起支付API参数

通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的前端JS方法调起公众号支付。
接口说明
适用对象: 直连商户

接口定义
此API无后台接口交互,需要将列表中的数据签名

  • 但是我们后台接口需要生成 前端调起 所需要的 接口参数。
    在这里插入图片描述
    OK,话不多说,贴代码:
import com.alibaba.fastjson.JSONObject;

// 调用方法
JSONObject paySign = WxSignV3Utils.WxTuneUp(prepayId, appid);

可以看到上面调用了 WxSignV3Utils 的 WxTuneUp() 方法。OK,继续贴代码,注意这是WxSignV3Utils类里面的方法:

/**
     * 微信调起支付参数
     * 返回参数如有不理解 请访问微信官方文档
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_1_4.shtml
     *
     * @param prepayId           微信下单返回的prepay_id
     * @param appId              应用ID(appid)
     * @return 当前调起支付所需的参数
     * @throws Exception
     */
    public static JSONObject WxTuneUp(String prepayId, String appId) throws Exception {
        String time = System.currentTimeMillis() / 1000 + "";
        String nonceStr = UUID.randomUUID().toString().replace("-", "");
        String packageStr = "prepay_id=" + prepayId;
        ArrayList<String> list = new ArrayList<>();
        list.add(appId);
        list.add(time);
        list.add(nonceStr);
        list.add(packageStr);
        //加载签名
        String packageSign = sign(buildSignMessage(list).getBytes());
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("appid", appId);
        jsonObject.put("timeStamp", time);
        jsonObject.put("nonceStr", nonceStr);
        jsonObject.put("packages", packageStr);
        jsonObject.put("signType", "RSA");
        jsonObject.put("paySign", packageSign);
        return jsonObject;
    }

/**
     * 构造签名串
     *
     * @param signMessage 待签名的参数
     * @return 构造后带待签名串
     */
    static String buildSignMessage(ArrayList<String> signMessage) {
        if (signMessage == null || signMessage.size() <= 0) {
            return null;
        }
        StringBuilder sbf = new StringBuilder();
        for (String str : signMessage) {
            sbf.append(str).append("\n");
        }
        return sbf.toString();
    }

OK,然后将生成的参数返回给前端就OK了!
在这里插入图片描述

  • 9
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
以下是使用 `com.github.wechatpay-apiv3` 库处理微信 H5 支付的 Java 代码示例: ```java import com.github.wechatpay.apiv3.WxPayApiV3; import com.github.wechatpay.apiv3.WxPayApiV3Config; import com.github.wechatpay.apiv3.model.notify.WxPayOrderNotifyResult; import com.github.wechatpay.apiv3.model.notify.WxPayOrderNotifyResult.NotifyResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class WeChatH5NotifyHandler { private static final String WECHAT_API_CERT_SERIAL_NUMBER = "YOUR_WECHAT_API_CERT_SERIAL_NUMBER"; private static final String WECHAT_API_CERTIFICATE_PATH = "path/to/your/wechat/api/certificate.pem"; public void handleNotify(HttpServletRequest request, HttpServletResponse response) throws IOException { try { // 创建微信支付 API 配置 WxPayApiV3Config config = new WxPayApiV3Config.Builder() .appId("your_app_id") .merchantId("your_merchant_id") .privateKeySerialNumber(WECHAT_API_CERT_SERIAL_NUMBER) .privateKeyPath(WECHAT_API_CERTIFICATE_PATH) .build(); // 创建微信支付 API 实例 WxPayApiV3 wxPayApiV3 = new WxPayApiV3(config); // 解析异步通知数据 WxPayOrderNotifyResult notifyResult = wxPayApiV3.parseOrderNotifyResult(request); // 验证签名 if (wxPayApiV3.verifySignature(notifyResult)) { // 签名验证成功 // 处理支付成功的逻辑 // ... // 返回成功响应给微信服务器 response.setStatus(HttpServletResponse.SC_OK); response.getWriter().write("SUCCESS"); } else { // 签名验证失败,返回失败响应给微信服务器 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().write("FAIL"); } } catch (Exception e) { e.printStackTrace(); // 返回失败响应给微信服务器 response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); response.getWriter().write("FAIL"); } } } ``` 在上述代码中,我们创建了一个名为 `WeChatH5NotifyHandler` 的类,其中的 `handleNotify` 方法用于处理微信 H5 支付的异步通知。该方法接收 `HttpServletRequest` 和 `HttpServletResponse` 对象作为参数,从请求中获取异步通知的数据,并进行相应的处理逻辑。 在 `handleNotify` 方法中,我们首先创建了一个 `WxPayApiV3Config` 对象,用于配置微信支付 API 的相关参数。其中,我们需要提供应用 ID(`appId`)、商户号(`merchantId`)、微信支付 API 证书的序列号(`privateKeySerialNumber`)以及证书的路径(`privateKeyPath`)。您需要将这些参数替换为您自己的值。 然后,我们使用 `WxPayApiV3` 实例来解析异步通知数据,并验证签名。如果签名验证成功,则表示支付成功,可以进行相应的处理逻辑,并返回成功响应给微信服务器。如果签名验证失败,则返回失败响应给微信服务器。 请注意,以上示例代码仅供参考,具体的实现可能因应用的需求而有所不同。您需要根据实际情况进行修改和完善。另外,在真实的项目中,请确保您已正确配置和保护微信支付 API 证书的私钥。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值