微信支付,签名,回调v3版本

微信最新版本的支付,v3,不用手动签名就可以发起支付,但是前端发起支付的时候需要签名,这时候就需要后端签名给前端使用,这里讲了支付,签名和回调,一站式完成。

支付流程:后端发起支付请求,生成一个预支付会话id,然后把相关数据返回给前端,前端发起支付,支付完之后微信调用回调接口,处理相关业务,结束。

开始上代码:

引入依赖

        
            com.github.wechatpay-apiv3
            wechatpay-apache-httpclient
            0.4.4
        

获取证书

    private WechatPayHttpClientBuilder getWechatPayHttpClientBuilder() throws IOException, GeneralSecurityException, HttpCodeException, NotFoundException {
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
            new FileInputStream(certUrl));

        CertificatesManager certificatesManager = CertificatesManager.getInstance();
        // 向证书管理器增加需要自动更新平台证书的商户信息
        certificatesManager.putMerchant(merchantId, new WechatPay2Credentials(merchantId,
            new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)), apiV3Key.getBytes(StandardCharsets.UTF_8));
        // ... 若有多个商户号,可继续调用putMerchant添加商户信息

        // 从证书管理器中获取verifier
        Verifier verifier = certificatesManager.getVerifier(merchantId);

        return WechatPayHttpClientBuilder.create()
            .withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
            .withValidator(new WechatPay2Validator(verifier));
    }

里面的参数说明一下:

certUrl:指的是下载的证书地址(apiclient_key.pem),这个文件的地址,开通微信支付的时候可以下载证书,里面有这个文件

merchantId: 商户id

merchantSerialNumber:商户序列号

apiV3Key:生成证书的时候需要设置一个密码,就是这个密码

