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

版权声明:大家好,我是笨笨,笨笨的笨,笨笨的笨,转载请注明出处,谢谢! https://blog.csdn.net/jx520/article/details/84640526

接入【微信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加密,字段名和字段值都采用原始值,不进行URL 转义。
	 * @param jsapi_ticket
	 * @param url
	 * @return
	 */
	private static Map<String, String> sign(String jsapi_ticket, String url) {
		Map<String, String> ret = new HashMap<String, String>();
		String nonce_str = create_nonce_str();
		String timestamp = create_timestamp();
		String string1;
		String signature = "";

		//注意这里参数名必须全部小写,且必须有序
		string1 = "jsapi_ticket=" + jsapi_ticket +
				"&noncestr=" + nonce_str +
				"&timestamp=" + timestamp +
				"&url=" + url;
		try	{
			MessageDigest crypt = MessageDigest.getInstance("SHA-1");
			crypt.reset();
			crypt.update(string1.getBytes("UTF-8"));
			signature = byteToHex(crypt.digest());
		}
		catch (NoSuchAlgorithmException e)
		{
			log.error(e.getMessage(), e);
		}
		catch (UnsupportedEncodingException e)
		{
			log.error(e.getMessage(), e);
		}

		//ret.put("url", url);
		//ret.put("jsapi_ticket", jsapi_ticket);
		ret.put("appId", APPID);
		ret.put("nonceStr", nonce_str);
		ret.put("timestamp", timestamp);
		ret.put("signature", signature);
		ret.put("string1", string1);
		ret.put("jsapi_ticket", new StringBuffer(jsapi_ticket).reverse().toString());

		log.debug(ret);
		return ret;
	}

	private static String byteToHex(final byte[] hash) {
		Formatter formatter = new Formatter();
		for (byte b : hash)
		{
			formatter.format("%02x", b);
		}
		String result = formatter.toString();
		formatter.close();
		return result;
	}

	private static String create_nonce_str() {
		return UUID.randomUUID().toString();
	}

	private static String create_timestamp() {
		return Long.toString(System.currentTimeMillis() / 1000);
	}
	
	//--------------------------------------------------------------------------------------------------
	/**
	 * 首先尝试从缓存获取 access_token,如果token无效就向服务器请求重新生成</br>
	 * <b>文档:</b>https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183
	 * @return
	 */
	public static synchronized AccessToken getAccessToken() {
		AccessToken token = (AccessToken)ServletContextUtil.get().getAttribute(ACCESS_TOKEN);
		
		//判断 token 如果过期就重新生成,否则直接返回。
		if(checkAccessToken(token)){
			return token;
		}else{
			log.info("AccessToken invalid");
			return getAccessToken(APPID, APPSECRET);
		}
	}
	
	public static String getAccessTokenStr() {
		AccessToken token = (AccessToken)ServletContextUtil.get().getAttribute(ACCESS_TOKEN);
		//判断 token 如果过期就重新生成,否则直接返回。
		if(token != null){
			return token.getToken();
		}else{
			log.info("AccessToken invalid");
			return "";
		}
	}
	
	/**
	 * 首先尝试从缓存获取 api_ticket,如果token无效就向服务器请求重新生成</br>
	 * </br><b>文档:</b> https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
	 * </br>附录1-JS-SDK使用权限签名算法
	 * @return
	 */
	public static synchronized String getJsApiTicket() {
		ApiTicket jsApi_ticket = (ApiTicket)ServletContextUtil.get().getAttribute(JS_API_TICKET);

		if (checkJsApiTicket(jsApi_ticket)) {
			return jsApi_ticket.getTicket();
		}else{
			log.info("JsApiTicket invalid");
			return getJsApiTicket(getAccessToken().getToken()).getTicket();
		}
	}
	
	/**
	 * JSSDK使用步骤,步骤三:通过config接口注入权限验证配置
	 * 获取 wx.config({此处所需要的参数});
	 * @param url
	 * @return
	 */
	public static Map<String, String> getWxConfigParam(String url) {
		String jsApiTicket = getJsApiTicket();
		return sign(jsApiTicket == null ? "" : jsApiTicket, url);
	}
	
}

class AccessToken {
	private String token; // 成功时返回
	private int expiresIn; // 成功时返回
	private String errcode; // 失败时返回
	private String errmsg; // 失败时返回
	private Date createDate;
	
	public String getToken() {
		return token;
	}
	public void setToken(String token) {
		this.token = token;
	}
	public int getExpiresIn() {
		return expiresIn;
	}
	public void setExpiresIn(int expiresIn) {
		this.expiresIn = expiresIn;
	}
	public String getErrcode() {
		return errcode;
	}
	public void setErrcode(String errcode) {
		this.errcode = errcode;
	}
	public String getErrmsg() {
		return errmsg;
	}
	public void setErrmsg(String errmsg) {
		this.errmsg = errmsg;
	}
	public Date getCreateDate() {
		return createDate;
	}
	public void setCreateDate(Date createDate) {
		this.createDate = createDate;
	}
	
}

class ApiTicket {
	private String errcode; //"errcode":0,
	private String errmsg; //"errmsg":"ok",
	private String ticket;
	private int expiresIn; //"expires_in":7200
	private Date createDate;
	
	public String getErrcode() {
		return errcode;
	}
	public void setErrcode(String errcode) {
		this.errcode = errcode;
	}
	public String getErrmsg() {
		return errmsg;
	}
	public void setErrmsg(String errmsg) {
		this.errmsg = errmsg;
	}
	public String getTicket() {
		return ticket;
	}
	public void setTicket(String ticket) {
		this.ticket = ticket;
	}
	public int getExpiresIn() {
		return expiresIn;
	}
	public void setExpiresIn(int expiresIn) {
		this.expiresIn = expiresIn;
	}
	public Date getCreateDate() {
		return createDate;
	}
	public void setCreateDate(Date createDate) {
		this.createDate = createDate;
	}
	
}

猜你喜欢

转载自blog.csdn.net/jx520/article/details/84640526