【Java】微信小程序V3支付(后台)

目录

        相关官网文档

        1.需要的参数

        2.引入库

        3.用到的工具类

        4.支付下单实现

        5.支付回调


相关官网文档

接入前准备-小程序支付 | 微信支付商户平台文档中心

微信支付-JSAPI下单

获取平台证书列表-文档中心-微信支付商户平台

微信支付-支付通知API

1.需要的参数

# appId

wechat.appid=${WECHAT_APPID}

# 商户号

wechat.mchid=${WECHAT_MCHID}

# 证书序列号

wechat.mch.certno=${WECHAT_CERTNO}

# APIv3密钥

wechat.pay.api-v3-key=${WECHAT_V3KEY}

证书下载,apiclient_cert.p12,放到resource目录下

需要的参数和证书,根据接入前准备官方文档获取。

2.引入库

<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-apache-httpclient</artifactId>
    <version>0.2.2</version>
</dependency>

3.用到的工具类

import com.wechat.pay.contrib.apache.httpclient.auth.AutoUpdateCertificatesVerifier;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author Grace
 */
@Component
public class KeyPairFactory {
    @Value("${wechat.mchid}")
    private String MCHID;

    @Value("${wechat.mch.certno}")
    private String CERTNO;

    @Value("${wechat.pay.api-v3-key}")
    private String V3KEY;
    
    private KeyStore store;
    private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private static final Random RANDOM = new SecureRandom();
    private final Object lock = new Object();
    
    private static final Map<String, Object> VERIFIER_MAP = new ConcurrentHashMap<>();
    
    /**
     * 获取公私钥.
     *
     * @param keyPath  API证书apiclient_cert.p12的classpath路径
     * @param keyAlias the key alias
     * @param keyPass  password
     * @return the key pair
     */
    public KeyPair createPKCS12(String keyPath, String keyAlias, 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(keyAlias);
            certificate.checkValidity();
            // 证书的序列号
//            String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase();
            // 证书的公钥
            PublicKey publicKey = certificate.getPublicKey();
            // 证书的私钥
            PrivateKey storeKey = (PrivateKey) store.getKey(keyAlias, pem);

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

    /**
     * V3  SHA256withRSA 签名.
     *
     * @param method       请求方法  GET  POST PUT DELETE 等
     * @param canonicalUrl 例如  https://api.mch.weixin.qq.com/v3/pay/transactions/app?version=1 ——> /v3/pay/transactions/app?version=1
     * @param timestamp    当前时间戳   因为要配置到TOKEN 中所以 签名中的要跟TOKEN 保持一致
     * @param nonceStr     随机字符串  要和TOKEN中的保持一致
     * @param body         请求体 GET 为 "" POST 为JSON
     * @param keyPair      商户API 证书解析的密钥对  实际使用的是其中的私钥
     * @return the string
     */
    @SneakyThrows
    public String requestSign(String method, String canonicalUrl, long timestamp, String nonceStr, String body, KeyPair keyPair)  {
        String signatureStr = Stream.of(method, canonicalUrl, String.valueOf(timestamp), nonceStr, body)
                .collect(Collectors.joining("\n", "", "\n"));
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(keyPair.getPrivate());
        sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));
        return Base64Utils.encodeToString(sign.sign());
    }

    @SneakyThrows
    public String awakenPaySign(String appid, long timestamp, String nonceStr, String body, KeyPair keyPair)  {
        String signatureStr = Stream.of(appid,String.valueOf(timestamp),  nonceStr, body)
                .collect(Collectors.joining("\n", "", "\n"));
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(keyPair.getPrivate());
        sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));
        return Base64Utils.encodeToString(sign.sign());
    }

    /**
     * 生成Token.
     *
     * @param mchId 商户号
     * @param nonceStr   随机字符串 
     * @param timestamp  时间戳
     * @param serialNo   证书序列号
     * @param signature  签名
     * @return the string
     */
    public String token(String mchId, String nonceStr, long timestamp, String serialNo, String signature) {
        final String TOKEN_PATTERN = "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\","
                + "timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\"";
        // 生成token
        return String.format(TOKEN_PATTERN,
                mchId,
                nonceStr, timestamp, serialNo, signature);
    }

    public String generateNonceStr() {
        char[] nonceChars = new char[32];
        for (int index = 0; index < nonceChars.length; ++index) {
            nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
        }
        return new String(nonceChars);
    }

    /**
     * 解密响应体.
     *
     * @param apiV3Key       API V3 KEY  API v3密钥 商户平台设置的32位字符串
     * @param associatedData  response.body.data[i].encrypt_certificate.associated_data
     * @param nonce          response.body.data[i].encrypt_certificate.nonce
     * @param ciphertext     response.body.data[i].encrypt_certificate.ciphertext
     * @return the string
     * @throws GeneralSecurityException the general security exception
     */
    public String decryptResponseBody(String apiV3Key, String associatedData, String nonce, String ciphertext) {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), "AES");
            GCMParameterSpec spec = new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8));

            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));
            byte[] bytes;
            try {
                bytes = cipher.doFinal(Base64Utils.decodeFromString(ciphertext));
            } catch (GeneralSecurityException e) {
                throw new IllegalArgumentException(e);
            }
            return new String(bytes, StandardCharsets.UTF_8);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new IllegalStateException(e);
        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * 构造验签名串.
     *
     * @param wechatpayTimestamp HTTP头 Wechatpay-Timestamp 中的应答时间戳。
     * @param wechatpayNonce     HTTP头 Wechatpay-Nonce 中的应答随机串
     * @param body               响应体
     * @return the string
     */
    public String responseSign(String wechatpayTimestamp, String wechatpayNonce, String body) {
        return Stream.of(wechatpayTimestamp, wechatpayNonce, body)
                .collect(Collectors.joining("\n", "", "\n"));
    }

    public AutoUpdateCertificatesVerifier getVerifier() {
        AutoUpdateCertificatesVerifier verifier = (AutoUpdateCertificatesVerifier) VERIFIER_MAP.get("verifier");
        Date notAfter = (Date) VERIFIER_MAP.get("notAfter");
        if(null == verifier || null == notAfter || notAfter.before(new Date())){
            // 加载证书
            KeyPair keyPair = createPKCS12("/apiclient_cert.p12",
                    "Tenpay Certificate", MCHID);
            // 加载官方自动更新证书
            verifier = new AutoUpdateCertificatesVerifier(new WechatPay2Credentials(MCHID,
                    new PrivateKeySigner(CERTNO, keyPair.getPrivate())),
                    V3KEY.getBytes(StandardCharsets.UTF_8));
            VERIFIER_MAP.put("verifier", verifier);
            VERIFIER_MAP.put("notAfter", verifier.getValidCertificate().getNotAfter());
        }
        return verifier;
    }
    
}

