前提条件
1、认证的微信公众号和微信小程序号(http://mp.weixin.qq.com/)
2、微信商户号(http://pay.weixin.qq.com/index.php/core/home/login?return_url=%2F)
3、带有公网ip的VPS 【端口映射用】,也可以使用其他方式映射
4、解析到公网ip的域名(公众号里面用到JS接口安全域名和网页授权域名)
5、映射知识参考这里:利用VPS服务器搭建一个FRP内网穿透服务和Web服务穿透
开发工具:微信开发者工具、Intellij idea
注册商户:
(注册过程比较繁琐,自行百度,需要营业执照)
需要设置的地方在商户平台 产品中心->AppID管理设置公众号对应的AppID
然后去 产品中心->开发配置 里面将支付目录设置为项目访问的根目录即可
(文档地址:http://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_3)
1.小程序里面的JSAPI支付
流程:
1、先下载官方的支付demo(http://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=11_1)
其中需要注意的地方是WXPay.java里面的这里:
默认加密方式用反了,需要修改成相反的方式。
2、赋值以下工具类和配置:
#######application.properties配置 ################
#小程序
keyAppID=wx***************
AppSecret=************************
#商户id
MchID=*********
#商户key(在微信支付商户网站自定义的32个字符的mchkey)
MchKey=************************
#终端ip,支持IPV4和IPV6两种格式的IP地址。调用微信支付API的机器IP(这里提供的是VPS的公网ip,其他映射方式需要自己试验)
spbill_create_ip=*.*.*.*
#异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数
notifyUrl=http://www.xxx.com/payment/receiveResultOfPay
/**
* @Description 统一返回类
**/public class ResultUtil implements Serializable {
private String code;
private String msg;
private Object data;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public ResultUtil() {
}
public ResultUtil(String code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}}
/**
* @Description 请求和返回的参数封装
**/public class ResParamsUtil {
public static JSONObject getBodyParamsJSON(httpervletRequest request) {
BufferedReader br = null;
try {
br = request.getReader();
String line = br.readLine();
if (line == null) {
return null;
} else {
StringBuilder ret = new StringBuilder();
ret.append(line);
while((line = br.readLine()) != null) {
ret.append('\n').append(line);
}
return JSONObject.parseObject(ret.toString());
}
} catch (IOException var4) {
throw new RuntimeException(var4);
}
}
public static String getBodyParamsXML(httpervletRequest request) {
BufferedReader br = null;
try {
br = request.getReader();
String line = br.readLine();
line= URLDecoder.decode(line,"UTF-8");
if (line == null) {
return null;
} else {
StringBuilder ret = new StringBuilder();
ret.append(line);
while((line = br.readLine()) != null) {
ret.append('\n').append(line);
}
return ret.toString();
}
} catch (IOException var4) {
throw new RuntimeException(var4);
}
}}
3、小程序端调用微信登录接口,返回code后,再用code发送到后台的接口,让后台去请求微信官方的接口换取带有openid的认证session(即authsession)
/* app.js */
App({
onLaunch: function () {
this.startLogin();//调用登录
},
startLogin:function(){
//登录
wx.login({
timeout:30000,
success: login_res =>{
//发送请求到后台
wx.request({
url: this.globalData.baseHost+'/user/getAuthSession',
method:'POST',
data:{
formData:JSON.stringify({
code:login_res.code,
})
},
success:function(codesession_res){
console.log("登录Code换取Session结果:",codesession_res)
if(codesession_res.data.code=='200'){
wx.setStorage({ key: 'openId', data: codesession_res.data.data });//存储到本地内存中
}else{
wx.setStorage({ key: 'opanId', data: null });
}
}
})
}
})
},
globalData: {
baseHost:'http://blogs.johngene.cn',
}})
Java后台的用code换取authsession接口:
import com.alibaba.fastjson.JSONObject;
import com.hongtai.clientapi.utils.ResParamsUtil;
import com.hongtai.clientapi.utils.ResultUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.httpervletRequest;
@Controller@RequestMapping("/user")public class UserController {
@Autowired
private RestTemplateBuilder restTemplateBuilder;
@Value("${AppID}")
private String appId;
@Value("${AppSecret}")
private String appSecret;
/**
* 根据前台调用的登录接口返回的code再次请求查询带有openid的authSession登录信息
**/
@PostMapping("/getAuthSession")
@ResponseBody
public ResultUtil getAuthSession(httpervletRequest request){
JSONObject formData= ResParamsUtil.getBodyParamsJSON(request).getJSONObject("formData");
String code=formData.getString("code");
if(code==null){
return new ResultUtil("1","失败",null);//返回自定义登录态
}
String url="http://api.weixin.qq.com/sns/jscode2session?appid="+appId+"&secret="+appSecret+"&js_code="+code+"&grant_type=authorization_code";
ResponseEntity responseEntity=restTemplateBuilder.build().getForEntity(url, String.class);//sessionJson带有openid和session_key,安全起见session_key不可返回给小程序
if(responseEntity.getStatusCodeValue()==200){
JSONObject entityJSON=JSONObject.parseObject(responseEntity.getBody().toString());
//获取到的两个有用的东西:openId和sessionKey,其中我只用到openId
String openId=entityJSON.getString("openid");
String sessionKey=entityJSON.getString("session_key");
if(openId!=null){
return new ResultUtil("200","登录成功",openId);//返回自定义登录态(openId理论上讲应存在后台,不返回给前端,只需要返回给前端一个自定义的userId,让前端使用userId查询openId)
}else{
return new ResultUtil("201","保存用户信息失败",null);//返回自定义登录态
}
}else{
return new ResultUtil("500","查询的登录信息失败",null);//返回自定义登录态
}
}}
4、调用支付的页面index.wxml代码:
<!-- index.wxml -->
<view>
<button type="primary" bindtap="startPay" style="margin-bottom:15px;">调用支付</button>
</view>
只有一个按钮:
index.js代码:
/* index.js */
const app = getApp()
Page({
//页面的初始数据
data: {},
//自定义:支付按钮事件
startPay:function(){
/*
小程序支付开发流程:
1、小程序内调用登录接口,获取到用户的openid,api参见公共api【小程序登录API】
2、商户server调用支付统一下单,api参见公共api【统一下单API】
3、商户server调用再次签名,api参见公共api【再次签名】
4、商户server接收支付通知,api参见公共api【支付结果通知API】
5、商户server查询支付结果,api参见公共api【查询订单API】
*/
let openId=wx.getStorageSync("openId") || [];
wx.request({
url: app.globalData.baseHost + '/payment/doUnifiedOrder',
header: {
"Content-Type": "application/json;charset=UTF-8"
},
data: {
formData:JSON.stringify({
openId : openId
})
},
method: 'POST',
dataType: 'json',
responseType: 'text',
success: function(res) {
console.log("服务端返回:",res);
var c=res.data;
wx.requestPayment({
timeStamp: res.data.data.timeStamp,
nonceStr: res.data.data.nonceStr,
package: res.data.data.package,
signType: res.data.data.signType,
paySign: res.data.data.paySign,
success(res) {
console.log("统一下单接口成功:",res);
},
fail(res) {
console.log("统一下单接口失败:",res);
}
});
},
fail: function(res) {
console.log("请求服务失败:",res);
},
complete: function(res) {},
});
},
//生命周期函数--监听页面加载
onLoad: function (options) {},
//生命周期函数--监听页面初次渲染完成
onReady: function () {},
//生命周期函数--监听页面显示
onShow: function () {},
//生命周期函数--监听页面隐藏
onHide: function () {},
//生命周期函数--监听页面卸载
onUnload: function () {},
//页面相关事件处理函数--监听用户下拉动作
onPullDownRefresh: function () {},
//页面上拉触底事件的处理函数
onReachBottom: function () {},
//用户点击右上角分享
onShareAppMessage: function () {}
})
自定义微信支付配置类MyConfig.java,继承自微信支付API Demo 中的WxPayConfig.java
import java.io.ByteArrayInputStream;
import java.io.InputStream;
/**
* 微信支付配置类
*/
public class MyConfig extends WXPayConfig {
private String appid;
private String mch_id;
private String mch_key;
private byte[] certData;
public MyConfig() throws Exception {
}
public MyConfig(String appid, String mch_id, String mch_key) {
this.appid = appid;
this.mch_id = mch_id;
this.mch_key = mch_key;
}
@Override
public String getAppID() {
return this.appid;
}
@Override
public String getMchID() {
return this.mch_id;
}
@Override
public String getKey() {
return this.mch_key;
}
@Override
public InputStream getCertStream() {
ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);
return certBis;
}
@Override
public int getHttpConnectTimeoutMs() {
return 8000;
}
@Override
public int getHttpReadTimeoutMs() {
return 10000;
}
@Override
IWXPayDomain getWXPayDomain() {
return new IWXPayDomain() {
@Override
public void report(String domain, long elapsedTimeMillis, Exception ex) {
}
@Override
public DomainInfo getDomain(WXPayConfig config) {
return new DomainInfo("api.mch.weixin.qq.com", false);
}
};
}
}
小程序JSAPI支付后台接口WxPayController.java:
import com.alibaba.fastjson.JSONObject;
import com.hongtai.clientapi.utils.ResParamsUtil;
import com.hongtai.clientapi.utils.ResultUtil;
import com.hongtai.clientapi.utils.wxpay.MyConfig;
import com.hongtai.clientapi.utils.wxpay.WXPay;
import com.hongtai.clientapi.utils.wxpay.WXPayConstants;
import com.hongtai.clientapi.utils.wxpay.WXPayUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.httpervletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
/**
* 小程序JSAPI支付
**/
@Controller
@RequestMapping("/payment")
public class WxPayController {
/**
支付流程
1、小程序内调用登录接口,获取到用户的openid,api参见公共api【小程序登录API】
2、商户server调用支付统一下单,api参见公共api【统一下单API】
3、商户server调用再次签名,api参见公共api【再次签名】
4、商户server接收支付通知,api参见公共api【支付结果通知API】
5、商户server查询支付结果,api参见公共api【查询订单API】
*/
@Value("${AppID}")
private String appid;
@Value("${AppSecret}")
private String appsecret;
@Value("${MchID}")
private String mch_id;
@Value("${MchKey}")
private String mch_key;
@Value("${notifyUrl}")
private String notifyUrl;
@Value("${spbill_create_ip}")
private String spbill_create_ip;
/**
* 后台下单接口(里面自动调用了微信统一下单接口)
**/
@RequestMapping(value = "/doUnifiedOrder", method = RequestMethod.POST)
@ResponseBody
public ResultUtil doUnifiedOrder(httpervletRequest request) {
JSONObject formData= ResParamsUtil.getBodyParamsJSON(request).getJSONObject("formData");
String openId=formData.getString("openId");
MyConfig config = null;
WXPay wxpay =null;
try {
config = new MyConfig(appid,mch_id,mch_key);
wxpay= new WXPay(config);
} catch (Exception e) {
e.printStackTrace();
}
//获取客户端的ip地址
//获取本机的ip地址
InetAddress addr = null;
try {
addr = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
String spbill_create_ip = addr.getHostAddress();//终端ip,支持IPV4和IPV6两种格式的IP地址。调用微信支付API的机器IP
int total_fee=1;//支付金额,需要使用字符串类型,否则后面的签名会失败,微信支付提交的金额是不能带小数点的,且是以分为单位
String body = "费用支付";//商品描述
String out_trade_no= WXPayUtil.generateNonceStr();//商户订单号
String nonceStr=WXPayUtil.generateNonceStr();//获取随机字符串
//统一下单接口参数
HashMap<String, String> data = new HashMap<>();
data.put("body", body);//商品描述
data.put("out_trade_no",out_trade_no);//商户订单号(随机生成)
data.put("total_fee", String.valueOf(total_fee));//支付金额
data.put("spbill_create_ip", spbill_create_ip);//终端ip,支持IPV4和IPV6两种格式的IP地址。调用微信支付API的机器IP
data.put("notify_url", notifyUrl);//异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数
data.put("trade_type","JSAPI");//交易类型
data.put("openid", openId);//用户openid,trade_type=JSAPI,此参数必传
data.put("nonce_str",nonceStr);
try {
Map<String, String> rMap = wxpay.unifiedOrder(data);//调用统一下单api
System.out.println("统一下单接口返回: " + rMap);
String return_code = rMap.get("return_code");
String result_code = rMap.get("result_code");
Long timeStamp = System.currentTimeMillis() / 1000;//获取时间戳
if ("SUCCESS".equals(return_code) && return_code.equals(result_code)) {
Map resultMap=new HashMap();
String prepayid = rMap.get("prepay_id");
resultMap.put("appId",appid);//appId,再次签名中的appId的i要大写不能写成appid
resultMap.put("timeStamp", timeStamp + "");//这边要将返回的时间戳转化成字符串,不然小程序端调用wx.requestPayment方法会报签名错误
resultMap.put("nonceStr", nonceStr);//随机字符串必须和统一下单接口使用的随机字符串相同
resultMap.put("package", "prepay_id="+prepayid);
resultMap.put("signType", WXPayConstants.MD5);//MD5或者HMAC-SHA256
String sign = WXPayUtil.generateSignature(resultMap, mch_key);//再次签名,这个签名用于小程序端调用wx.requesetPayment方法
String xml=WXPayUtil.generateSignedXml(resultMap,mch_key);
resultMap.put("paySign", sign);
System.out.println("生成的签名paySign : "+ sign);
return new ResultUtil("200","",resultMap);
}else{
return new ResultUtil("500","",null);
}
} catch (Exception e) {
e.printStackTrace();
return new ResultUtil("500","",null);
}
}
/**
* @Author zj
* @Description 接收成功后返回的支付结果
* @Date 2020/07/17 15:13:01
* @Param []
* @return void
**/
@RequestMapping(value = "/receiveResultOfPay")
@ResponseBody
public void receiveResultOfPay(httpervletRequest request){
System.out.println("支付成功!!!");
String xmlData=ResParamsUtil.getBodyParamsXML(request);
System.out.println(xmlData);
Map<String,String> map=null;
try {
map=WXPayUtil.xmlToMap(xmlData);
} catch (Exception e) {
e.printStackTrace();
}
//此处map就是接收到的支付成功的结果,同样的通知可能会多次,发送给商户系统。商户系统必须能够正确处理重复的通知
}
}
2.公众号里面的JSAPI支付
公众号调用该支付与小程序调用支付的不同之处在于小程序直接可以调用登录api获取code然后利用code去后台调用统一下单接口,而公众号的网页无法直接调用,需要通过JSSDK调用。
微信JSSDK开发文档地址:http://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html
微信支付文档地址:http://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_1
步骤如下:1、公众号开发配置
需要点击一下重置来获取AppSecret和ip白名单,白名单需要设置成测试环境下的公网ip(百度ip地址即可查看公网ip)和服务器对应的ip(上线后用到)
2、网页授权配置
在接口权限目录中找到 网页服务->网页授权->点击后面的修改按钮
3、关联商户号
关联成功后可以看到如下界面:
进入重点:
思路:
先获取到微信的网页授权,利用网页授权接口返回的code和state,用后台再次请求通过code换取网页授权access_token接口,完成授权后重定向到支付页面并将openid、appId等信息带过去,支付页面中用appid等信息获取签名后初始化JSSDK,请求后台的支付接口,利用后台的支付接口请求微信统一下单接口进行支付操作,后台等待支付成功消息。
简化思路:
网页授权:
第一步:用户同意授权,获取code
第二步:通过code换取网页授权access_token
调用JSSDK:
直接初始化JSSDK
调用支付:
直接调用后台支付接口,让后台完成操作
开始写代码:
工具类和上面小程序JSAPI支付里面的两个工具类以外还有一个签名用的工具类WxUtil.java
application.properties配置类如下:
#商户id
MchID=********
#商户key
MchKey=********
#终端ip,支持IPV4和IPV6两种格式的IP地址。调用微信支付API的机器IP(要与实际调用的机器相同)
spbill_create_ip=*.*.*.*
#异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数
notifyUrl=http://www.xxx.com/payment/receiveResultOfPay
#公众号Token
token=**********
#公众号appid
app_id=**********
app_secret=**********
签名工具类:WxUtil.java
import org.springframework.http.ResponseEntity;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
/**
* @Description 公众号的weixin工具类
* @Date 2020/07/28 15:05:25
* @Param
* @return
**/
public class WxUtils {
/**
* @Description 将字节转换为十六进制字符串
* @Date 2020/07/27 19:38:02
* @Param [mByte]
* @return java.lang.String
**/
public static String byteToHexStr(byte mByte) {
char[] Digit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
char[] tempArr = new char[2];
tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
tempArr[1] = Digit[mByte & 0X0F];
String s = new String(tempArr);
return s;
}
/**
* @Description 将数组中的内容进行字典排序也可以直接使用Arrays.sort(arr);
* @Date 2020/07/27 19:37:40
* @Param [a]
* @return void
**/
public static void sort(String a[]) {
for (int i = 0; i < a.length - 1; i++) {
for (int j = i + 1; j < a.length; j++) {
if (a[j].compareTo(a[i]) < 0) {
String temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
}
/**
* @Description 将字节数组转换为十六进制字符串
* @Date 2020/07/27 19:38:39
* @Param [byteArray]
* @return java.lang.String
**/
public static String byteToStr(byte[] byteArray) {
String strDigest = "";
for (int i = 0; i < byteArray.length; i++) {
strDigest += byteToHexStr(byteArray[i]);
}
return strDigest;
}
/**
* @Description 转换页面的code为session信息(session中带有openid等信息)
* @Date 2020/07/29 17:45:38
* @Param [code, appId, appSecret]
* @return org.springframework.http.ResponseEntity
**/
public static ResponseEntity code2Session(String code,String appId,String appSecret){
System.out.println("code2Session----appId:"+appId+",appSecret:"+appSecret);
return HttpUtil.getFormRequest("http://api.weixin.qq.com/sns/oauth2/access_token?appid="+appId+"&secret="+appSecret+"&code="+code+"&grant_type=authorization_code");
}
/**
* @Description 获取JSSDKd 签名
* @Date 2020/07/29 17:46:52
* @Param []
* @return java.lang.String
**/
public static String getJsSDKSignature(String noncestr,String jsapi_ticket,Long timestamp,String url){
/*
签名生成规则如下:
参与签名的字段包括:
noncestr(随机字符串),有效的jsapi_ticket, timestamp(时间戳),url(当前网页的URL,不包含#及其后面部分) 。
步骤如下:
1)对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序),
2)使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。这里需要注意的是所有参数名均为小写字符。
3)对string1作sha1加密,字段名和字段值都采用原始值,不进行URL 转义
注意事项
签名用的noncestr和timestamp必须与前端的wx.config中的nonceStr和timestamp相同。
签名用的url必须是调用JS接口页面的完整URL。
出于安全考虑,开发者必须在服务器端实现签名的逻辑
*/
//利用TreeMap 默认排序规则:按照key的字典顺序来排序(升序)
TreeMap<String,Object> treeMap=new TreeMap<>();
treeMap.put("noncestr",noncestr);
treeMap.put("jsapi_ticket",jsapi_ticket);
treeMap.put("timestamp",timestamp);
treeMap.put("url",url);
StringBuffer stringBuffer=new StringBuffer();
Iterator<Map.Entry<String, Object>> it = treeMap.entrySet().iterator();
while(it.hasNext()) {
Map.Entry<String, Object> entry = it.next();
stringBuffer.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
String string1=stringBuffer.substring(0,stringBuffer.length()-1);
String signature = null;
MessageDigest md=null;
try {
md = MessageDigest.getInstance("SHA-1");
// 将字符串string1进行sha1加密
byte[] digest = md.digest(string1.getBytes());
signature = WxUtils.byteToStr(digest);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return signature;
}
}
其中用到的HttpUtil.java
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
/**
* @Description Http工具类
* @Date 2020/07/03 10:50:55
* @Param
* @return
**/
@Component
public class HttpUtil {
private static RestTemplate getRestTemplate() {
return new RestTemplateBuilder().build();
}
private static HttpHeaders getHttpHeaders(){
return new HttpHeaders();
}
/**
* @Description POST的方式发送json格式的请求
* @Date 2020/07/03 10:43:32
* @Param [url, params]
* @return java.lang.String
**/
public static String postJsonRequest(String url, Map<String,Object> params){
RestTemplate restTemplate=getRestTemplate();
HttpHeaders httpHeaders=getHttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(params, httpHeaders);
ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity,String.class);
return responseEntity.getBody();
}
/**
* @Description POST的方式发送表单格式的请求
* @Date 2020/07/03 10:49:37
* @Param [url, params]
* @return java.lang.String
**/
public static String postFormRequest(String url, MultiValueMap<String,Object> params){
RestTemplate restTemplate=getRestTemplate();
HttpHeaders httpHeaders=getHttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(params, httpHeaders);
ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity,String.class);
return responseEntity.getBody();
}
/**
* @Description get的方式发送表达格式的请求
* @Date 2020/07/03 10:50:20
* @Param [url]
* @return java.lang.String
**/
public static ResponseEntity getFormRequest(String url){
RestTemplate restTemplate = getRestTemplate();
ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class);
return responseEntity;
}
}
程序入口类:IndexController.java
import com.alibaba.fastjson.JSONObject;
import com.hongtai.driverapi.utils.WxUtils;
import com.sun.deploy.net.URLEncoder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.httpervletRequest;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
/**
* @Description 程序入口
* @Date 2020/08/31 17:19:38
**/
@Controller
public class IndexController {
@Value("${app_id}")
private String app_id;
@Value("${app_secret}")
private String app_secret;
/**
* @Description 跳转到授权页
* @Date 2020/07/28 15:40:27
* @Param [model]
* @return java.lang.String
**/
@RequestMapping("/")
public String auth(Model model){
model.addAttribute("app_id",app_id);
System.out.println("app_id:"+app_id);
try {
String url="http://blogs.johngene.cn/authCallBack";
String redirect_uri= URLEncoder.encode(url,"UTF-8");
System.out.println("redirect_uri:"+redirect_uri);
model.addAttribute("redirect_uri",redirect_uri);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return "auth";
}
/**
* @Description 跳转到微信支付测试页面
* @Date 2020/07/19 18:15:25
* @Param []
* @return java.lang.String
**/
@RequestMapping("/authCallBack")
public String authCallBack(String code, String state, httpervletRequest request){
System.out.println("网页授权获得:code:"+code+",state:"+state);
//code换取Session信息,应保留在服务器,需要时查询,过期时重新获取
ResponseEntity responseEntity= WxUtils.code2Session(code,app_id,app_secret);
String openid="";
String access_token="";
JSONObject entityJSON=null;
if(responseEntity.getStatusCodeValue()==200) {
entityJSON = JSONObject.parseObject(responseEntity.getBody().toString());
access_token = entityJSON.getString("access_token");//这里的access_token是网页的access_token并不是公众号的基本access_token
String expires_in = entityJSON.getString("expires_in");
String refresh_token = entityJSON.getString("refresh_token");
openid = entityJSON.getString("openid");
String scope = entityJSON.getString("scope");
System.out.println("根据网页授权获取的code再次请求换取的session信息,access_token:"+access_token+",expires_in:"+expires_in+"" +
",refresh_token:"+refresh_token+",openid:"+openid+",scope:"+scope);
}else if(responseEntity.getStatusCodeValue()==40029){
System.out.println("网页授权获得的code无效!");
}
request.getSession().setAttribute("openid",openid);
//attr.addAttribute("access_token",access_token);//网页授权获取的access_token只为进一步获取用户信息,别无他用
return "redirect:/toWxPay";
}
/**
* @Description 跳转到微信JSAPI支付页面
* @Date 2020/07/28 23:01:38
* @Param [code, state, model]
* @return java.lang.String
**/
@RequestMapping("/toWxPay")
public String wxPay(Model model,httpervletRequest request){
String openid=request.getSession().getAttribute("openid")+"";
model.addAttribute("openid",openid);
model.addAttribute("app_id",app_id);
System.out.println("已经重定向到了wx_pay页面,openid:"+openid);
return "wx_pay";
}
/**
* @Description JS接口安全域名和网页安全域名验证
* @Date 2020/07/26 09:50:37
* @Param []
* @return java.lang.String
**/
@RequestMapping("/MP_verify_xJwVNVyXJG2YvgOj.txt")
@ResponseBody
public String validateDomainName(){
//根据MP_verify_xJwVNVyXJG2YvgOj.txt的文件内容返回,这里是按照文件内容造假,直接返回
return "******";//这里需要复制出根据MP_verify_xJwVNVyXJG2YvgOj.txt文件内容,我用*号代替了
}
//----------------分割线,下面的是用来验证服务器配置的,和支付无关-----------------
@Value("${token}")
private String token;
/**
* @Description 服务器配置对应的接口:检验微信公众号Token的地址(接入微信后台的验证器)这里暂时用不到
* signature:微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
* timestamp:时间戳
* nonce:随机数
* echostr:随机字符串
* @Date 2020/07/27 19:17:49
* @Param [signature, timestamp, nonce, echostr]
* @return java.lang.String
**/
@RequestMapping("/checkToken")
@ResponseBody
public String checkToken(String signature,String timestamp,String nonce,String echostr){
// 1)将token、timestamp、nonce三个参数进行字典序排序
// 2)将三个参数字符串拼接成一个字符串进行sha1加密
// 3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
// 4)成功则将echostr原样返回,失败则不返回或返回其他
String[] arr = new String[] { token, timestamp, nonce };
// 将token、timestamp、nonce三个参数进行字典序排序
Arrays.sort(arr);
StringBuilder content = new StringBuilder();
for (int i = 0; i < arr.length; i++) {
content.append(arr[i]);
}
MessageDigest md;
String tmpStr = null;
try {
md = MessageDigest.getInstance("SHA-1");
// 将三个参数字符串拼接成一个字符串进行sha1加密
byte[] digest = md.digest(content.toString().getBytes());
tmpStr = WxUtils.byteToStr(digest);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
// 将sha1加密后的字符串可与signature对比
Boolean result=tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;
System.out.println("Token验证结果:"+result);
//通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败
if(result){
return echostr;
}else{
return "";
}
}
}
后台支付接口和获取初始化JSSDK所需要的signature签名接口
import com.alibaba.fastjson.JSONObject;
import com.github.wxpay.sdk.WXPayConstants;
import com.hongtai.driverapi.service.WxTokenService;
import com.hongtai.driverapi.utils.*;
import com.hongtai.driverapi.utils.wxpay.MyConfig;
import com.hongtai.driverapi.utils.wxpay.WXPay;
import com.hongtai.driverapi.utils.wxpay.WXPayUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.httpervletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
/**
* 公众号JSAPI支付
**/
@Controller
@RequestMapping("/payment")
public class WxPayOfficialController {
/*
1、商户server调用统一下单接口请求订单,api参见公共api【统一下单API】
2、商户server接收支付通知,api参见公共api【支付结果通知API】
3、商户server查询支付结果,api参见公共api【查询订单API】
*/
@Value("${app_id}")
private String app_id;
@Value("${app_secret}")
private String app_secret;
@Value("${MchID}")
private String mch_id;
@Value("${MchKey}")
private String mch_key;
@Value("${notifyUrl}")
private String notifyUrl;
@Value("${spbill_create_ip}")
private String spbill_create_ip;
/**
* @Description 公众号统一下单接口
* 注意:需要公众号设置的地方:ip白名单、JS接口安全域名、网页授权域名
* @Date 2020/07/27 22:14:29
**/
@RequestMapping(value = "/doUnifiedOrder", method = RequestMethod.POST)
@ResponseBody
public ResultUtil doUnifiedOrder(httpervletRequest request) {
JSONObject formData=ResParamsUtil.getBodyParamsJSON(request);
String openid=formData.getString("openid");
MyConfig config = null;
WXPay wxpay =null;
try {
config = new MyConfig(app_id,mch_id,mch_key);
wxpay= new WXPay(config);
} catch (Exception e) {
e.printStackTrace();
}
//获取客户端的ip地址
//获取本机的ip地址
InetAddress addr = null;
try {
addr = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
String spbill_create_ip = addr.getHostAddress();//终端ip,支持IPV4和IPV6两种格式的IP地址。调用微信支付API的机器IP
//支付金额,需要使用字符串类型,否则后面的签名会失败,微信支付提交的金额是不能带小数点的,且是以分为单位
int total_fee=1;
//商品描述
String body = "费用支付";
//商户订单号
String out_trade_no= WXPayUtil.generateNonceStr();
String nonceStr=WXPayUtil.generateNonceStr();
//统一下单接口参数
HashMap<String, String> data = new HashMap<>();
data.put("nonce_str",nonceStr);//随机字符串
data.put("body",body);//商品描述
data.put("out_trade_no",out_trade_no);//商户订单号(随机生成)
data.put("total_fee", String.valueOf(total_fee));//支付金额
data.put("spbill_create_ip", spbill_create_ip);//终端ip,支持IPV4和IPV6两种格式的IP地址。调用微信支付API的机器IP
data.put("notify_url", notifyUrl);//异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数
data.put("trade_type","JSAPI");//交易类型
data.put("openid", openid);//用户openid,trade_type=JSAPI,此参数必传
try {
Map<String, String> rMap = wxpay.unifiedOrder(data);//调用统一下单api
System.out.println("统一下单接口返回: " + rMap);
String return_code = rMap.get("return_code");
String result_code = rMap.get("result_code");
Long timeStamp = System.currentTimeMillis() / 1000;//获取时间戳
if ("SUCCESS".equals(return_code) && return_code.equals(result_code)) {
Map resultMap=new HashMap();
String prepayid = rMap.get("prepay_id");
resultMap.put("appId",app_id);//appId
resultMap.put("timeStamp", timeStamp + "");//这边要将返回的时间戳转化成字符串,不然小程序端调用wx.requestPayment方法会报签名错误
resultMap.put("nonceStr", nonceStr);//随机字符串必须和统一下单接口使用的随机字符串相同
resultMap.put("package", "prepay_id="+prepayid);
resultMap.put("signType", WXPayConstants.MD5);//MD5或者HMAC-SHA256
String sign = WXPayUtil.generateSignature(resultMap, mch_key);//再次签名,这个签名用于小程序端调用wx.requesetPayment方法
String xml=WXPayUtil.generateSignedXml(resultMap,mch_key);
resultMap.put("paySign", sign);
System.out.println("生成的签名paySign : "+ sign);
return new ResultUtil("200","",resultMap);
}else{
return new ResultUtil("500","",null);
}
} catch (Exception e) {
e.printStackTrace();
return new ResultUtil("500","",null);
}
}
/**
* @Description 接收支付结果
* @Date 2020/07/17 15:13:01
* @Param []
* @return void
**/
@RequestMapping(value = "/receiveResultOfPay")
@ResponseBody
public void receiveResultOfPay(httpervletRequest request){
System.out.println("支付成功!!!");
String xmlData=ResParamsUtil.getBodyParamsXML(request);
System.out.println(xmlData);
Map<String,String> map=null;
try {
map=WXPayUtil.xmlToMap(xmlData);
} catch (Exception e) {
e.printStackTrace();
}
//此处map就是接收到的支付成功的结果,同样的通知可能会多次(经过测试为13次左右)发送给商户系统。商户系统必须能够正确处理重复的通知
}
@Autowired
private WxTokenService wxAccessTokenService;
/**
* @Description 使用JSSDK前需要获取的signature签名数据
* @Date 2020/07/29 16:00:03
* @Param []
**/
@RequestMapping(value = "/getSignature")
@ResponseBody
public ResultUtil getSignature(String url){
//url是页面完整的url(请在当前页面alert(location.href.split('#')[0])确认),包括'http(s)://'部分,以及'?'后面的GET参数部分,但不包括'#'hash后面的部分。
if(url.indexOf("#")>0){
url=url.split("#")[0];
}
Map<String,Object> map=new HashMap<>();
JSONObject accessTokenJson=wxAccessTokenService.findAccessToken();
String access_token=accessTokenJson.getString("access_token");
JSONObject jsapiTicketJson=wxAccessTokenService.findJsAPITicket(access_token);
String jsapi_ticket=jsapiTicketJson.getString("ticket");
String noncestr=WXPayUtil.generateNonceStr();
Long timestamp = System.currentTimeMillis() / 1000;//获取时间戳
String signature= WxUtils.getJsSDKSignature(noncestr,jsapi_ticket,timestamp,url);
if(StringTools.isNotEmpty(signature)){
map.put("timestamp",timestamp);
map.put("noncestr",noncestr);
map.put("signature",signature);
//map.put("jsapi_ticket",jsapi_ticket);
//map.put("url",url);
return new ResultUtil("200","",map);
}else{
return new ResultUtil("500","",null);
}
}
}
下面来写页面
第一个入口页面代码:auth.html 只是用来直接跳转以获取微信的授权,这里采用静默授权snsapi_base,另一种授权snsapi_userinfo参考官网:http://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html#0
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="app">
</div>
</body>
<script>
/* 直接跳转到微信静默授权 */
let app_id='[[${app_id}]]';
let redirect_uri="[[${redirect_uri}]]";
let url='http://open.weixin.qq.com/connect/oauth2/authorize?' +
'appid='+app_id+
'&redirect_uri='+redirect_uri+
'&response_type=code' +
'&scope=snsapi_base' +
'&state=STATE#wechat_redirect';
console.log(url);
window.location.href=url;
</script>
</html>
支付测试页面:wx_pay.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>微信JSAPI支付测试</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0,user-scalable=no">
<!-- mintui -->
<link rel="stylesheet" href="http://unpkg.com/mint-ui/lib/style.css">
</head>
<body>
<div id="app">
<mt-button size="normal" type="primary" @click.native="startPay">发起支付</mt-button>
</div>
</body>
<!-- jquery -->
<script src="/js/jquery.js"></script>
<!-- vue -->
<script src="http://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- mintui -->
<script src="http://unpkg.com/mint-ui/lib/index.js"></script>
<!-- 引入js-sdk -->
<script src="http://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<script>
let vue=new Vue({
el:'#app',
data(){
return {
openid:'',
appid:'',
}
},
mounted(){
this.openid="[[${openid}]]";
this.appid="[[${app_id}]]";
console.log("openid:",this.openid,",appid:"+this.appid);
this.initJSSDK();
},
methods:{
//初始化jssdk
initJSSDK(){
let _this=this;
$.ajax({//请求服务器进行签名
type:'post',
dataType:'json',
url:'/payment/getSignature',
data:{
url:window.location.href,
},
success:function(res){
console.log("initJSSDK:",res);
if(res.code==200){
wx.config({
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: _this.appid, // 必填,公众号的唯一标识
timestamp: res.data.timestamp, // 必填,生成签名的时间戳
nonceStr: res.data.noncestr, // 必填,生成签名的随机串
signature: res.data.signature,// 必填,签名
jsApiList: ['chooseWXPay'] // 必填,需要使用的JS接口列表
});
}else{
}
},
error:function(res){
console.error(res);
}
});
},
//发起支付
startPay(){
console.log("发起支付");
let _this=this;
$.ajax({//调用统一下单接口
type:'post',
dataType:'json',
url:'/payment/doUnifiedOrder',
data:JSON.stringify({
openid : _this.openid
}),
success:function(res){
console.log(res)
if(res.code==200){
wx.chooseWXPay({//开始支付
timestamp: res.data.timeStamp, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
nonceStr: res.data.nonceStr, // 支付签名随机串,不长于 32 位
package: res.data.package, // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=\*\*\*)
signType: 'MD5', // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
paySign: res.data.paySign, // 支付签名
success: function (res) {
// 支付成功后的回调函数
console.log("支付成功:",res);
}
});
}else{
}
},
error:function(res){
console.error(res)
}
});
},
}
})
</script>
</html>
最后用手机测试支付就可以了,有什么不正确的地方或者缺少的工具类欢迎到评论区提醒!