自定义分享是微信分享的自我定制化实现,需要了解JSSDK的开发规范
一、自定义分享简介
先看官方文档
微信JS-SDK是微信公众平台面向网页开发者提供的基于微信内的网页开发工具包。
通过使用微信JS-SDK,网页开发者可借助微信高效地使用拍照、选图、语音、位置等手机系统的能力,同时可以直接使用微信分享、扫一扫、卡券、支付等微信特有的能力,为微信用户提供更优质的网页体验。
此文档面向网页开发者介绍微信JS-SDK如何使用及相关注意事项。步骤一:绑定域名
先登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。
备注:登录后可在“开发者中心”查看对应的接口权限。步骤二:引入JS文件
在需要调用JS接口的页面引入如下JS文件,(支持https):
http://res.wx.qq.com/open/js/jweixin-1.2.0.js
备注:支持使用 AMD/CMD 标准模块加载方法加载步骤三:通过config接口注入权限验证配置
所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用(同一个url仅需调用一次,对于变化url的SPA的web app可在每次url变化时进行调用,目前Android微信客户端不支持pushState的H5新特性,所以使用pushState来实现web app的页面会导致签名失败,此问题会在Android6.2中修复)。
wx.config({
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: ”, // 必填,公众号的唯一标识
timestamp: , // 必填,生成签名的时间戳
nonceStr: ”, // 必填,生成签名的随机串
signature: ”,// 必填,签名,见附录1
jsApiList: [] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
});步骤四:通过ready接口处理成功验证
wx.ready(function(){
// config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
});步骤五:通过error接口处理失败验证
wx.error(function(res){
// config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
});接口调用说明
所有接口通过wx对象(也可使用jWeixin对象)来调用,参数是一个对象,除了每个接口本身需要传的参数之外,还有以下通用参数:
1.success:接口调用成功时执行的回调函数。
2.fail:接口调用失败时执行的回调函数。
3.complete:接口调用完成时执行的回调函数,无论成功或失败都会执行。
4.cancel:用户点击取消时的回调函数,仅部分有用户取消操作的api才会用到。
5.trigger: 监听Menu中的按钮点击时触发的方法,该方法仅支持Menu中的相关接口。
备注:不要尝试在trigger中使用ajax异步请求修改本次分享的内容,因为客户端分享操作是一个同步操作,这时候使用ajax的回包会还没有返回。以上几个函数都带有一个参数,类型为对象,其中除了每个接口本身返回的数据之外,还有一个通用属性errMsg,其值格式如下:
调用成功时:”xxx:ok” ,其中xxx为调用的接口名
用户取消时:”xxx:cancel”,其中xxx为调用的接口名
调用失败时:其值为具体错误信息
基础接口
判断当前客户端版本是否支持指定JS接口
wx.checkJsApi({
jsApiList: [‘chooseImage’], // 需要检测的JS接口列表,所有JS接口列表见附录2,
success: function(res) {
// 以键值对的形式返回,可用的api值true,不可用为false
// 如:{“checkResult”:{“chooseImage”:true},”errMsg”:”checkJsApi:ok”}
}
});
备注:checkJsApi接口是客户端6.0.2新引入的一个预留接口,第一期开放的接口均可不使用checkJsApi来检测。分享接口
请注意不要有诱导分享等违规行为,对于诱导分享行为将永久回收公众号接口权限,详细规则请查看:朋友圈管理常见问题 。获取“分享到朋友圈”按钮点击状态及自定义分享内容接口
wx.onMenuShareTimeline({
title: ”, // 分享标题
link: ”, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: ”, // 分享图标
success: function () {
// 用户确认分享后执行的回调函数
},
cancel: function () {
// 用户取消分享后执行的回调函数
}
});获取“分享给朋友”按钮点击状态及自定义分享内容接口
wx.onMenuShareAppMessage({
title: ”, // 分享标题
desc: ”, // 分享描述
link: ”, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: ”, // 分享图标
type: ”, // 分享类型,music、video或link,不填默认为link
dataUrl: ”, // 如果type是music或video,则要提供数据链接,默认为空
success: function () {
// 用户确认分享后执行的回调函数
},
cancel: function () {
// 用户取消分享后执行的回调函数
}
});
以上是官方文档的描述,前提条件有两个(1)配置js安全域名;(2)在使用分享的页面中引入js文件,有了这是前提条件,我们就可以实现下面的开发操作了。
二、开发
先定义一个分享的bean保存可能用到的分享字段,bean定义名字为WechatJSShareBean:
package com.wtp.wechat.bean;
/**
* @ClassName: WechatJSShareBean
* @Description: 微信分享 微信好友/朋友圈分享...模型
* @author tianpengw
* @date 2017年10月11日 上午10:49:31
*
*/
public class WechatJSShareBean {
/**
* 微信公众号ID
*/
private String appId;
/**
* 时间戳
*/
private String timestamp;
/**
* 指定长度的随机字符串
*/
private String nonceStr;
/**
* 签名
*/
private String signature;
/**
* 分享标题
*/
private String title;
/**
* 分享描述 【分享给朋友有此字段】
*/
private String desc;
/**
* 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
*/
private String link;
/**
* 分享图标
*/
private String imgUrl;
/**
* 分享类型 music、video或link,不填默认为link 【分享给朋友有此字段】
*/
private String type;
/**
* 如果type是music或video,则要提供数据链接,默认为空 【分享给朋友有此字段】
*/
private String dataUrl;
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
public String getTimestamp() {
return timestamp;
}
public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}
public String getNonceStr() {
return nonceStr;
}
public void setNonceStr(String nonceStr) {
this.nonceStr = nonceStr;
}
public String getSignature() {
return signature;
}
public void setSignature(String signature) {
this.signature = signature;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public String getLink() {
return link;
}
public void setLink(String link) {
this.link = link;
}
public String getImgUrl() {
return imgUrl;
}
public void setImgUrl(String imgUrl) {
this.imgUrl = imgUrl;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getDataUrl() {
return dataUrl;
}
public void setDataUrl(String dataUrl) {
this.dataUrl = dataUrl;
}
}
不用多讲,这些字段都提取自两类分享所需的数据里,再看下前提展示
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<script src="https://res.wx.qq.com/open/js/jweixin-1.2.0.js"> </script>
<script type="text/javascript">
wx.config({
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: '${commonShare.appId}', //必填,公众号的唯一标识
timestamp: '${commonShare.timestamp}', //必填,生成签名的时间戳
nonceStr: '${commonShare.nonceStr}', // 必填,生成签名的随机串
signature: '${commonShare.signature}', // 必填,签名,见附录1
jsApiList: [ // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
'checkJsApi',
'onMenuShareTimeline',
'onMenuShareAppMessage'
]
});
wx.ready(function(){
// config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
wx.checkJsApi({
jsApiList: [
'onMenuShareTimeline',
'onMenuShareAppMessage'
],
success: function (res) {
//alert(JSON.stringify(res));
}
});
// 监听“分享给朋友”,按钮点击、自定义分享内容及分享结果接口
wx.onMenuShareAppMessage({
title: '${commonShare.title}', // 分享标题
desc: '${commonShare.desc}', // 分享描述
link: '${commonShare.link}', // 分享链接
imgUrl: '${commonShare.imgUrl}', // 分享图标
type: 'link', // 分享类型,music、video或link,不填默认为link
dataUrl: '', // 如果type是music或video,则要提供数据链接,默认为空
trigger: function (res) {
// 不要尝试在trigger中使用ajax异步请求修改本次分享的内容,因为客户端分享操作是一个同步操作,这时候使用ajax的回包会还没有返回
//alert('用户点击发送给朋友');
},
success: function (res) {
//alert('已分享');
},
cancel: function (res) {
//alert('已取消');
},
fail: function (res) {
//alert(JSON.stringify(res));
}
});
// 监听“分享到朋友圈”按钮点击、自定义分享内容及分享结果接口
wx.onMenuShareTimeline({
title: '${commonShare.title}', // 分享标题
link: '${commonShare.link}', // 分享链接
imgUrl: '${commonShare.imgUrl}', // 分享图标
trigger: function (res) {
// 不要尝试在trigger中使用ajax异步请求修改本次分享的内容,因为客户端分享操作是一个同步操作,这时候使用ajax的回包会还没有返回
//alert('用户点击分享到朋友圈');
},
success: function (res) {
//alert('已分享');
},
cancel: function (res) {
//alert('已取消');
},
fail: function (res) {
//alert(JSON.stringify(res));
}
});
});
wx.error(function(res){
// config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
jQAlert('微信分享异常:'+res);
});
</script>
这里的commonShare就是WechatJSShareBean对象实例化的内容,我将此代码生成单独jsp文件(名为wechatShare.jsp),在需要的时候调用它,部分代码如下:
<c:if test="${isWechat }">
<%@ include file="wechatShare.jsp"%>
</c:if>
再来看下后台实现代码片段:
if (HttpHelper.isWechatBrowser(req)) {// 是微信浏览器
WechatJSShareBean wjsb = new WechatJSShareBean();
String title = MyConstants.project_title;
wjsb.setTitle(title);
wjsb.setImgUrl(p.getProductImgStr());
wjsb.setDesc(p.getProductName());
WechatUtil.wechatShareConfig(wjsb, req);
mv.addObject("commonShare", wjsb);
mv.addObject("isWechat", true);
}
判断是否微信浏览器如果是且有自定义分享需求则初始化分享bean,最后返回到前台页面,让我们在回顾下这个wechatShareConfig方法(在公众号支付那章我已经展示过代码,不过没有讲解)
/**
*
* @Description: 获取微信jsapi_ticket
* @author tianpengw
* @return
*/
private static JSAPITicket getJsApiTicket(){
JSAPITicket ticket = new JSAPITicket();
BaseAccessToken token = getBaseToken();
if(wechatCacheMap.containsKey("jsApiTicket") && null != wechatCacheMap.get("jsApiTicket")){
Long expireTime = (Long) wechatCacheMap.get("ticketExpireMillis");
if(new Date().getTime() <= expireTime){//ticket 仍然有效
ticket.setTicket((String)wechatCacheMap.get("jsApiTicket"));
ticket.setErrcode("0");
ticket.setErrmsg("ok");
ticket.setExpires_in(7200);
return ticket;
}
}
String url = jsApiTicketUrl.replace("ACCESS_TOKEN", token.getAccess_token());
String ticketResp = HttpHelper.httpsRequest(url, "POST", null);
if(!CommonUtil.isEmpty(ticketResp)){
Gson json=new Gson();
ticket = json.fromJson(ticketResp,JSAPITicket.class);
if(!"".equals(token.getAccess_token())){
wechatCacheMap.put("jsApiTicket", ticket.getTicket());
wechatCacheMap.put("ticketExpireMillis", (new Date().getTime() + expiresInMillisecond));
}
}
return ticket;
}
/**
*
* @Description: 微信自定义分享配置
* @author tianpengw
* @param commonShare
* @param req
*/
public static void wechatShareConfig(WechatJSShareBean commonShare, HttpServletRequest req){
String timestamp = Long.toString(System.currentTimeMillis() / 1000);
commonShare.setAppId(WechatUtil.appid);
commonShare.setTimestamp(timestamp);//获取时间戳
commonShare.setNonceStr(CommonUtil.getUUID());//获取指定长度的随机字符串
commonShare.setLink(HttpHelper.getFullRequestUrl(req));//签名用的url必须是调用JS接口页面的完整URL
JSAPITicket ticket = getJsApiTicket();
Map<String, String> params = new HashMap<String, String>();
params.put("noncestr", commonShare.getNonceStr());
params.put("timestamp", commonShare.getTimestamp());
params.put("url", HttpHelper.getFullRequestUrl(req));//签名用的url必须是调用JS接口页面的完整URL
params.put("jsapi_ticket", ticket.getTicket());
commonShare.setSignature(SignatureUtil.signatures(params,"SHA1"));
}
获得全地址工具方法
/**
*
* @Description: 根据req对象获得当前请求地址,带参数
* @author tianpengw
* @param req
* @return
*/
public static String getFullRequestUrl(HttpServletRequest req){
String url = req.getRequestURL().toString();
String queryStr = req.getQueryString();
if(CommonUtil.isEmpty(queryStr)){
return url;
}
return url + "?" + queryStr;
}
签名工具方法
/**
*
* @Description: 微信签名算法
* @author tianpengw
* @param params 参数格式
* @param signatureType 加密类型 SHA1/MD5,默认MD5
* @return
*/
public static String signatures(Map<String, String> params, String signatureType){
String str="";
try {
List<String> paramsStr = new ArrayList<String>();
for (String key : params.keySet()) {
paramsStr.add(key);
}
Collections.sort(paramsStr);
StringBuilder sbff = new StringBuilder();
for (String kk : paramsStr) {
String value = params.get(kk);
if (CommonUtil.isEmpty(sbff.toString())) {
sbff.append(kk + "=" + value);
} else {
sbff.append("&" + kk + "=" + value);
}
}
if("SHA1".equals(signatureType)){
str = SHA1(sbff.toString());
}else{
str = MD5(sbff.toString());
}
} catch (Exception e) {
e.printStackTrace();
}
return str;
}
好了代码写完了,开始介绍关键字段,先看下官方文档吧
附录1-JS-SDK使用权限签名算法
jsapi_ticket
生成签名之前必须先了解一下jsapi_ticket,jsapi_ticket是公众号用于调用微信JS接口的临时票据。正常情况下,jsapi_ticket的有效期为7200秒,通过access_token来获取。由于获取jsapi_ticket的api调用次数非常有限,频繁刷新jsapi_ticket会导致api调用受限,影响自身业务,开发者必须在自己的服务全局缓存jsapi_ticket 。
1.参考以下文档获取access_token(有效期7200秒,开发者必须在自己的服务全局缓存access_token):../15/54ce45d8d30b6bf6758f68d2e95bc627.html
2.用第一步拿到的access_token 采用http GET方式请求获得jsapi_ticket(有效期7200秒,开发者必须在自己的服务全局缓存jsapi_ticket):https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi
成功返回如下JSON:
{
“errcode”:0,
“errmsg”:”ok”,
“ticket”:”bxLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA”,
“expires_in”:7200
}
获得jsapi_ticket之后,就可以生成JS-SDK权限验证的签名了。签名算法
签名生成规则如下:参与签名的字段包括noncestr(随机字符串), 有效的jsapi_ticket, timestamp(时间戳), url(当前网页的URL,不包含#及其后面部分) 。对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。这里需要注意的是所有参数名均为小写字符。对string1作sha1加密,字段名和字段值都采用原始值,不进行URL 转义。
即signature=sha1(string1)。 示例:
noncestr=Wm3WZYTPz0wzccnW
jsapi_ticket=sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg
timestamp=1414587457
url=http://mp.weixin.qq.com?params=value步骤1. 对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1:
jsapi_ticket=sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg&noncestr=Wm3WZYTPz0wzccnW×tamp=1414587457&url=http://mp.weixin.qq.com?params=value步骤2. 对string1进行sha1签名,得到signature:
0f9de62fce790f9a083d5c99e95740ceb90c27ed
注意事项
1.签名用的noncestr和timestamp必须与wx.config中的nonceStr和timestamp相同。
2.签名用的url必须是调用JS接口页面的完整URL。
3.出于安全考虑,开发者必须在服务器端实现签名的逻辑。
如出现invalid signature 等错误详见附录5常见错误及解决办法。
上面提到两个关键的地方 1)jsapi_ticket的获取方法;2)sign签名方式需采用SHA1方式。对应到上面我贴出的代码片段,相信有Java基础的网友们肯定一目了然,通过以上的配置,我们将实现自定义微信分享,至于微信提供的其他的一批批接口,按照API的提示,相信你也都能完全实现的
至此微信自定义分享的方法介绍完毕,希望你能从其中获得收益。下一张将详细展示下我整理的jar包,如果感兴趣的网友,请继续阅读