官方证书验证工具放到了全局变量中,

4.支付下单实现

    @ApiOperation(value = "支付", httpMethod = "POST")
    @RequestMapping(value = "/api/pay/order", method = RequestMethod.POST)
    @Transactional
    public PrePayResponse wxToPay(@Valid @RequestBody PayOrderRequest dto, HttpServletRequest request, HttpServletResponse response) throws Exception {
        PrePayResponse res = new PrePayResponse();
        // 用户登录校验
        ......
        // 获取用户(数据库中的实体类)
        WechatUser user = xxx.getUser();

        int fee = 0;
        // 得到小程序传过来的价格,注意这里的价格必须为整数,1代表1分,所以传过来的值必须*100;
        if (null != dto.getPrice()) {
            double price = dto.getPrice() * 100;
            fee = (int) price;

        }
        // 生成订单号,商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
        String OutTradeNo = DateUtil.getOrderNum() + String.valueOf(System.currentTimeMillis()).substring(4) + new Random().nextInt(999999999);

        // 保存订单信息
        PayOrder order = new PayOrder();
        order.setUser(user);
        order.setCreatedAt(new Date());
        order.setPrice(dto.getPrice());
        order.setTradeNo(OutTradeNo);
        ......
        payOrderRepository.save(order);

        // 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 公网域名必须为https,如果是走专线接入,使用专线NAT IP或者私有回调域名可使用http
        String basePath  = request.getScheme() + "://" + request.getServerName() + ":" +
                request.getServerPort()  + request.getContextPath();
        String notify_url = basePath+"/api/anon/pay/notify";

        //请求参数
        cn.hutool.json.JSONObject json = new cn.hutool.json.JSONObject();
        json.set("appid", APPID);
        json.set("mchid", MCHID); // 商户号
        json.set("description", "test"); // 商品描述,必填
        json.set("out_trade_no", OutTradeNo);
        json.set("attach", "自定义参数"); // 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
        json.set("notify_url", notify_url); // 回调地址
        cn.hutool.json.JSONObject amount = new cn.hutool.json.JSONObject();
        amount.set("total", fee);
        amount.set("currency", "CNY");
        json.set("amount",amount); // 订单金额
        cn.hutool.json.JSONObject payer = new cn.hutool.json.JSONObject();
        payer.set("openid", user.getOpenId()); // 用户openId
        json.set("payer",payer); // 支付者

        //获取token
        String nonceStr = keyPairFactory.generateNonceStr();
        long timestamp = System.currentTimeMillis()/1000;
        res.setNonceStr(nonceStr);
        res.setTimeStamp(timestamp);
        String wechatToken = this.getToken(JSON.toJSONString(json), "POST",  "/v3/pay/transactions/jsapi", nonceStr, timestamp);

        Map<String,String> headers = new HashMap<String,String>();
        headers.put("Accept","application/json");
        headers.put("Content-Type","application/json; charset=utf-8");
        headers.put("Authorization",wechatToken);

        HttpResponse httpResponse = HttpRequest.post("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi")
                .headerMap(headers, false)
                .body(String.valueOf(json))
                .timeout(5 * 60 * 1000)
                .execute();
        String resultBody = httpResponse.body();

        com.alibaba.fastjson.JSONObject jsonObject = com.alibaba.fastjson.JSONObject.parseObject(resultBody);
        String prepayId = jsonObject.getString("prepay_id");
        if(StringUtils.isEmpty(prepayId)){
            order.setStatus(-1);
            res.setCode(Constants.SC_MSG);
            res.setMessage("支付失败");
            return res;
        }
        res.setPrepayId(prepayId);
        res.setSignType("RSA");
        // 加载证书
        KeyPair keyPair = keyPairFactory.createPKCS12("/apiclient_cert.p12",
                "Tenpay Certificate", MCHID);
        String paySign = keyPairFactory.awakenPaySign(APPID, timestamp, nonceStr, "prepay_id="+prepayId, keyPair);
        res.setPaySign(paySign);
        res.setCode(Constants.SC_OK);
        return res;
    }


    private String getToken(String body,String method,String url, String nonceStr, Long timestamp) {
	    //1.加载证书
	    KeyPair keyPair = keyPairFactory.createPKCS12("/apiclient_cert.p12", "Tenpay Certificate", MCHID);
	    //2.获取签名
	    String sign = keyPairFactory.requestSign(method, url, timestamp, nonceStr, body,keyPair);
	    //3.封装token
	    String token = keyPairFactory.token(MCHID, nonceStr, timestamp, CERTNO, sign);
	    return token;
    }

