1. 程式人生 > >接入【微信JS-SDK】 坑多多

接入【微信JS-SDK】 坑多多

接入【微信JS-SDK】

用於實現自定義朋友圈分享的標題和圖示。

開始之前需要在公眾平臺進行一些設定:

  1. 請求 access_token 的服務ip 要新增進【後臺》基本設定》公眾號開發資訊》IP白名單】
  2. 接入JS-SDK的頁面域名要新增進【後臺》公眾號設定》功能設定》JS介面安全域名】列表(最多隻能加三個,每個月最多改三次)

js-sdk使用需要獲取簽名,驗證通過後方能呼叫各種【微信介面】

  1. 前臺將當前頁面(呼叫微信介面的頁面)url發給我方伺服器,或我方後臺接到頁面請求後自己取出請求的url
  2. 我方伺服器向微信方傳送 AppID、AppSecret(可登入公眾平臺查) 請求 access_token
  3. 再用 access_token 請求 ticket
  4. 拿到 ticket 如果按微信給的演算法進行簽名,這一步要用到1中提到的url(也就是簽名對頁面是唯一的,引數變化什麼的就得重新簽名了,想想我之前在地址後面加的那些 rand=math.random() 【想哭】)
  5. 將簽名返回給前端
  6. 前端頁面載入後,通過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加密,欄位名和欄位值都採用