接入【微信JS-SDK】 坑多多
接入【微信JS-SDK】
用於實現自定義朋友圈分享的標題和圖示。
開始之前需要在公眾平臺進行一些設定:
- 請求 access_token 的服務ip 要新增進【後臺》基本設定》公眾號開發資訊》IP白名單】
- 接入JS-SDK的頁面域名要新增進【後臺》公眾號設定》功能設定》JS介面安全域名】列表(最多隻能加三個,每個月最多改三次)
js-sdk使用需要獲取簽名,驗證通過後方能呼叫各種【微信介面】
- 前臺將當前頁面(呼叫微信介面的頁面)url發給我方伺服器,或我方後臺接到頁面請求後自己取出請求的url
- 我方伺服器向微信方傳送 AppID、AppSecret(可登入公眾平臺查) 請求 access_token
- 再用 access_token 請求 ticket
- 拿到 ticket 如果按微信給的演算法進行簽名,這一步要用到1中提到的url(也就是簽名對頁面是唯一的,引數變化什麼的就得重新簽名了,想想我之前在地址後面加的那些 rand=math.random() 【想哭】)
- 將簽名返回給前端
- 前端頁面載入後,通過config介面注入許可權驗證配置
wx.config({
debug: true, // 開啟除錯模式,呼叫的所有api的返回值會在客戶端alert出來,若要檢視傳入的引數,可以在pc端開啟,引數資訊會通過log打出,僅在pc端時才會列印。
appId: '${configParam.appId}', // 必填,公眾號的唯一標識
timestamp: '${configParam.timestamp}', // 必填,生成簽名的時間戳
nonceStr: '${configParam.nonceStr}', // 必填,生成簽名的隨機串
signature: '${configParam.signature}',// 必填,簽名
jsApiList: [ // 必填,需要使用的JS介面列表
'checkJsApi',
'onMenuShareTimeline',
'onMenuShareAppMessage'
]
});
7、如果通過驗證,呼叫 ready 介面
wx.ready(function(){
// config資訊驗證後會執行ready方法,所有介面呼叫都必須在config介面獲得結果之後,config是一個客戶端的非同步操作,
// 所以如果需要在頁面載入時就呼叫相關介面,則須把相關介面放在ready函式中呼叫來確保正確執行。對於使用者觸發時才呼叫的介面,
// 則可以直接呼叫,不需要放在ready函式中。
});
8、否則呼叫 error介面
wx.error(function(res){
// config資訊驗證失敗會執行error函式,如簽名過期導致驗證失敗,具體錯誤資訊可以開啟config的debug模式檢視,
// 也可以在返回的res引數中檢視,對於SPA可以在這裡更新簽名。
});
詳情見文件:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
是不是覺得 So Easy ? 那你就圖羊圖身破了
============================ 理想與現實的分割線 ============================
理想自然是美好的。但現實。。。。。。。。。。
1、新介面沒卵用
騰訊說,我們升級了,給了兩個新介面,快快用它吧!
廢了九牛二虎之力,把他那個變態的簽名拿到手。結果這兩個新介面根本沒卵用。 【今天是 2018-11-29】
驗證通過,許可權拿到。
2、舊介面,瞎JB亂取值
然後到網上看到一個解決方案,牛B的方案。
放棄新介面,用回原來的。
結果~~~~~~~~ 標題和圖片明明傳給它了,但它就是自己瞎JB亂取一通。。。
我TMD搞這麼多鬼事,就是為了讓你來放飛自我的?(我TMD隨便找個瀏覽器分享過來都能帶圖啊,瞎JB取,我還用你啊)
【取的是頁面的 title 和 body 下的第一張>=300x300的圖,道聽途說的】
3、詭異的 wx.error
任你配置引數怎麼錯,死活不呼叫
然後是那個詭異的 wx.error
這段中文的意思難道不就是 wx.config
驗證失敗會執行它嗎?
但無論我怎麼亂填配置引數,它任你錯,就是不走這裡。
4、官方的QQ尾巴 我fuuuuuuuuuuuuuuuck
分享給好友加這個
https://mp.csdn.net/mdeditor/84640526?from=groupmessage
分享到朋友圈加這個
https://mp.csdn.net/mdeditor/84640526?from=timeline
你自己搞個JB簽名要用url 你自己心裡沒點B數嗎?全中國的網路上,因為你這JB設計要浪費多少請求?
5、這破玩意也是死魚。只有手動執行它才有反應。
放到 wx.ready 裡面外面都一樣,沒卵用。只有手動執行它才有反應。
wx.checkJsApi({
jsApiList: [
'updateAppMessageShareData',
'updateTimelineShareData',
],
success: function (res) {
alert(JSON.stringify(res));
}
});
6、另外的另外,下面詭異的 success
每當我開啟頁面,success 的內容就輸出了。我都搞不懂,你是怎麼側漏的。
這不是要等我執行了分享操作才執行的嗎?(另外在網上看到別人說官方把回撥規則調整了,但是文件竟然絲毫沒有提及。我不知道他們是不是還給開發者搞了個VIP,要充錢才能看到最新的文件麼?)
wx.ready(function () {
wx.onMenuShareTimeline({
title: '【'+ document.title+'】', // 分享標題
link: window.location.href, // 分享連結,該連結域名或路徑必須與當前頁面對應的公眾號JS安全域名一致
imgUrl: $('#weChatTimelineIco').attr('src') || "<%=basePath%>images/icon/logo/site_${USER_SITE.siteId}.jpg", // 分享圖示
success: function () {
console && console.info && console.info('獲取“分享到朋友圈”按鈕點選狀態及自定義分享內容介面(即將廢棄)');
}
});
});
總之面對
我只想說一個大寫的
FUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUCK
最後的最後,還是乖乖用了舊介面。耐心調調還是能用的
確保後臺生成的 configParam 資料正確,前端程式碼如下:
<script src="http://res.wx.qq.com/open/js/jweixin-1.4.0.js"> </script>
<script>
//判斷是否微信登陸
function isWeChat() {
var ua = window.navigator.userAgent.toLowerCase();
if (ua.match(/MicroMessenger/i) == 'micromessenger') {
return true;
} else {
return false;
}
};
//剪掉微信尾巴,轉發給微信好友或朋友圈的URL開啟後會加尾巴
function cutWeChatTail(){
try{
var url = window.location.href;
if(url.indexOf('from=timeline') > -1 || url.indexOf('from=groupmessage') > -1){
window.location.href = url.replace(/[?&]from=.*/,'');
}
}catch(e){}
}
/*
* 注意:
* 1. 所有的JS介面只能在公眾號繫結的域名下呼叫,公眾號開發者需要先登入微信公眾平臺進入“公眾號設定”的“功能設定”裡填寫“JS介面安全域名”。
* 2. 如果發現在 Android 不能分享自定義內容,請到官網下載最新的包覆蓋安裝,Android 自定義分享介面需升級至 6.0.2.58 版本及以上。
* 3. 常見問題及完整 JS-SDK 文件地址:http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html
*
* 開發中遇到問題詳見文件“附錄5-常見錯誤及解決辦法”解決,如仍未能解決可通過以下渠道反饋:
* 郵箱地址:[email protected]
* 郵件主題:【微信JS-SDK反饋】具體問題
* 郵件內容說明:用簡明的語言描述問題所在,並交代清楚遇到該問題的場景,可附上截圖圖片,微信團隊會盡快處理你的反饋。
*/
wx.jerryParam = {
debug: false,
appId: '${configParam.appId}',
timestamp: '${configParam.timestamp}',
nonceStr: '${configParam.nonceStr}',
signature: '${configParam.signature}',
jsApiList: [
'checkJsApi',
'onMenuShareTimeline',
'onMenuShareAppMessage',
'onMenuShareQZone',
'onMenuShareQQ'
]
};
if(isWeChat()){
cutWeChatTail();
wx.config(wx.jerryParam);
}
wx.ready(function () {
console && console.info && console.info('------------- 成功呼叫 wx.ready() -------------');
var title = '【'+ document.title+'】';
var desc = $('[name="description"]').attr("content") || "大家好,我是笨笨,笨笨的笨,笨笨的笨,謝謝!";
var link = window.location.href;
var imgUrl = $('#weChatTimelineIco').attr('src') || "https://avatar.csdn.net/9/7/4/1_jx520.jpg";
//獲取“分享到朋友圈”按鈕點選狀態及自定義分享內容介面(即將廢棄)
var paramDataOld = {
title: title, // 分享標題
link: link, // 分享連結,該連結域名或路徑必須與當前頁面對應的公眾號JS安全域名一致
imgUrl: imgUrl, // 分享圖示
success: function () {
console && console.info && console.info('獲取“分享到朋友圈”按鈕點選狀態及自定義分享內容介面(即將廢棄)');
}
}
wx.onMenuShareTimeline(paramDataOld);
paramDataOld.desc = desc; // 分享描述
wx.onMenuShareAppMessage(paramDataOld);
wx.onMenuShareQQ(paramDataOld);
wx.onMenuShareQZone(paramDataOld);
//----------------------------------------------------------
wx.checkJsApi({
jsApiList: [
'checkJsApi',
'onMenuShareTimeline',
'onMenuShareAppMessage',
'onMenuShareQZone',
'onMenuShareQQ'
],
success: function (res) {
console && console.info && console.info(JSON.stringify(res));
}
});
});
// 我這裡對訪問的url規則是有控制的,所以用了簡單粗暴的方式來減除微信尾巴。
wx.error(function(res){
console && console.info && console.info('config出錯:' + JSON.stringify(res,null,4));
if(isWeChat()){
cutWeChatTail();
}
});
</script>
Java 後臺。希望哪天我失憶了,這段程式碼拿來就能直接用。233333
見的有點匆忙,並且整個過程籠罩在被忽悠的陰雲之下,所以程式碼還應該優化一下的。
如果是前端動態用ajax獲取簽名,我看到別人用個單例來實現。
不過我這裡只要簽名不過期,就不需要總請求伺服器。所以將就用咯。。。
package com.jerry.web.util;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.ParseException;
import org.apache.http.client.methods.HttpGet;
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.util.EntityUtils;
import org.apache.log4j.Logger;
import net.sf.json.JSONObject;
/**
* 微信工具類
* ACCESS_TOKEN 和 JS_API_TICKET 由定時作業每隔 MAX_TIME 重新整理一次。
* by JerryJin 2018-11-29
*/
public class WeChatUtil {
private static final Logger log = Logger.getLogger(WeChatUtil.class);
//--------------------------------------------------------------------------------------------------
private static final String APPID = SystemConfigUtil.readConfig("wxpay.app_id");//公司公眾號的APPID(已通過認證)
private static final String APPSECRET = SystemConfigUtil.readConfig("wxpay.transfer_api_password");//公司公眾號的APPSECRET(已通過認證)
private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
private static final String JS_API_TICKET_URL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi";
private static final String ACCESS_TOKEN = "ACCESS_TOKEN";
private static final String JS_API_TICKET = "JS_API_TICKET";
private static final long TOKEN_MAX_TIME = 7000 * 1000;// 微信允許最長Access_token有效時間為7200秒,這裡設定為7000秒
private static final long TICKET_MAX_TIME = 7000 * 1000;// 微信允許最長js_api_ticket有效時間為7200秒,這裡設定為7000秒
//--------------------------------------------------------------------------------------------------
private 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);
log.info(result);
}
return jsonObject;
}
private static String httpGet(String url){
String strResult = null;
try {
CloseableHttpClient client = HttpClients.createDefault();
HttpGet request = new HttpGet(url);
HttpResponse response = client.execute(request);
/**請求傳送成功,並得到響應**/
if (response.getStatusLine().getStatusCode() == org.apache.http.HttpStatus.SC_OK) {
/**讀取伺服器返回過來的json字串資料**/
strResult = EntityUtils.toString(response.getEntity());
log.info(strResult);
} else {
log.error("get請求提交失敗");
}
} catch (IOException e) {
log.error("get請求提交失敗:" + e.getMessage(), e);
}
return strResult;
}
/**
* 向微信介面請求 access_token</br>
* <b>文件:</b>https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183
* @param appid
* @param appsecret
* @return
*/
private static AccessToken getAccessToken(String appid,String appsecret) {
log.info("獲取 AccessToken 開始");
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.error(e.getMessage(), e);
} catch (IOException e) {
log.error(e.getMessage(), e);
}
if(jsonObject!=null){
// 成功: {"access_token":"ACCESS_TOKEN","expires_in":7200}
// 失敗: {"errcode":40013,"errmsg":"invalid appid"}
token.setToken(jsonObject.optString("access_token"));
token.setExpiresIn(jsonObject.optInt("expires_in"));
token.setErrcode(jsonObject.optString("errcode"));
token.setErrmsg(jsonObject.optString("errmsg"));
token.setCreateDate(new Date());//獲取時間
}
if (checkAccessToken(token)) {
ServletContextUtil.get().setAttribute(ACCESS_TOKEN, token);// 快取全域性變數
log.info("獲取 AccessToken 成功!");
}
return token;
}
/**
* 驗證本地快取的 AccessToken,如果有效,就返回 true 否則 false
* @return
*/
private static boolean checkAccessToken(AccessToken accessToken){
if (accessToken != null) {
return accessToken.getToken() != null
&& !"".equals(accessToken.getToken())
&& System.currentTimeMillis() - accessToken.getCreateDate().getTime() < TOKEN_MAX_TIME;
}
return false;
}
/**
* 向微信介面請求 ticket
* </br><b>文件:</b> https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
* </br>附錄1-JS-SDK使用許可權簽名演算法
* @param accessToken
* @return
*/
private static ApiTicket getApiTicket(String accessToken, String ticketUrl) {
// String jsapi_ticket = null;
ApiTicket ticket = new ApiTicket();
try {
String responseText = httpGet(String.format(ticketUrl, accessToken));
// jsapi_ticket = null;
JSONObject object = JSONObject.fromObject(responseText);
if (object.containsKey("ticket")) {
ticket.setTicket(object.optString("ticket"));
ticket.setExpiresIn(object.optInt("expires_in"));
ticket.setErrcode(object.optString("errcode"));
ticket.setErrmsg(object.optString("errmsg"));
ticket.setCreateDate(new Date());
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return ticket;
}
/**
* 向微信介面請求 api_ticket
* </br><b>文件:</b> https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
* </br>附錄1-JS-SDK使用許可權簽名演算法
* @param accessToken
* @return
*/
private static ApiTicket getJsApiTicket(String accessToken) {
log.info("獲取 JsApiTicket 開始");
ApiTicket ticket = getApiTicket(accessToken, JS_API_TICKET_URL);
if (checkJsApiTicket(ticket)) {
log.info("獲取 JsApiTicket 成功!");
ServletContextUtil.get().setAttribute(JS_API_TICKET, ticket);// 快取全域性變數
}
return ticket;
}
/**
* 驗證本地快取的 JsApiTicket,如果有效,就返回 true 否則 false
* @param api_ticket
* @return
*/
private static boolean checkJsApiTicket(ApiTicket api_ticket){
if (api_ticket != null) {
return "ok".equals(api_ticket.getErrmsg())
&& System.currentTimeMillis() - api_ticket.getCreateDate().getTime() < TICKET_MAX_TIME;
}
return false;
}
/**
* 簽名:這個就是官方給的例子程式碼。直接用就行了。
* </br><b>文件:</b> https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
* </br> 【附錄6-DEMO頁面和示例程式碼】
* </br> <b>簽名演算法</b>
* 簽名生成規則如下:參與簽名的欄位包括noncestr(隨機字串), 有效的jsapi_ticket, timestamp(時間戳),
* url(當前網頁的URL,不包含#及其後面部分) 。對所有待簽名引數按照欄位名的ASCII 碼從小到大排序(字典序)後,
* 使用URL鍵值對的格式(即key1=value1&key2=value2…)拼接成字串string1。這裡需要注意的是所有引數名均
* 為小寫字元。對string1作sha1加密,欄位名和欄位值都採用