手搓微信支付Demo【套全参数即可跑通】

涉及目前我所在公司隐私,有些参数我会全部覆盖掉,里面都会加上注释的,兄弟们一定要注意养成看开发文档开发的习惯,微x官方对于他们的SDK的讲解还是比较透彻的,只不过没有直接就跑通的项目,大多是一些方法类辅助性的加签解签方法,客户端创建方法,证书的验证,至于微x给出的统一下单获取prepay_id的方法到是可以直接测试跑通,也能拿到这个预支付会话Id,但是要注意的是二次验签,小程序在吊起支付前你需要将你封装的pakage(由prepay_id组装而成)等参数用RSA算法进行再次加密,这个加密的算法在微x开发文档里也有示范代码。这个验证签名非常重要,后续的拉起支付的过程也会解签获取真正的sign如果前后的sign不一致就会报出验证签名失败!务必注重验证签名过程。至于支付之后的退单等操作其实你搞懂了支付的验签与解签流程,其他的就是对数据进行封装,格式转化,加密传输,校验参数,拉起请求等步骤。

那么我们进入正题:

我只复写了一个简单的微信授权登录,获取openId之后将openId传入获取prepay_id,关于微信登录获取openId的方法兄弟们看我前面的文章。

首先是微x小程序拉起支付的JS代码:

// pages/login/login.js
Page({

  /**
   * 页面的初始数据
   */
  data: {
      // code存放变量
      js_code:"",
      // 封装用户信息
      userInfo:"",
      // 获取的登录唯一凭证
      openid:"",
      // 临时登录的会话密钥,可以作为登录状态的记录信号
      session_key:"",
      result:[],
      timeStamp:"",
      nonce_str:"",
      package:"",
      signature:""
  },
  login(){
    var th = this;
    // 授权获取当前用户信息
    wx.getUserProfile({
      desc: '用于登录校验',
      success(res){
         console.log(res);
        //  接收这个响应信息里的用户信息
         var userInfo = res.userInfo;
         th.setData({
           userInfo: userInfo,
         })
      }
    })
    // 调用login接口获取临时交换凭证code
      wx.login({
        success: function(res){
          console.log(res.code);
          // 将获取的临时凭证存放起来
          th.setData({
            js_code: res.code
          });
        }
      });
      // 设置延时是先进行授权结束后再回调这个微信登录的openId换取函数
        setTimeout(function(){
        let code = th.data.js_code;
        wx.request({
            // 注意反引号,注意这种方式是不能上传的,微信为了保护小程序安全要求这类参数在后端实现,不要暴露在前端,大家可以看我前面文章给出的解决方案解决,这里为了测试我就简单的获取,没有多写。
            url: `https://api.weixin.qq.com/sns/jscode2session?appid=你的小程序appid&secret=你的小程序密钥&js_code=${code}&grant_type=authorization_code`,
            success(res){
             console.log(res);
              // 将获取的两个关键信息存储
              th.setData({
               openid: res.data.openid
              })
              console.log(th.data.openid);
            },
          })
        },2000)
        wx.showToast({
              title: '获取openId成功',
            })
  },
  // 拉起支付函数,先向后端发送调用统一下单接口需要的数据
    pay(){
      let th = this;
      wx.request({
        url: 'http://测试机的ip地址与端口/createOrder',
        method:"GET",
        header:{
          'content-type': 'application/json'
        },
        success(res){
          console.log(res.data);
          th.setData({
            nonce_str: res.data.nonceStr,
            signature: res.data.paySign,
            package: res.data.packages,
            timeStamp: res.data.timeStamp
          })
          setTimeout(function(){
            //拉起微信支付
            wx.requestPayment({
              "timeStamp": res.data.timeStamp,
              "nonceStr": res.data.nonceStr,
              "package": res.data.packages,
              "signType": "RSA",
              "paySign": res.data.paySign,
              "success":function(res){},
              "fail":function(res){
                console.log(res);
              },
              "complete":function(res){
                console.log(res);
              }
              })
          },2000)
        }
      })
     
  },
  payto(){
    wx.requestPayment({
      "timeStamp": this.data.timeStamp,
      "nonceStr": this.data.nonce_str,
      "package": this.data.package,
      "signType": "RSA",
      "paySign": this.data.signature,
      "success":function(res){},
      "fail":function(res){
        console.log(res);
      },
      "complete":function(res){
        console.log(res);
      }
      })
  },
  /**
   * 生命周期函数--监听页面加载
   */

  onLoad(options) {

  },
............省略...........
})

这里是wxml代码(示范demo前端随意点):

<!--pages/login/login.wxml-->
<button type="primary" open-type="getUserInfo" bindtap="login">
  授权微信登录