下单:

    public WxPayResp payOrder(String orderId, Double amount, String userOpenId) {
        try {
            //获取证书私钥
            WechatPayHttpClientBuilder builder = getWechatPayHttpClientBuilder();

            // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签
            CloseableHttpClient httpClient = builder.build();
            HttpPost httpPost = new HttpPost(JS_API_URL);
            httpPost.addHeader("Accept", "application/json");
            httpPost.addHeader("Content-type", "application/json; charset=utf-8");

            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectMapper objectMapper = new ObjectMapper();
            //支付金额单位是:分
            int total = (int) (amount * 100);
            ObjectNode rootNode = objectMapper.createObjectNode();
            rootNode.put("mchid", merchantId)
                .put("appid", appId)
                .put("description", "微信支付")
                .put("notify_url", notifyUrl)//回调地址
                .put("out_trade_no", orderId);
            rootNode.putObject("amount")
                .put("total", total);
            rootNode.putObject("payer")
                .put("openid", userOpenId);//支付者的openId
            objectMapper.writeValue(bos, rootNode);

            httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
            CloseableHttpResponse response = httpClient.execute(httpPost);
            String bodyAsString = EntityUtils.toString(response.getEntity());
            //获取预支付会话id
            String prepay_id = JsonUtil.fromJson(bodyAsString, JSONObject.class).getString("prepay_id");

            //生成支付所需的参数
            String nonceStr = RandomUtil.getRandomToInviteCodeToNum(10);
            long timestamp = System.currentTimeMillis() / 1000;

            //生成签名,给前端使用,和后端请求微信端没有半点关系,主要给前端使用
            String token = getToken(timestamp, nonceStr, "prepay_id="+prepay_id);
        
            //封装前端支付所需的参数
            //这里非常注意预支付会话id 返回格式为:"prepay_id="+prepay_id,必须加上前面的prepay_id,否则前端调用支付的时候报没有填入total_fee
            WxPayResp resp = WxPayResp.builder()
                .appId(appId)
                .nonceStr(nonceStr)
                .pakeage("prepay_id="+prepay_id)
                .timestamp(timestamp)
                .signType("RSA")
                .paySign(token)
                .build();

            return resp;
        } catch (IOException e) {
            e.printStackTrace();
        } catch (GeneralSecurityException e) {
            e.printStackTrace();
        } catch (HttpCodeException e) {
            e.printStackTrace();
        } catch (NotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }

这里的签名千万要注意,这个签名不是用来后端请求微信端的签名,而是返回给前端使用的签名,所以签名规则也是前端的签名规则,接口文档是:
微信支付,签名,回调v3版本_第1张图片

一定要注意这点,这个坑我已经踩过了,希望朋友们别在踩了

剩下的具体签名方式:

    private String getToken( long timestamp, String nonceStr, String pakeage) {

        String message = buildMessage(timestamp, nonceStr, pakeage);
        String signature = null;
        try {
            signature = sign(message.getBytes("utf-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        return signature;
    }

    private String buildMessage( long timestamp, String nonceStr, String pakeage) {

        return appId + "\n"
            + timestamp + "\n"
            + nonceStr + "\n"
            + pakeage + "\n";
    }

    private String sign(byte[] message) {
        Signature sign = null;
        try {
            PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
                new FileInputStream(certUrl));
            sign = Signature.getInstance("SHA256withRSA");
            sign.initSign(merchantPrivateKey);
            sign.update(message);
            return Base64.getEncoder().encodeToString(sign.sign());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (SignatureException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }

 这样就完成了后端向微信发起支付,返回一个预支付会话id,然后前端拿到后端给的数据以及后端的签名,前端就可以发起支付了。剩下的就是回调了

回调逻辑---微信请求 回调接口,返回的数据是加密的,需要自行解密,才可以拿到相关数据

代码如下:

接收消息

    /**
     * 回调
     * @param request
     * @param response
     */
    public void wxMergePayNotify(HttpServletRequest request, HttpServletResponse response) {
        //给微信的回应
        Map result = new HashMap<>(2);
        //解密数据
        String data = this.payRequestDecryption(request);
        if (data == null){
            result.put("code","FAILED");
            result.put("message","失败");
            String json = JSON.toJSONString(result);
            PrintWriter out = null;
            try {
                out = response.getWriter();
            } catch (IOException e) {
                e.printStackTrace();
            }
            out.write(json);
            //return result;
        }
        log.info("微信支付处理后的数据data={}", data);
        JSONObject jsonObject = JSONObject.parseObject(data);
        //TODO .......业务逻辑处理
        try {
            bindingBusinessService.payResult(jsonObject);
        } catch (Exception e) {
            System.out.println("支付成功,逻辑处理失败---,支付返回的数据---"+jsonObject);
            e.printStackTrace();
        }
        log.info("微信支付回调成功");
        result.put("code","SUCCESS");
        result.put("message","成功");
        String json = JSON.toJSONString(result);
        PrintWriter out = null;
        try {
            out = response.getWriter();
        } catch (IOException e) {
            e.printStackTrace();
        }
        out.write(json);
    }

签名验证:

/**
     * 签名校验
     * url https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter5_1_13.shtml
     */
    public String verifySign(HttpServletRequest request) throws Exception {
        //检查header
        String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
        for (String headerName : headers) {
            if (request.getHeader(headerName) == null) {
                log.info("{} is null", headerName);
                return null;
            }
        }
        //检查时间
        String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
        Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
        if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
            log.info("超过应答时间");
            return null;
        }
        //获取微信返回的参数
        String data;
        try {
            data = request.getReader().lines().collect(Collectors.joining());
        } catch (IOException e) {
            log.error("获取微信V3回调参数失败",e);
            return null;
        }
        //校验签名
        String nonce = request.getHeader(WECHAT_PAY_NONCE);
        String message =  timestamp + "\n" + nonce + "\n" + data + "\n";
        String serial = request.getHeader(WECHAT_PAY_SERIAL);
        String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
        Verifier verifier = getVerifier();

        if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
            log.info("签名校验失败");
            return null;
        }
        return data;
    }

    private Verifier getVerifier() throws IOException, GeneralSecurityException, HttpCodeException, NotFoundException {
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
            new FileInputStream(certUrl));
        CertificatesManager certificatesManager = CertificatesManager.getInstance();
        certificatesManager.putMerchant(merchantId,
            new WechatPay2Credentials(merchantId,
                new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)),
            apiV3Key.getBytes(StandardCharsets.UTF_8));
        // 从证书管理器中获取verifier
        return certificatesManager.getVerifier(merchantId);
    }

验证签名,并解密数据:

    /**
     * v3支付回调数据校验
     */
    public String payRequestDecryption(HttpServletRequest request){
        //校验签名
        String data = null;
        try {
            data = this.verifySign(request);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (data == null){
            return null;
        }
        JSONObject jsonObject = JSONObject.parseObject(data);
        System.out.println(jsonObject);



        String eventType = jsonObject.getString("event_type");
        String resourceType = jsonObject.getString("resource_type");
        if (!Objects.equals(eventType,"TRANSACTION.SUCCESS") || !Objects.equals(resourceType,"encrypt-resource")){
            log.info("不是支付通知不处理:{}",data);
            return null;
        }
        //参数解密
        JSONObject resource = jsonObject.getJSONObject("resource");
        String ciphertext = resource.getString("ciphertext");
        String nonce = resource.getString("nonce");
        String associatedData = resource.getString("associated_data");
        AesUtil aesUtil = new AesUtil(apiV3Key.getBytes(StandardCharsets.UTF_8));
        String result = null;
        try {
            result = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),nonce.getBytes(StandardCharsets.UTF_8),ciphertext);
        } catch (GeneralSecurityException e) {
            log.error("微信v3解密异常",e);
        }
        System.out.println("解密之后的数据是:"+result);
        return result;
    }

至此,结束。

你可能感兴趣的:(微信,java)