Java开发微信支付实践
前提條件
1、認(rèn)證的微信公眾號和微信小程序號(http://mp.weixin.qq.com/)
2、微信商戶號(http://pay.weixin.qq.com/index.php/core/home/login?return_url=%2F)
3、帶有公網(wǎng)ip的VPS? 【端口映射用】,也可以使用其他方式映射
4、解析到公網(wǎng)ip的域名(公眾號里面用到JS接口安全域名和網(wǎng)頁授權(quán)域名)
5、映射知識參考這里:利用VPS服務(wù)器搭建一個(gè)FRP內(nèi)網(wǎng)穿透服務(wù)和Web服務(wù)穿透
開發(fā)工具:微信開發(fā)者工具、Intellij?idea
注冊商戶:
(注冊過程比較繁瑣,自行百度,需要營業(yè)執(zhí)照)
需要設(shè)置的地方在商戶平臺? 產(chǎn)品中心->AppID管理設(shè)置公眾號對應(yīng)的AppID
然后去?產(chǎn)品中心->開發(fā)配置? 里面將支付目錄設(shè)置為項(xiàng)目訪問的根目錄即可
(文檔地址: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里面的這里:
默認(rèn)加密方式用反了,需要修改成相反的方式。
2、賦值以下工具類和配置:
#######application.properties配置 ################ #小程序 keyAppID=wx*************** AppSecret=************************ #商戶id MchID=********* #商戶key(在微信支付商戶網(wǎng)站自定義的32個(gè)字符的mchkey) MchKey=************************ #終端ip,支持IPV4和IPV6兩種格式的IP地址。調(diào)用微信支付API的機(jī)器IP(這里提供的是VPS的公網(wǎng)ip,其他映射方式需要自己試驗(yàn)) spbill_create_ip=*.*.*.* #異步接收微信支付結(jié)果通知的回調(diào)地址,通知url必須為外網(wǎng)可訪問的url,不能攜帶參數(shù) notifyUrl=http://www.xxx.com/payment/receiveResultOfPay /** * @Description 統(tǒng)一返回類 **/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 請求和返回的參數(shù)封裝 **/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、小程序端調(diào)用微信登錄接口,返回code后,再用code發(fā)送到后臺的接口,讓后臺去請求微信官方的接口換取帶有openid的認(rèn)證session(即authsession)
/* app.js */ App({onLaunch: function () {this.startLogin();//調(diào)用登錄 },startLogin:function(){//登錄wx.login({timeout:30000,success: login_res =>{//發(fā)送請求到后臺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結(jié)果:",codesession_res)if(codesession_res.data.code=='200'){wx.setStorage({ key: 'openId', data: codesession_res.data.data });//存儲到本地內(nèi)存中}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 {@Autowiredprivate RestTemplateBuilder restTemplateBuilder;@Value("${AppID}")private String appId; @Value("${AppSecret}")private String appSecret;/*** 根據(jù)前臺調(diào)用的登錄接口返回的code再次請求查詢帶有openid的authSession登錄信息**/@PostMapping("/getAuthSession")@ResponseBodypublic ResultUtil getAuthSession(httpervletRequest request){JSONObject formData= ResParamsUtil.getBodyParamsJSON(request).getJSONObject("formData");String code=formData.getString("code");if(code==null){return new ResultUtil("1","失敗",null);//返回自定義登錄態(tài)}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());//獲取到的兩個(gè)有用的東西:openId和sessionKey,其中我只用到openIdString openId=entityJSON.getString("openid");String sessionKey=entityJSON.getString("session_key"); if(openId!=null){return new ResultUtil("200","登錄成功",openId);//返回自定義登錄態(tài)(openId理論上講應(yīng)存在后臺,不返回給前端,只需要返回給前端一個(gè)自定義的userId,讓前端使用userId查詢openId)}else{return new ResultUtil("201","保存用戶信息失敗",null);//返回自定義登錄態(tài)}}else{return new ResultUtil("500","查詢的登錄信息失敗",null);//返回自定義登錄態(tài)}}}4、調(diào)用支付的頁面index.wxml代碼:
<!-- index.wxml --> <view><button type="primary" bindtap="startPay" style="margin-bottom:15px;">調(diào)用支付</button> </view>只有一個(gè)按鈕:
index.js代碼:
/* index.js */ const app = getApp() Page({//頁面的初始數(shù)據(jù)data: {},//自定義:支付按鈕事件startPay:function(){/*小程序支付開發(fā)流程:1、小程序內(nèi)調(diào)用登錄接口,獲取到用戶的openid,api參見公共api【小程序登錄API】2、商戶server調(diào)用支付統(tǒng)一下單,api參見公共api【統(tǒng)一下單API】3、商戶server調(diào)用再次簽名,api參見公共api【再次簽名】4、商戶server接收支付通知,api參見公共api【支付結(jié)果通知API】5、商戶server查詢支付結(jié)果,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("服務(wù)端返回:",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("統(tǒng)一下單接口成功:",res);},fail(res) {console.log("統(tǒng)一下單接口失敗:",res);}});},fail: function(res) {console.log("請求服務(wù)失敗:",res);},complete: function(res) {},});},//生命周期函數(shù)--監(jiān)聽頁面加載onLoad: function (options) {},//生命周期函數(shù)--監(jiān)聽頁面初次渲染完成onReady: function () {},//生命周期函數(shù)--監(jiān)聽頁面顯示onShow: function () {},//生命周期函數(shù)--監(jiān)聽頁面隱藏onHide: function () {},//生命周期函數(shù)--監(jiān)聽頁面卸載onUnload: function () {},//頁面相關(guān)事件處理函數(shù)--監(jiān)聽用戶下拉動作onPullDownRefresh: function () {},//頁面上拉觸底事件的處理函數(shù)onReachBottom: function () {},//用戶點(diǎn)擊右上角分享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;}@Overridepublic String getAppID() {return this.appid;}@Overridepublic String getMchID() {return this.mch_id;}@Overridepublic String getKey() {return this.mch_key;}@Overridepublic InputStream getCertStream() {ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);return certBis;}@Overridepublic int getHttpConnectTimeoutMs() {return 8000;}@Overridepublic int getHttpReadTimeoutMs() {return 10000;}@OverrideIWXPayDomain getWXPayDomain() {return new IWXPayDomain() {@Overridepublic void report(String domain, long elapsedTimeMillis, Exception ex) {}@Overridepublic 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、小程序內(nèi)調(diào)用登錄接口,獲取到用戶的openid,api參見公共api【小程序登錄API】2、商戶server調(diào)用支付統(tǒng)一下單,api參見公共api【統(tǒng)一下單API】3、商戶server調(diào)用再次簽名,api參見公共api【再次簽名】4、商戶server接收支付通知,api參見公共api【支付結(jié)果通知API】5、商戶server查詢支付結(jié)果,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;/*** 后臺下單接口(里面自動調(diào)用了微信統(tǒng)一下單接口)**/@RequestMapping(value = "/doUnifiedOrder", method = RequestMethod.POST)@ResponseBodypublic 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地址//獲取本機(jī)的ip地址InetAddress addr = null;try {addr = InetAddress.getLocalHost();} catch (UnknownHostException e) {e.printStackTrace();}String spbill_create_ip = addr.getHostAddress();//終端ip,支持IPV4和IPV6兩種格式的IP地址。調(diào)用微信支付API的機(jī)器IPint total_fee=1;//支付金額,需要使用字符串類型,否則后面的簽名會失敗,微信支付提交的金額是不能帶小數(shù)點(diǎn)的,且是以分為單位String body = "費(fèi)用支付";//商品描述String out_trade_no= WXPayUtil.generateNonceStr();//商戶訂單號String nonceStr=WXPayUtil.generateNonceStr();//獲取隨機(jī)字符串//統(tǒng)一下單接口參數(shù)HashMap<String, String> data = new HashMap<>();data.put("body", body);//商品描述data.put("out_trade_no",out_trade_no);//商戶訂單號(隨機(jī)生成)data.put("total_fee", String.valueOf(total_fee));//支付金額data.put("spbill_create_ip", spbill_create_ip);//終端ip,支持IPV4和IPV6兩種格式的IP地址。調(diào)用微信支付API的機(jī)器IPdata.put("notify_url", notifyUrl);//異步接收微信支付結(jié)果通知的回調(diào)地址,通知url必須為外網(wǎng)可訪問的url,不能攜帶參數(shù)data.put("trade_type","JSAPI");//交易類型data.put("openid", openId);//用戶openid,trade_type=JSAPI,此參數(shù)必傳data.put("nonce_str",nonceStr);try {Map<String, String> rMap = wxpay.unifiedOrder(data);//調(diào)用統(tǒng)一下單apiSystem.out.println("統(tǒng)一下單接口返回: " + rMap);String return_code = rMap.get("return_code");String result_code = rMap.get("result_code");Long timeStamp = System.currentTimeMillis() / 1000;//獲取時(shí)間戳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要大寫不能寫成appidresultMap.put("timeStamp", timeStamp + "");//這邊要將返回的時(shí)間戳轉(zhuǎn)化成字符串,不然小程序端調(diào)用wx.requestPayment方法會報(bào)簽名錯(cuò)誤resultMap.put("nonceStr", nonceStr);//隨機(jī)字符串必須和統(tǒng)一下單接口使用的隨機(jī)字符串相同resultMap.put("package", "prepay_id="+prepayid);resultMap.put("signType", WXPayConstants.MD5);//MD5或者HMAC-SHA256String sign = WXPayUtil.generateSignature(resultMap, mch_key);//再次簽名,這個(gè)簽名用于小程序端調(diào)用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 接收成功后返回的支付結(jié)果* @Date 2020/07/17 15:13:01* @Param []* @return void**/@RequestMapping(value = "/receiveResultOfPay")@ResponseBodypublic 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就是接收到的支付成功的結(jié)果,同樣的通知可能會多次,發(fā)送給商戶系統(tǒng)。商戶系統(tǒng)必須能夠正確處理重復(fù)的通知} }2.公眾號里面的JSAPI支付
公眾號調(diào)用該支付與小程序調(diào)用支付的不同之處在于小程序直接可以調(diào)用登錄api獲取code然后利用code去后臺調(diào)用統(tǒng)一下單接口,而公眾號的網(wǎng)頁無法直接調(diào)用,需要通過JSSDK調(diào)用。
微信JSSDK開發(fā)文檔地址: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、公眾號開發(fā)配置
需要點(diǎn)擊一下重置來獲取AppSecret和ip白名單,白名單需要設(shè)置成測試環(huán)境下的公網(wǎng)ip(百度ip地址即可查看公網(wǎng)ip)和服務(wù)器對應(yīng)的ip(上線后用到)
2、網(wǎng)頁授權(quán)配置
在接口權(quán)限目錄中找到? 網(wǎng)頁服務(wù)->網(wǎng)頁授權(quán)->點(diǎn)擊后面的修改按鈕
3、關(guān)聯(lián)商戶號
關(guān)聯(lián)成功后可以看到如下界面:
?
進(jìn)入重點(diǎn):
思路:
? 先獲取到微信的網(wǎng)頁授權(quán),利用網(wǎng)頁授權(quán)接口返回的code和state,用后臺再次請求通過code換取網(wǎng)頁授權(quán)access_token接口,完成授權(quán)后重定向到支付頁面并將openid、appId等信息帶過去,支付頁面中用appid等信息獲取簽名后初始化JSSDK,請求后臺的支付接口,利用后臺的支付接口請求微信統(tǒng)一下單接口進(jìn)行支付操作,后臺等待支付成功消息。
簡化思路:
? 網(wǎng)頁授權(quán):
第一步:用戶同意授權(quán),獲取code
第二步:通過code換取網(wǎng)頁授權(quán)access_token
? 調(diào)用JSSDK:
直接初始化JSSDK
? 調(diào)用支付:
直接調(diào)用后臺支付接口,讓后臺完成操作
開始寫代碼:
工具類和上面小程序JSAPI支付里面的兩個(gè)工具類以外還有一個(gè)簽名用的工具類WxUtil.java
application.properties配置類如下:
#商戶id MchID=******** #商戶key MchKey=********#終端ip,支持IPV4和IPV6兩種格式的IP地址。調(diào)用微信支付API的機(jī)器IP(要與實(shí)際調(diào)用的機(jī)器相同) spbill_create_ip=*.*.*.*#異步接收微信支付結(jié)果通知的回調(diào)地址,通知url必須為外網(wǎng)可訪問的url,不能攜帶參數(shù) 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 將字節(jié)轉(zhuǎn)換為十六進(jìn)制字符串* @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 將數(shù)組中的內(nèi)容進(jìn)行字典排序也可以直接使用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 將字節(jié)數(shù)組轉(zhuǎn)換為十六進(jìn)制字符串* @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 轉(zhuǎn)換頁面的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){/*簽名生成規(guī)則如下:參與簽名的字段包括:noncestr(隨機(jī)字符串),有效的jsapi_ticket, timestamp(時(shí)間戳),url(當(dāng)前網(wǎng)頁的URL,不包含#及其后面部分) 。步驟如下:1)對所有待簽名參數(shù)按照字段名的ASCII 碼從小到大排序(字典序),2)使用URL鍵值對的格式(即key1=value1&key2=value2…)拼接成字符串string1。這里需要注意的是所有參數(shù)名均為小寫字符。3)對string1作sha1加密,字段名和字段值都采用原始值,不進(jìn)行URL 轉(zhuǎn)義注意事項(xiàng)簽名用的noncestr和timestamp必須與前端的wx.config中的nonceStr和timestamp相同。簽名用的url必須是調(diào)用JS接口頁面的完整URL。出于安全考慮,開發(fā)者必須在服務(wù)器端實(shí)現(xiàn)簽名的邏輯*///利用TreeMap 默認(rèn)排序規(guī)則:按照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進(jìn)行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的方式發(fā)送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的方式發(fā)送表單格式的請求* @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的方式發(fā)送表達(dá)格式的請求* @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 跳轉(zhuǎn)到授權(quán)頁* @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 跳轉(zhuǎn)到微信支付測試頁面* @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("網(wǎng)頁授權(quán)獲得:code:"+code+",state:"+state);//code換取Session信息,應(yīng)保留在服務(wù)器,需要時(shí)查詢,過期時(shí)重新獲取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是網(wǎng)頁的access_token并不是公眾號的基本access_tokenString 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("根據(jù)網(wǎng)頁授權(quán)獲取的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("網(wǎng)頁授權(quán)獲得的code無效!");}request.getSession().setAttribute("openid",openid);//attr.addAttribute("access_token",access_token);//網(wǎng)頁授權(quán)獲取的access_token只為進(jìn)一步獲取用戶信息,別無他用return "redirect:/toWxPay";}/*** @Description 跳轉(zhuǎn)到微信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("已經(jīng)重定向到了wx_pay頁面,openid:"+openid);return "wx_pay";}/*** @Description JS接口安全域名和網(wǎng)頁安全域名驗(yàn)證* @Date 2020/07/26 09:50:37* @Param []* @return java.lang.String**/@RequestMapping("/MP_verify_xJwVNVyXJG2YvgOj.txt")@ResponseBodypublic String validateDomainName(){//根據(jù)MP_verify_xJwVNVyXJG2YvgOj.txt的文件內(nèi)容返回,這里是按照文件內(nèi)容造假,直接返回return "******";//這里需要復(fù)制出根據(jù)MP_verify_xJwVNVyXJG2YvgOj.txt文件內(nèi)容,我用*號代替了}//----------------分割線,下面的是用來驗(yàn)證服務(wù)器配置的,和支付無關(guān)-----------------@Value("${token}")private String token;/*** @Description 服務(wù)器配置對應(yīng)的接口:檢驗(yàn)微信公眾號Token的地址(接入微信后臺的驗(yàn)證器)這里暫時(shí)用不到* signature:微信加密簽名,signature結(jié)合了開發(fā)者填寫的token參數(shù)和請求中的timestamp參數(shù)、nonce參數(shù)。* timestamp:時(shí)間戳* nonce:隨機(jī)數(shù)* echostr:隨機(jī)字符串* @Date 2020/07/27 19:17:49* @Param [signature, timestamp, nonce, echostr]* @return java.lang.String**/@RequestMapping("/checkToken")@ResponseBodypublic String checkToken(String signature,String timestamp,String nonce,String echostr){// 1)將token、timestamp、nonce三個(gè)參數(shù)進(jìn)行字典序排序// 2)將三個(gè)參數(shù)字符串拼接成一個(gè)字符串進(jìn)行sha1加密// 3)開發(fā)者獲得加密后的字符串可與signature對比,標(biāo)識該請求來源于微信// 4)成功則將echostr原樣返回,失敗則不返回或返回其他String[] arr = new String[] { token, timestamp, nonce };// 將token、timestamp、nonce三個(gè)參數(shù)進(jìn)行字典序排序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");// 將三個(gè)參數(shù)字符串拼接成一個(gè)字符串進(jìn)行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驗(yàn)證結(jié)果:"+result);//通過檢驗(yàn)signature對請求進(jìn)行校驗(yàn),若校驗(yàn)成功則原樣返回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調(diào)用統(tǒng)一下單接口請求訂單,api參見公共api【統(tǒng)一下單API】2、商戶server接收支付通知,api參見公共api【支付結(jié)果通知API】3、商戶server查詢支付結(jié)果,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 公眾號統(tǒng)一下單接口* 注意:需要公眾號設(shè)置的地方:ip白名單、JS接口安全域名、網(wǎng)頁授權(quán)域名* @Date 2020/07/27 22:14:29**/@RequestMapping(value = "/doUnifiedOrder", method = RequestMethod.POST)@ResponseBodypublic 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地址//獲取本機(jī)的ip地址InetAddress addr = null;try {addr = InetAddress.getLocalHost();} catch (UnknownHostException e) {e.printStackTrace();}String spbill_create_ip = addr.getHostAddress();//終端ip,支持IPV4和IPV6兩種格式的IP地址。調(diào)用微信支付API的機(jī)器IP//支付金額,需要使用字符串類型,否則后面的簽名會失敗,微信支付提交的金額是不能帶小數(shù)點(diǎn)的,且是以分為單位int total_fee=1;//商品描述String body = "費(fèi)用支付";//商戶訂單號String out_trade_no= WXPayUtil.generateNonceStr();String nonceStr=WXPayUtil.generateNonceStr();//統(tǒng)一下單接口參數(shù)HashMap<String, String> data = new HashMap<>();data.put("nonce_str",nonceStr);//隨機(jī)字符串data.put("body",body);//商品描述data.put("out_trade_no",out_trade_no);//商戶訂單號(隨機(jī)生成)data.put("total_fee", String.valueOf(total_fee));//支付金額data.put("spbill_create_ip", spbill_create_ip);//終端ip,支持IPV4和IPV6兩種格式的IP地址。調(diào)用微信支付API的機(jī)器IPdata.put("notify_url", notifyUrl);//異步接收微信支付結(jié)果通知的回調(diào)地址,通知url必須為外網(wǎng)可訪問的url,不能攜帶參數(shù)data.put("trade_type","JSAPI");//交易類型data.put("openid", openid);//用戶openid,trade_type=JSAPI,此參數(shù)必傳try {Map<String, String> rMap = wxpay.unifiedOrder(data);//調(diào)用統(tǒng)一下單apiSystem.out.println("統(tǒng)一下單接口返回: " + rMap);String return_code = rMap.get("return_code");String result_code = rMap.get("result_code");Long timeStamp = System.currentTimeMillis() / 1000;//獲取時(shí)間戳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);//appIdresultMap.put("timeStamp", timeStamp + "");//這邊要將返回的時(shí)間戳轉(zhuǎn)化成字符串,不然小程序端調(diào)用wx.requestPayment方法會報(bào)簽名錯(cuò)誤resultMap.put("nonceStr", nonceStr);//隨機(jī)字符串必須和統(tǒng)一下單接口使用的隨機(jī)字符串相同resultMap.put("package", "prepay_id="+prepayid);resultMap.put("signType", WXPayConstants.MD5);//MD5或者HMAC-SHA256String sign = WXPayUtil.generateSignature(resultMap, mch_key);//再次簽名,這個(gè)簽名用于小程序端調(diào)用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 接收支付結(jié)果* @Date 2020/07/17 15:13:01* @Param []* @return void**/@RequestMapping(value = "/receiveResultOfPay")@ResponseBodypublic 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就是接收到的支付成功的結(jié)果,同樣的通知可能會多次(經(jīng)過測試為13次左右)發(fā)送給商戶系統(tǒng)。商戶系統(tǒng)必須能夠正確處理重復(fù)的通知}@Autowiredprivate WxTokenService wxAccessTokenService;/*** @Description 使用JSSDK前需要獲取的signature簽名數(shù)據(jù)* @Date 2020/07/29 16:00:03* @Param []**/@RequestMapping(value = "/getSignature")@ResponseBodypublic ResultUtil getSignature(String url){//url是頁面完整的url(請?jiān)诋?dāng)前頁面alert(location.href.split('#')[0])確認(rèn)),包括'http(s)://'部分,以及'?'后面的GET參數(shù)部分,但不包括'#'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;//獲取時(shí)間戳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);}} }下面來寫頁面
第一個(gè)入口頁面代碼:auth.html?只是用來直接跳轉(zhuǎn)以獲取微信的授權(quán),這里采用靜默授權(quán)snsapi_base,另一種授權(quán)snsapi_userinfo參考官網(wǎng):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>/* 直接跳轉(zhuǎn)到微信靜默授權(quán) */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">發(fā)起支付</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:{//初始化jssdkinitJSSDK(){let _this=this;$.ajax({//請求服務(wù)器進(jìn)行簽名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, // 開啟調(diào)試模式,調(diào)用的所有api的返回值會在客戶端alert出來,若要查看傳入的參數(shù),可以在pc端打開,參數(shù)信息會通過log打出,僅在pc端時(shí)才會打印。appId: _this.appid, // 必填,公眾號的唯一標(biāo)識timestamp: res.data.timestamp, // 必填,生成簽名的時(shí)間戳nonceStr: res.data.noncestr, // 必填,生成簽名的隨機(jī)串signature: res.data.signature,// 必填,簽名jsApiList: ['chooseWXPay'] // 必填,需要使用的JS接口列表});}else{}},error:function(res){console.error(res);}});},//發(fā)起支付startPay(){console.log("發(fā)起支付");let _this=this;$.ajax({//調(diào)用統(tǒng)一下單接口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, // 支付簽名時(shí)間戳,注意微信jssdk中的所有使用timestamp字段均為小寫。但最新版的支付后臺生成簽名使用的timeStamp字段名需大寫其中的S字符nonceStr: res.data.nonceStr, // 支付簽名隨機(jī)串,不長于 32 位package: res.data.package, // 統(tǒng)一支付接口返回的prepay_id參數(shù)值,提交格式如:prepay_id=\*\*\*)signType: 'MD5', // 簽名方式,默認(rèn)為'SHA1',使用新版支付需傳入'MD5'paySign: res.data.paySign, // 支付簽名success: function (res) {// 支付成功后的回調(diào)函數(shù)console.log("支付成功:",res);}});}else{}},error:function(res){console.error(res)}});},}}) </script> </html>最后用手機(jī)測試支付就可以了,有什么不正確的地方或者缺少的工具類歡迎到評論區(qū)提醒!
總結(jié)
以上是生活随笔為你收集整理的Java开发微信支付实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php swoole websocket
- 下一篇: 学号 20175212 《Java程序设