</button>
<image src="{{userInfo.avatarUrl}}" style="height: 40px;width: 40px;"></image>
<text>{{userInfo.nickName}}</text>
<button type="primary" bindtap="pay">下单</button>
<view style="margin-top: 20px;"></view>
<button type="primary" bindtap="payto">去付款</button>

其次是springboot的后端代码:

全局使用的参数封装一下(你可以在yml文件写,也可以用properties文件写,但是我习惯封装在java类里了,所以这里只是个象征,哪种方式都可以的):

public final class WxPayConfig {
    public final  static String appid = "你的小程序appid";
    public final  static String mchId = "你小程序关联的商户号";
    public final  static String mchSerialNo = "你这个商户号申请的API证书的序列号";
    public final  static String apiV3Key = "你给商户设置的APV3密钥";
    public static final String url_prex = "https://api.mch.weixin.qq.com/";
    public static final String charset = "UTF-8";
    public static final String getPrivateKey = "-----BEGIN PRIVATE KEY-----\n" +
           你的商户证书私钥文件的内容     +
            "-----END PRIVATE KEY-----\n";
    public static final String url = "v3/pay/transactions/jsapi";
}

下单工具类(包含加签,解签,随机数生成,统一下单请求获取prepay_id,二次加签等方法)

package com.pay.utils;
/*
    User: 黄林春
    Date: 2022/8/14
    Time: 周日
    Project_Name: WxPay
    */

import cn.hutool.core.util.StrUtil;
import cn.hutool.http.ContentType;
import cn.hutool.json.JSONUtil;
import net.sf.json.JSONObject;
import okhttp3.HttpUrl;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
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.BasicHeader;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;

import static com.pay.config.WxPayConfig.*;

public class Tools {

