涉及目前我所在公司隐私,有些参数我会全部覆盖掉,里面都会加上注释的,兄弟们一定要注意养成看开发文档开发的习惯,微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去做一个营业性的开发,建议考虑结合缓存与消息技术将支付流程化)。