5.支付回调

    @ApiOperation(value = "支付回调地址")
    @RequestMapping(value = "/api/anon/pay/notify", produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    @Transactional
    public ResultResponse notify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        ResultResponse res = new ResultResponse();

        // 从请求头获取验签字段
        String Timestamp = request.getHeader("Wechatpay-Timestamp");
        String Nonce = request.getHeader("Wechatpay-Nonce");
        String Signature = request.getHeader("Wechatpay-Signature");
        String Serial = request.getHeader("Wechatpay-Serial");

        // 获取官方验签工具
        AutoUpdateCertificatesVerifier verifier = keyPairFactory.getVerifier();

        // 读取请求体的信息
        ServletInputStream inputStream = request.getInputStream();
        StringBuffer stringBuffer = new StringBuffer();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        String s;
        // 读取回调请求体
        while ((s = bufferedReader.readLine()) != null) {
            stringBuffer.append(s);
        }
        String s1 = stringBuffer.toString();
        Map requestMap = (Map) JSON.parse(s1);
        String resource = String.valueOf(requestMap.get("resource"));
        Map requestMap2 = (Map) JSON.parse(resource);
        String associated_data = requestMap2.get("associated_data").toString();
        String nonce = requestMap2.get("nonce").toString();
        String ciphertext = requestMap2.get("ciphertext").toString();

        //按照文档要求拼接验签串
        String VerifySignature = Timestamp + "\n" + Nonce + "\n" + s1 + "\n";
//        System.out.println("拼接后的验签串=" + VerifySignature);

        //使用官方验签工具进行验签
        boolean verify = verifier.verify(Serial, VerifySignature.getBytes(), Signature);

        //判断验签的结果
//        System.out.println("验签结果:"+verify);

        // 验签成功
//        System.out.println("验签成功后,开始进行解密");
        com.wechat.pay.contrib.apache.httpclient.util.AesUtil aesUtil = new AesUtil(V3KEY.getBytes());
        String aes = aesUtil.decryptToString(associated_data.getBytes(), nonce.getBytes(), ciphertext);

        com.alibaba.fastjson.JSONObject jsonObject = com.alibaba.fastjson.JSONObject.parseObject(aes);
        if ("SUCCESS".equals(jsonObject.getString("trade_state"))) {
            String out_trade_no = jsonObject.getString("out_trade_no");
            // 订单不为空
            if (!StringUtils.isEmpty(out_trade_no)) {
                //支付成功后的业务处理
                PayOrder payOrder = payOrderRepository.findByTradeNo(out_trade_no);
                payOrder.setStatus(1);
                ......
                res.setCode(Constants.SC_OK);
                return res;
            }
        } else {
            // 支付失败后的操作
        }
        res.setCode(Constants.SC_OK);
        return res;
    }

注意:回调接口不要被拦截

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值