    /**
     * 微信支付下单
     * @param jsonStr 请求体 json字符串
     * @return 订单支付的参数
     * @throws Exception
     */
    public static String V3PayGet(String jsonStr) throws Exception {
        String body = "";
        //创建httpclient对象
        CloseableHttpClient client = HttpClients.createDefault();
        //创建post方式请求对象
        HttpPost httpPost = new HttpPost(url_prex + url);
        //装填参数
        StringEntity s = new StringEntity(jsonStr, charset);
        s.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE,
                "application/json"));
        //设置参数到请求对象中
        httpPost.setEntity(s);
        String post = getToken(HttpUrl.parse(url_prex + url), jsonStr);
        //设置header信息
        //指定报文头【Content-type】、【User-Agent】
        httpPost.setHeader("Content-type", "application/json");
        httpPost.setHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
        httpPost.setHeader("Accept", "application/json");
        httpPost.setHeader("Authorization",
                "WECHATPAY2-SHA256-RSA2048 " + post);
        //执行请求操作,并拿到结果(同步阻塞)
        CloseableHttpResponse response = client.execute(httpPost);
        //获取结果实体
        HttpEntity entity = response.getEntity();
        if (entity != null) {
            //按指定编码转换结果实体为String类型
            body = EntityUtils.toString(entity, charset);
        }
        EntityUtils.consume(entity);
        //释放链接
        response.close();
        //返回JSAPI支付所需的参数
        return JSONObject.fromObject(body).getString("prepay_id");
    }

    /**
     * 微信调起支付参数
     * @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 request
     * @param response
     * @param privateKey APV3秘钥
     */
    public static String notify(HttpServletRequest request, HttpServletResponse response, String privateKey) throws Exception {
        Map<String, String> map = new HashMap<>(12);
        String result = readData(request);
        // 需要通过证书序列号查找对应的证书,verifyNotify 中有验证证书的序列号
        String plainText = verifyNotify(result, privateKey);
        if (StrUtil.isNotEmpty(plainText)) {
            response.setStatus(200);
            map.put("code", "SUCCESS");
            map.put("message", "SUCCESS");
        } else {
            response.setStatus(500);
            map.put("code", "ERROR");
            map.put("message", "签名错误");
        }
        response.setHeader("Content-type", ContentType.JSON.toString());
        response.getOutputStream().write(JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8));
        response.flushBuffer();
        return JSONObject.fromObject(plainText).getString("out_trade_no");
    }

    /**
     * 生成组装请求头
     * @param url  请求地址
     * @param body 请求体
     * @return 组装请求的数据
     * @throws Exception
     */
    static String getToken(HttpUrl url, String body) throws Exception {
        String nonceStr = UUID.randomUUID().toString().replace("-", "");
        long timestamp = System.currentTimeMillis() / 1000;
        String message = buildMessage(url, timestamp, nonceStr, body);
        String signature = sign(message.getBytes(StandardCharsets.UTF_8));
        return "mchid=\"" + mchId + "\","
                + "nonce_str=\"" + nonceStr + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + mchSerialNo + "\","
                + "signature=\"" + signature + "\"";
    }

    /**
     * 生成签名
     * @param message  请求体
     * @return 生成base64位签名信息
     * @throws Exception
     */
    static String sign(byte[] message) throws Exception {
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(getPrivateKey());
        sign.update(message);
        return Base64.getEncoder().encodeToString(sign.sign());
    }

    /**
     * 组装签名加载
     * @param url       请求地址
     * @param timestamp 请求时间
     * @param nonceStr  请求随机字符串
     * @param body      请求体
     * @return 组装的字符串
     */
    static String buildMessage(HttpUrl url, long timestamp, String nonceStr, String body) {
        String canonicalUrl = url.encodedPath();
        if (url.encodedQuery() != null) {
            canonicalUrl += "?" + url.encodedQuery();
        }
        return "POST" + "\n"
                + canonicalUrl + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + body + "\n";
    }

    /**
     * 获取私钥
     * @return 私钥对象
     */
    static PrivateKey getPrivateKey() throws IOException {
        /*getPrivateKey静态定义的私钥路径,感觉没有字符串直接输进去好用,我就擅自改了*/
        //String content = Files.readString(Paths.get(getPrivateKey));
        try {
            String privateKey = getPrivateKey.replace("-----BEGIN PRIVATE KEY-----", "")
                    .replace("-----END PRIVATE KEY-----", "")
                    .replaceAll("\\s+", "");
            KeyFactory kf = KeyFactory.getInstance("RSA");
            return kf.generatePrivate(
                    new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持RSA", e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("无效的密钥格式");
        }
    }

    /**
     * 构造签名串
     * @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();
    }

    /**
     * v3 支付异步通知验证签名
     * @param body 异步通知密文
     * @param key  api 密钥
     * @return 异步通知明文
     * @throws Exception 异常信息
     */
    static String verifyNotify(String body, String key) throws Exception {
        // 获取平台证书序列号
        cn.hutool.json.JSONObject resultObject = JSONUtil.parseObj(body);
        cn.hutool.json.JSONObject resource = resultObject.getJSONObject("resource");
        String cipherText = resource.getStr("ciphertext");
        String nonceStr = resource.getStr("nonce");
        String associatedData = resource.getStr("associated_data");
        AesUtil aesUtil = new AesUtil(key.getBytes(StandardCharsets.UTF_8));
        // 密文解密
        return aesUtil.decryptToString(
                associatedData.getBytes(StandardCharsets.UTF_8),
                nonceStr.getBytes(StandardCharsets.UTF_8),
                cipherText
        );
    }

    /**
     * 处理返回对象
     * @param request 请求
     * @return 
     */
    static String readData(HttpServletRequest request) {
        BufferedReader br = null;
        try {
            StringBuilder result = new StringBuilder();
            br = request.getReader();
            for (String line; (line = br.readLine()) != null; ) {
                if (result.length() > 0) {
                    result.append("\n");
                }
                result.append(line);
            }
            return result.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

最后就是熟知的controller类了,这里的按照官方给出的预付单格式直接粘贴过来的,大家可以赢map先封装再转成json格式。

import com.pay.utils.Tools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import static com.github.wxpay.sdk.WXPayUtil.generateNonceStr;
import static com.pay.config.WxPayConfig.appid;
import static com.pay.config.WxPayConfig.mchId;

@Slf4j
@Controller
public class WxPayController {

    @GetMapping("/createOrder")
    @ResponseBody
    public Object createOrder() throws Exception {
//        String.format()拆分url补全需要的参数,用s%作为占位符
//        RestTemplate restTemplate = new RestTemplate(); springmvc的客户端对象可以实现访问网址获取返回
        String reqdata = "{"
                + "\"amount\": {"
                + "\"total\": 1 金额一分,"
                + "\"currency\": \"CNY\""
                + "},"
                + "\"mchid\": \""+mchId+"\","
                + "\"description\": \"商品描述\","
                + "\"notify_url\": \"回调网址,最好是能访问到的,别整自己的测试机,微信属于外网,除非搭建隧道,不然绝对不通\","
                + "\"payer\": {"
                + "\"openid\": \"可以直接复制过来你测试的微信账户的openId\"" + "},"
                + "\"out_trade_no\": \""+generateNonceStr()+"\","
                + "\"appid\": \""+appid+"\"" + "}";
        String prepay_id = Tools.V3PayGet(reqdata);
        return Tools.WxTuneUp(prepay_id, appid);
    }
}

 最后兄弟们遇到自己在学校的项目需要用到微x相关的功能,如果有问题的话评论区留言,我会及时回复大家的。(生产环境微x支付需要考虑的东西还有很多,所以千万不要尝试用如此简单且不安全的demo去做一个营业性的开发,建议考虑结合缓存与消息技术将支付流程化)。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ForestSpringH

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值