转自:https://www.cnblogs.com/backtozero/p/7064247.html
前言
刚进入一家新公司,接到的第一个任务就是需要需要自定义微信分享的效果(自定义缩略图,标题,摘要),一开始真是一脸懵逼,在网上搜索了半天之后大概有了方案。值得注意的是一开始搜索到的解决方案全是调用微信的自带的JS-SDK,然而腾讯是不会让广大吃瓜群众这么轻而易举的调用他们的东西的。微信开发团队已经把调用的权限收回,现在无法直接在页面直接调用JS-SDK了。话不多说,直接上干货。
预期效果
原始的分享效果:
使用微信JS-SDK的分享效果:
可以看出缩略图,标题,摘要样式良好,给用户的体验很好。
准备工作
微信官方开发者文档地址:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
现在的思路已经很明确了,就是通过调用微信的JS-SDK实现自定义分享效果。但是这个调用过程比较繁琐,需要提前准备如下东西:
(1)微信服务号一个,并且已经通过了实名认证;
没有实名认证的话,一些接口没有调用权限。
(2)一个ICP备案的域名;
这个域名需要设置为微信公众号后台的JS接口安全域名,否则微信仍然不允许调用它的接口。
这时大家应该就犯难了,这样的话岂不是不能在本地测试,只能部署到生产环境才能测试?不用着急,解决方案告诉大家:花生壳的内网穿透服务(收费,20元以内)
花生壳官网:http://hsk.oray.com/price/#personal
选择个人免费版就可以了,虽然说是免费版,但是其实注册过程中还是要收几块钱的,因为我自己买了域名和流量所以花的钱更多一些,但也在20元以内。不建议大家购买流量,送的流量可以用很久了。
当准备好上面提到的就可以开始敲代码了。
(3)安装微信开发者工具,用于本地调试。
下载地址:https://mp.weixin.qq.com/debug/cgi-bin/webdebugger/download?from=mpwiki&os=x64
官方使用教程:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1455784140
具体步骤
(1)查看AppId,AppSecret以及绑定域名
进入微信后台,找到下面的菜单
获取AppID和AppSecret
设置JS接口安全域名
注意第三步,如果微信服务器不能在我们的服务器*问到这个txt文件,域名是无法设置成功的,这里先告诉大家在哪里设置,想要成功设置域名还需要使用花生壳的服务,让微信服务器访问我们本地工程中的的txt文件才行。
hkh3321313.vicp.io是在花生壳上购买的域名,免费送的域名是在太难记了,完全不能忍。
(2)引入JS文件
这里需要注意是http还是https,如果生产环境是https,务必前缀是https,都则会出现mix content这样的错误,导致引入失败。
<script typet="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>
(3)通过AppId和AppSecret请求accessToken,然后通过accessToken获取jsapi_ticket,生成config接口所需参数
因为获取这两个参数的次数是有限制的(accessToke 每日2000次,jsapi_ticket 每日100000次),有效期是7200秒,每两小时请求一次就行啦,把获取的accessToke和jsapi_ticket保存在后台,所以accessToken和jsapi_ticket这两个参数的获取是通过ajax方式请求后台,而不是实时去获取的。
config几个参数需要详细说明一下:
- timestamp 生成签名的时间戳 create_nonce_str()
- nonceStr 随机生成的字符串 create_timestamp()
- signature 按照微信文档签名算法生成的签名 makeWXTicket()
附上signature算法的官方说明:
https://mp.weixin.qq.com/wiki?action=doc&id=mp1421141115&t=0.15697429783636763#buzhou3
在附录1中可以找到详细说明。
此外,官方提供了一个签名算法的校验工具:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign
下面只附上了主要的方法:
//获取accessToken private JSONObject getAccessToken(){ //String accessTokenUrl= https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET String requestUrl = accessTokenUrl.replace("APPID",appId).replace("APPSECRET",appSecret); log.info("getAccessToken.requestUrl====>"+requestUrl); JSONObject result = HttpUtil.doGet(requestUrl); return result ; } //获取ticket private JSONObject getJsApiTicket(){ //String apiTicketUrl= https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi String requestUrl = apiTicketUrl.replace("ACCESS_TOKEN", accessToken); log.info("getJsApiTicket.requestUrl====>"+requestUrl); JSONObject result = HttpUtil.doGet(requestUrl); return result; } //生成微信权限验证的参数 public Map<String, String> makeWXTicket(String jsApiTicket, String url) { Map<String, String> ret = new HashMap<String, String>(); String nonceStr = createNonceStr(); String timestamp = createTimestamp(); String string1; String signature = ""; //注意这里参数名必须全部小写,且必须有序 string1 = "jsapi_ticket=" + jsApiTicket + "&noncestr=" + nonceStr + "×tamp=" + timestamp + "&url=" + url; log.info("String1=====>"+string1); try { MessageDigest crypt = MessageDigest.getInstance("SHA-1"); crypt.reset(); crypt.update(string1.getBytes("UTF-8")); signature = byteToHex(crypt.digest()); log.info("signature=====>"+signature); } catch (NoSuchAlgorithmException e) { log.error("WeChatController.makeWXTicket=====Start"); log.error(e.getMessage(),e); log.error("WeChatController.makeWXTicket=====End"); } catch (UnsupportedEncodingException e) { log.error("WeChatController.makeWXTicket=====Start"); log.error(e.getMessage(),e); log.error("WeChatController.makeWXTicket=====End"); } ret.put("url", url); ret.put("jsapi_ticket", jsApiTicket); ret.put("nonceStr", nonceStr); ret.put("timestamp", timestamp); ret.put("signature", signature); ret.put("appid", appId); 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 createNonceStr() { return UUID.randomUUID().toString(); } //生成时间戳 private static String createTimestamp() { return Long.toString(System.currentTimeMillis() / 1000); }
HttpUtil代码
public class HttpUtil { public static Log logger = LogFactory.getLog(HttpUtil.class); //get请求 public static com.alibaba.fastjson.JSONObject doGet(String requestUrl) { CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = null; String responseContent = null; com.alibaba.fastjson.JSONObject result = null; try { //创建Get请求, HttpGet httpGet = new HttpGet(requestUrl); //执行Get请求, response = httpClient.execute(httpGet); //得到响应体 HttpEntity entity = response.getEntity(); //获取响应内容 responseContent = EntityUtils.toString(entity,"UTF-8"); //转换为map result = JSON.parseObject(responseContent); } catch (IOException e) { logger.error("HttpUtil=====Start"); logger.error(e.getMessage(),e); logger.error("HttpUtil=====End"); } return result; } }
(4)通过config接口注入权限验证配置
官方示例:
wx.config({ debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: '', // 必填,公众号的唯一标识
timestamp: , // 必填,生成签名的时间戳 nonceStr: '', // 必填,生成签名的随机串
signature: '',// 必填,签名,见附录1 jsApiList: [] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2 });
自己的代码:
其中的url不能硬编码写在后台,必须通过动态传递。
$(function(){ var url = location.href.split('#').toString();//url不能写死 $.ajax({ type : "get", url : "/wechatParam", dataType : "json", async : false, data:{url:url}, success : function(data) { wx.config({ debug: false,////生产环境需要关闭debug模式 appId: data.appid,//appId通过微信服务号后台查看 timestamp: data.timestamp,//生成签名的时间戳 nonceStr: data.nonceStr,//生成签名的随机字符串 signature: data.signature,//签名 jsApiList: [//需要调用的JS接口列表 'checkJsApi',//判断当前客户端版本是否支持指定JS接口 'onMenuShareTimeline',//分享给好友 'onMenuShareAppMessage'//分享到朋友圈 ] }); }, error: function(xhr, status, error) { //alert(status); //alert(xhr.responseText); } }) });
(5)通过ready接口处理成功验证
官方示例:
wx.ready(function(){ // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。 });
自己的代码:
wx.ready(function () { var link = window.location.href; var protocol = window.location.protocol; var host = window.location.host; //分享朋友圈 wx.onMenuShareTimeline({ title: '这是一个自定义的标题!', link: link, imgUrl: protocol+'//'+host+'/resources/images/icon.jpg',// 自定义图标 trigger: function (res) { // 不要尝试在trigger中使用ajax异步请求修改本次分享的内容,因为客户端分享操作是一个同步操作,这时候使用ajax的回包会还没有返回. //alert('click shared'); }, success: function (res) { //alert('shared success'); //some thing you should do }, cancel: function (res) { //alert('shared cancle'); }, fail: function (res) { //alert(JSON.stringify(res)); } }); //分享给好友 wx.onMenuShareAppMessage({ title: '这是一个自定义的标题!', // 分享标题 desc: '这是一个自定义的描述!', // 分享描述 link: link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致 imgUrl: protocol+'//'+host+'/resources/images/icon.jpg', // 自定义图标 type: 'link', // 分享类型,music、video或link,不填默认为link dataUrl: '', // 如果type是music或video,则要提供数据链接,默认为空 success: function () { // 用户确认分享后执行的回调函数 }, cancel: function () { // 用户取消分享后执行的回调函数 } }); wx.error(function (res) { alert(res.errMsg); }); });
到这里所有的代码都已经分享完毕了。
(6)启动花生壳的内网穿透服务,设置JS接口安全域名
这个基本是傻瓜式的,只要下载他们的客户端就可以了。
官网教程:http://hsk.oray.com/news/4345.html
添加一个映射就可以了
把之前下载的txt文件放在工程目录webapp下,然后本地启动工程,确定通过域名可以访问本地项目后,设置JS安全域名
现在访问 域名:端口号(例如:hkh3321313.vicp.io:8080)就可以访问本地项目啦。
(7)使用微信开发者工具测试
微信开发者工具其实就是微信的浏览器,其中集成了chrome的调试工具,前面提到wx.config中的debug模式这里就发挥作用了,浏览器会自动弹出调用微信接口的返回结果。
成功返回的话结果应该是ok什么的,图就不上了。提醒大家,上生产环境一定要把debug改为false~
后记
虽然已经给了主要的代码,大家一定还是不想写接口,下面附上完整的代码,如果你觉得解了燃眉之急,就点个顶吧,哈哈哈~
@Controller public class WeChatController { private final Logger log = LoggerFactory.getLogger(this.getClass()); //获取相关的参数,在application.properties文件中 @Value("${wechat.appId}") private String appId; @Value("${wechat.appSecret}") private String appSecret; @Value("${wechat.url.accessToken}") private String accessTokenUrl; @Value("${wechat.url.apiTicket}") private String apiTicketUrl; //微信参数 private String accessToken; private String jsApiTicket; //获取参数的时刻 private Long getTiketTime = 0L; private Long getTokenTime = 0L; //参数的有效时间,单位是秒(s) private Long tokenExpireTime = 0L; private Long ticketExpireTime = 0L; //获取微信参数 @RequestMapping("/wechatParam") @ResponseBody public Map<String, String> getWechatParam(String url){ //当前时间 long now = System.currentTimeMillis(); log.info("currentTime====>"+now+"ms"); //判断accessToken是否已经存在或者token是否过期 if(StringUtils.isBlank(accessToken)||(now - getTokenTime > tokenExpireTime*1000)){ JSONObject tokenInfo = getAccessToken(); if(tokenInfo != null){ log.info("tokenInfo====>"+tokenInfo.toJSONString()); accessToken = tokenInfo.getString("access_token"); tokenExpireTime = tokenInfo.getLongValue("expires_in"); //获取token的时间 getTokenTime = System.currentTimeMillis(); log.info("accessToken====>"+accessToken); log.info("tokenExpireTime====>"+tokenExpireTime+"s"); log.info("getTokenTime====>"+getTokenTime+"ms"); }else{ log.info("====>tokenInfo is null~"); log.info("====>failure of getting tokenInfo,please do some check~"); } } //判断jsApiTicket是否已经存在或者是否过期 if(StringUtils.isBlank(jsApiTicket)||(now - getTiketTime > ticketExpireTime*1000)){ JSONObject ticketInfo = getJsApiTicket(); if(ticketInfo!=null){ log.info("ticketInfo====>"+ticketInfo.toJSONString()); jsApiTicket = ticketInfo.getString("ticket"); ticketExpireTime = ticketInfo.getLongValue("expires_in"); getTiketTime = System.currentTimeMillis(); log.info("jsApiTicket====>"+jsApiTicket); log.info("ticketExpireTime====>"+ticketExpireTime+"s"); log.info("getTiketTime====>"+getTiketTime+"ms"); }else{ log.info("====>ticketInfo is null~"); log.info("====>failure of getting tokenInfo,please do some check~"); } } //生成微信权限验证的参数 Map<String, String> wechatParam= makeWXTicket(jsApiTicket,url); return wechatParam; } //获取accessToken private JSONObject getAccessToken(){ //String accessTokenUrl = https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET String requestUrl = accessTokenUrl.replace("APPID",appId).replace("APPSECRET",appSecret); log.info("getAccessToken.requestUrl====>"+requestUrl); JSONObject result = HttpUtil.doGet(requestUrl); return result ; } //获取ticket private JSONObject getJsApiTicket(){ //String apiTicketUrl = https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi String requestUrl = apiTicketUrl.replace("ACCESS_TOKEN", accessToken); log.info("getJsApiTicket.requestUrl====>"+requestUrl); JSONObject result = HttpUtil.doGet(requestUrl); return result; } //生成微信权限验证的参数 public Map<String, String> makeWXTicket(String jsApiTicket, String url) { Map<String, String> ret = new HashMap<String, String>(); String nonceStr = createNonceStr(); String timestamp = createTimestamp(); String string1; String signature = ""; //注意这里参数名必须全部小写,且必须有序 string1 = "jsapi_ticket=" + jsApiTicket + "&noncestr=" + nonceStr + "×tamp=" + timestamp + "&url=" + url; log.info("String1=====>"+string1); try { MessageDigest crypt = MessageDigest.getInstance("SHA-1"); crypt.reset(); crypt.update(string1.getBytes("UTF-8")); signature = byteToHex(crypt.digest()); log.info("signature=====>"+signature); } catch (NoSuchAlgorithmException e) { log.error("WeChatController.makeWXTicket=====Start"); log.error(e.getMessage(),e); log.error("WeChatController.makeWXTicket=====End"); } catch (UnsupportedEncodingException e) { log.error("WeChatController.makeWXTicket=====Start"); log.error(e.getMessage(),e); log.error("WeChatController.makeWXTicket=====End"); } ret.put("url", url); ret.put("jsapi_ticket", jsApiTicket); ret.put("nonceStr", nonceStr); ret.put("timestamp", timestamp); ret.put("signature", signature); ret.put("appid", appId); 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 createNonceStr() { return UUID.randomUUID().toString(); } //生成时间戳 private static String createTimestamp() { return Long.toString(System.currentTimeMillis() / 1000); } }
页面js解决二次分享问题:
<script typet="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>
<script type="text/javascript">
$(function(){
refreshUrl();//刷新页面去除一次分享带的参数
var url = location.href.split('#').toString();//url不能写死
$.ajax({
type : "get",
url : "/wechatParam",
dataType : "json",
async : false,
data:{url:url},
success : function(result) {
var data = result.data;
wx.config({
debug: false,////生产环境需要关闭debug模式
appId: data.appid,//appId通过微信服务号后台查看
timestamp: data.timestamp,//生成签名的时间戳
nonceStr: data.nonceStr,//生成签名的随机字符串
signature: encodeURIComponent(data.signature),//签名
jsApiList: [//需要调用的JS接口列表
'checkJsApi',//判断当前客户端版本是否支持指定JS接口
'onMenuShareTimeline',//分享给好友
'onMenuShareAppMessage'//分享到朋友圈
]
});
},
error: function(xhr, status, error) {
}
});
var invite_uid = fnGetQueryString('invite_uid');
wx.ready(function () {
var link = window.location.href;
var protocol = window.location.protocol;
var host = window.location.host;
//分享朋友圈
wx.onMenuShareTimeline({
title: '这是自定义标题',
link: location.href.split("#")[0]+'?invite_uid=' + invite_uid,
imgUrl: '',// 自定义图标
trigger: function (res) {
//alert('click shared');
},
success: function (res) {
//alert('shared success');
//some thing you should do
},
cancel: function (res) {
//alert('shared cancle');
},
fail: function (res) {
//alert(JSON.stringify(res));
}
});
//分享给好友
wx.onMenuShareAppMessage({
title: “这是自定义标题”, // 分享标题
desc: “这是自定义描述”, // 分享描述
link: location.href.split("#")[0]+'?invite_uid=' + invite_uid, // 分享链接,该链接域名或路径必须与当前页面对应
imgUrl: '', // 自定义图标
type: 'link', // 分享类型,music、video或link,不填默认为link
dataUrl: '', // 如果type是music或video,则要提供数据链接,默认为空
success: function () {
// 用户确认分享后执行的回调函数
},
cancel: function () {
// 用户取消分享后执行的回调函数
}
});
wx.error(function (res) {
//alert(res.errMsg);
});
});
});
function fnGetQueryString(key) { //正则获取url后面的参数值,如?env=dev&exp=123
var reg = new RegExp("(^|&)" + key + "=([^&]*)(&|$)");
var result = window.location.search.substr(1).match(reg);
return result ? decodeURIComponent(result[2]) : false;
};
function funcUrlDel(name) {//删除url指定参数名并返回新的url
var loca = window.location;
var baseUrl = loca.origin + loca.pathname + "?";
var query = loca.search.substr(1);
if (query.indexOf(name) > -1) {
var obj = {};
var arr = query.split("&");
for (var i = 0; i < arr.length; i++) {
arr[i] = arr[i].split("=");
obj[arr[i][0]] = arr[i][1];
};
delete obj[name];
var url = baseUrl + JSON.stringify(obj).replace(/[\"\{\}]/g, "").replace(/\:/g, "=").replace(/\,/g, "&");
return url
};
};
function getlinkSearch(key, reqStr) {
var result = reqStr.substr(1).match(reg);
return result ? decodeURIComponent(result[2]) : false;
};
function refreshUrl() {//强制刷新到不带二次分享参数页面
var url = "",
reqStr = "";
if (fnGetQueryString('from')) {//from为微信二次分享自带参数
url = funcUrlDel('from');
var reqIndex = url.indexOf('?');
reqStr = url.substr(reqIndex);//截取去除from参数后的地址
if (getlinkSearch('isappinstalled', reqStr)) {//isappinstalled为微信二次分享自带参数
url = url.substr(0, url.indexOf('?'));//截取去除isappinstalled参数后的地址
window.location.href = url;
} else {
window.location.href = url;
}
}
};
</script>