1.流程
首先准备微信支付统一配置,进入可能发起支付的页面时,将要支付的手机端网页url传入后台,返回四个参数,写进函数wx.config进行授权
注:这是前端调用微信的统一配置,对真正调起手机微信支付没有影响。如果使用的是小程序云开发静态网站托管的域名的网页,可以免鉴权直接跳任意合法合规小程序,调用 wx.config 时 appId 需填入非个人主体的已认证小程序,不需计算签名,timestamp、nonceStr、signature 填入非空任意值即可。
然后在真正要在手机端拉起支付页面时,先将openid传入后台,后台根据openid和其他参数向微信端统一下单接口发送请求,返回prepay_id,再根据结合其他参数传给手机前端,前端调用wx.chooseWxPay函数拉起微信支付界面
注:流程中有三个签名(图中的signature和paySign),授权的时候后台生成一个签名传给前台(第一个),后台向微信统一下单的时候生成一个传给微信(第二个),结合微信统一下单返回的信息,生成一个给手机端用来拉起微信支付(第三个,也叫微信支付的二次签名)
统一下单接口:https://api.mch.weixin.qq.com/pay/unifiedorder 调用weixin-java-pay里的 unifiedOrder 可以直接发起
2.具体代码
依赖:
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-miniapp</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>3.4.0</version>
</dependency>
2.1wx.config参数获取
前端传过参数:url
appid:通过微信公众号配置取得
timestamp:意思是当前毫秒数,可以通过代码获取:String.valueOf(System.currentTimeMillis() / 1000)
nonceStr:32位长度随机字符串,可以通过代码获取:WXPayUtil.generateNonceStr(); (wxpay-sdk里的方法)
signature:签名,首先通过access_token获取jsapi_ticket,然后结合jsapi_ticket,timestamp,nonceStr和传入的url,通过排序和加密生成的字符串
注:下面两部分是拿access_token和jsapi_ticket的代码,网上也有很多,属于微信统一配置,对拉起微信支付没有影响,不感兴趣的可以跳到2.2
得到access_token的代码:
向微信接口发送请求拿access_token:https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
==================================================================================================================================
private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
public static AccessToken getAccessToken(String appid,String appsecret) {
AccessToken token = new AccessToken();
String url = ACCESS_TOKEN_URL.replace("APPID", appid).replace("APPSECRET", appsecret);
JSONObject jsonObject = null;
try {
jsonObject = doGetStr(url);
} catch (ParseException e) {
log.info(e.toString());
} catch (IOException e) {
log.info(e.toString());
e.printStackTrace();
}
if(jsonObject!=null){
token.setToken(jsonObject.getString("access_token"));
token.setExpiresIn(jsonObject.getInt("expires_in"));
}
return token;
}
==================================
public static JSONObject doGetStr(String url) throws ParseException, IOException{
CloseableHttpClient client = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(url);
JSONObject jsonObject = null;
HttpResponse httpResponse = client.execute(httpGet);
HttpEntity entity = httpResponse.getEntity();
if(entity != null){
String result = EntityUtils.toString(entity,"UTF-8");
jsonObject = JSONObject.fromObject(result);
}
return jsonObject;
}
==================================================================================================================================
拿到access_token之后,向微信接口发送请求:https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" + accessToken + "&type=jsapi"; 拿jsapi_ticket
public static String getJsApiTicket(String accessToken) {
String jsapi_ticket = null;
try {
String urlticket = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" + accessToken + "&type=jsapi";
String responseText = httpGet(urlticket);
jsapi_ticket = null;
JSONObject object = JSONObject.fromObject(responseText);
if (object.containsKey("ticket")) {
jsapi_ticket = object.getString("ticket");
}
} catch (Exception e) {
log.info(e.toString());
}
return jsapi_ticket;
}
==================================================================================================================================
根据jsapi_ticket生成wx.config所需参数的代码:
public R payConfig(ServletRequest request) {
String url = request.getParameter("url");
String noncestr = WXPayUtil.generateNonceStr();
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
//获取签名
//获取token
String accessToken = getAccessToken(payProperties.getAppId(),properties.getConfigs().get(0).getSecret()).getToken();
//获取jsapi_ticket
String jsapi_ticket = getJsApiTicket(accessToken);
log.info("accessToken = " + accessToken);
log.info("jsapi_ticket = " + jsapi_ticket);
String signature = createAutoSign("jsapi_ticket",timestamp+"",noncestr,url);
//创建Map用于创建签名串
Map<String, Object> params = new HashMap<>(16);
params.put("timestamp", timestamp);
params.put("noncestr", noncestr);
params.put("url", url);
//得到签名再组装到Map里
params.put("signature", signature);
//传入对应的appId
params.put("appId", payProperties.getAppId());
System.out.println(params);
return R.ok().put("res",params);
}
==================================================================================================================================
根据传入的map生成授权签名的代码(第一个签名):
/**
* JSTicketURL模板
*/
private static final String JSAPISIGN = "jsapi_ticket=%s&noncestr=%s×tamp=%s&url=%s";
public static String createAutoSign(String jsapi_ticket,String timestamp,String noncestr,String url){
try{
String string1 = String.format(JSAPISIGN,jsapi_ticket,timestamp,noncestr,url);
// SHA1签名生成
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(string1.getBytes(Charset.forName("UTF-8")));
byte[] digest = md.digest();
return new String(Hex.encodeHex(digest));
}catch(Exception e){
log.info(e.toString());
return null;
}
}
首先拼接成 “jsapi_ticket=%s&noncestr=%s×tamp=%s&url=%s”的字符串,然后进行SHA1加密
==================================================================================================================================
2.2wx.chooseWxPay参数生成
统一下单:
wx.chooseWxPay直接调起微信支付接口,要获得所需参数分为两步,首先后台向微信统一下单,根据微信统一下单的返回信息,返回给手机端
前端传过参数:openid
微信统一下单需要参数:
body: 商品信息
openid: 前端传入的参数
out_trade_no: 交易单号,自己随机生成的字符串,可以通过下面的 generateOrderSN() 方法生成
total_fee:交易金额
sign_type:统一下单需求签名方式("MD5" 就行)
spbill_create_ip:手机端发起支付的ip地址,可以通过下面的 getIpAddress() 方法获取
nonce_str:随机生成的字符串(生成一个新的,别用上面的)
trade_type:JSAPI--JSAPI支付(或小程序支付)、NATIVE--Native支付、APP--app支付,MWEB--H5支付
notify_url:支付成功之后的回调访问地址,微信会向这个地址发送支付成功消息
sign:将上面所有参数放进一个map里,用 WXPayUtil.generateSignature 生成的字符串 //wxpay-sdk里封装的方法
统一下单代码:
==================================================================================================================================
String openid = request.getParameter("openid");
String body = "商品";
String out_trade_no = generateOrderSN();
int total_fee = 1;
String spbill_create_ip = getIpAddress(request);
String notify_url = payProperties.getCallback();
String trade_type = "JSAPI"; //JSAPI--JSAPI支付(或小程序支付)、NATIVE--Native支付、APP--app支付,MWEB--H5支
String noncestr = WXPayUtil.generateNonceStr();
String type = "MD5";
try{
Map<String, String> data = new HashMap<>();
data.put("body", body);
data.put("openid", openid);
data.put("out_trade_no", out_trade_no);
data.put("total_fee", "1");
data.put("sign_type", type);
data.put("spbill_create_ip", spbill_create_ip);
data.put("nonce_str", noncestr);
data.put("trade_type", trade_type);
data.put("notify_url", notify_url);
String sign = WXPayUtil.generateSignature(data, payProperties.getMchKey()); //wxpay-sdk里封装的方法,payProperties是微信配置文件
//用weixin-java-pay里封装的方法直接发起统一下单
WxPayUnifiedOrderRequest unif = new WxPayUnifiedOrderRequest();
unif.setBody(body);
unif.setOpenid(openid);
unif.setOutTradeNo(out_trade_no);
unif.setTotalFee(total_fee);
unif.setSignType(type);
unif.setSpbillCreateIp(spbill_create_ip);
unif.setNonceStr(noncestr);
unif.setTradeType(trade_type);
unif.setNotifyUrl(notify_url);
unif.setSign(sign);
//统一下单返回结果
WxPayUnifiedOrderResult res = this.wxService.unifiedOrder(unif);
String appidRes = payProperties.getAppId();//payProperties是微信配置,拿appid
String prepay_id = res.getPrepayId();//统一下单返回的
String st = String.valueOf(System.currentTimeMillis() / 1000);;
String str = WXPayUtil.generateNonceStr();
String packageRes = "prepay_id=" + prepay_id;
Map<String, String> dataRes = new HashMap<>();
dataRes.put("appId", appidRes);
dataRes.put("timeStamp", st);// “timeStamp”参与验签
dataRes.put("nonceStr", str);
dataRes.put("package", packageRes);
dataRes.put("signType", type);
String signRes = WXPayUtil.generateSignature(dataRes, payProperties.getMchKey());//payProperties是微信支付配置,拿MchKey
Map<String, String> resfin= new HashMap<>();
resfin.put("res","success");
resfin.put("timestamp",st);
resfin.put("nonceStr",str);
resfin.put("package",packageRes);
resfin.put("signType",type);
resfin.put("signature",signRes);
return resfin;
}catch(WxPayException payex){
log.info(payex.toString());
return R.ok().put("res","failed");
}catch(Exception e){
log.info(e.toString());
return R.ok().put("res","failed");
}
================
生成随机字符串的代码(可以用来当out_trade_no):
private final static String AB = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890_*|";
/**
* 生成唯一识别码
*/
public static String generateOrderSN() {
StringBuffer orderSNBuffer = new StringBuffer();
orderSNBuffer.append(System.currentTimeMillis());
orderSNBuffer.append(getRandomString());
return orderSNBuffer.toString();
}
/**
* 获取随机字符串
*/
private static String getRandomString() {
int len = 7;
StringBuilder sb = new StringBuilder(len);
Random rnd = new Random();
for (int i = 0; i < len; i++) {
sb.append(AB.charAt(rnd.nextInt(AB.length())));
}
return sb.toString();
}
===========================
根据 HttpServletRequest 获取请求端ip的代码
/**
* 获取ip
*/
public static String getIpAddress(HttpServletRequest request) {
if (null == request) {
return "UNKNOWN";
}
String ipAddress = request.getHeader("x-forwarded-for");
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
if ("unknown".equals(ipAddress) || ip == null) {
ipAddress = request.getRemoteAddr();
if (StringUtils.equalsIgnoreCase("127.0.0.1", ipAddress)
|| StringUtils.equalsIgnoreCase("0:0:0:0:0:0:0:1", ipAddress)) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
ipAddress = inet.getHostAddress();
} catch (UnknownHostException e) {
log.info("[StringUtil.getIpAddr]", e);
}
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
// "***.***.***.***".length() = 15
if (StringUtils.isNotBlank(ipAddress) && 15 < ipAddress.length()) {
int idx = ipAddress.indexOf(",");
if (-1 < idx) {
ipAddress = ipAddress.substring(0, idx);
}
}
return ipAddress;
}
===========================
最后一步:回调,微信支付成功之后,微信会向这个网址发送消息,可以进行插入数据库的操作
public String wxPayCallBack(HttpServletRequest request) throws Exception {
log.info("微信支付回调");
// 1.接收微信官方返回的支付结果
InputStream inputStream = request.getInputStream();
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String temp;
StringBuilder stringBuilder = new StringBuilder();
while ((temp = in.readLine()) != null) {
stringBuilder.append(temp);
}
in.close();
inputStream.close();
log.info("[AccServiceOrderServiceImpl.accOrderNotify]微信支付-统一下单结果通知(验签前),resp = {}", stringBuilder.toString());
// 2.微信结果验证
Map<String, String> resultMap = PayCommonUtil.accOrderNotifyVerify(stringBuilder.toString(),payProperties.getMchKey());
//resultMap里面包含了transaction_id等所有信息
//to do 插入数据库的逻辑,根据resultMap里的信息插入数据库
log.info("微信支付回调结束");
return "SUCCESS";
}
==================================================================================================================================
总结:
统一配置,前端发送url给后端,后端通过access_token拿到jsapi_ticket,再根据其他参数生成签名和其他数值,返给前端,前端调用wx.config实现微信统一配置
拉起支付,前端发送appid给后端,后端向微信的unifiedorder统一下单接口发送请求,根据微信的返回,将参数发送回前端,前端调用wx.chooseWxpay调起微信支付