前言
本以为有微信公众平台开发文档,自定义分享样式还不简单?带着这样的想法,经历了两天半的踩坑经历,我写下了这篇文章。
开发分享接口在目录中: 微信网页开发 --> 微信SDK说明文档中
流程摘要
一、按照文档,第一步要绑定安全域名,即“JS接口安全域名”(此处标记1号坑)
二、第二步引入JS文件,需要下载并将其上传到服务器。这里要注意,在第一步中绑定的域名(路径)要能够访问到这个文件
三、第三步通过config接口注入权限验证配置。这里面的参数全部都要从后端获取,我的方案是服务端渲染。如果验证不通过最有可能的原因是签名算法错误。此步骤后端有很多的工作要做:先获取access_token,再根据它获取jsapi_ticket,然后再写签名算法进行计算得出签名,才将这些数据渲染到前端页面上。(此处标记踩坑点)
四、第四步ready和error接口,并且将分享接口写在ready内。
正文
一、关于JS接口安全域名。
最理想的是绑定网站地址,例如www.baidu.com。如果暂时不知道如何办成,可以先绑一个路径,如www.baidu.com/mp。
为什么这里是1号坑?因为你如果绑定的是路径,你要分享的链接一定要在绑定的这个路径下,即使是同一个域名,不在这个路径下,对不起,签名非法。
所以如果你想让该域名下所有的链接都可以用上自定义分享样式,就要将其绑定网站地址。
二、关于config接口。主要的工作在后端:
1.获取access_token。(二号大坑,内含若干小坑)
微信公众平台有两种access_token,一种可以用来第三方登录,另一种就是JS-SDK使用。获取它需要你的开发者ID和密码。
此处要注意,(一)服务号和企业号是不一样的!
(1)ID和密码不一样。服务号是appID和appSecret,而企业号叫做corpid和corpsecret
(2)获取token的链接不一样,前者是api开头,后者是qyapi开头。我做的是服务号(虽然认证主体也是企业),地址是https://api.weixin.qq.com/cgi-bin/token
(二)两种access_token要区别清楚
(1)请求地址和参数不一样,前者地址是https://api.weixin.qq.com/sns/oauth2/access_token 后者是https://api.weixin.qq.com/cgi-bin/token。此处我们用的是后者
(2)请求次数不一样。前者可以无限次获取,后者获取次数非常有限(具体次数不详),官方建议自己设缓存,且有效期7200秒。
(三)如果access_token获取不对,下一步会报错,官方有提供测试获取token
2.获取jsapi_ticket(三号大坑)
确保token获取正确后,我们来获取ticket。同样地,
(1)要区别服务号和企业号,因为请求地址都不一样,此处是https://api.weixin.qq.com/cgi-bin/ticket/getticket(不含参数)
(2)获取次数有限,有效期同7200S,需设缓存。
3.签名算法
这里不多说,官方有提供签名算法的验证检测。
三、一切都完成以后
调试没有报错,测试分享成功,本以为大功告成,可又出了一些问题?
- IOS端分享正常,Android端却还是自定义之前的老样子
- 无论是IOS端还是Android端,点击第一次分享出来的链接,再次分享,又回到了老样子
解决:
- 问题出在微信平台。分享接口有很多个,我只用了updateAppMessageShareData和updateTimelineShareData,后面即将废弃的几个接口既没有声明也没有使用。这看上去没问题,可这就是问题所在(测试人员的微信都是7.0版本,JS-SDK版本是1.4.0)。解决办法是将所有分享接口都在config中声明。
- 二次分享问题。原因在第一次分享以后腾讯在原url上加了一些参数,导致新的url与原来的不符,与计算签名时的url不同,签名无效。解决办法是前端第一行js代码判断当前url是否是原url,如果不是则跳转到原url。
四、代码贡上
Python Django后端:
def get_accesstoken(): "获取access_token" appid = '************' appsecret = '***********' def http_get_token(conn, now_time): "向服务器获取token" try: url = u'https://api.weixin.qq.com/cgi-bin/token' params = { 'appid': appid, 'secret': appsecret, 'grant_type': 'client_credential' } res = requests.get(url, params=params).json() access_token = res['access_token'] # 存redis缓存 conn.hmset("access_token", {"token": access_token, "timestamp": now_time}) return access_token except Exception as e: traceback.print_exc() return HttpResponse(json.dumps('access_token获取失败:%s' % e)) # 首先取缓存 conn = redis.StrictRedis(host='127.0.0.1', port='6379') redis_token = conn.hgetall("access_token") now_time = time.time() if redis_token: # 如果有token缓存 redis_time = float(str(redis_token[b"timestamp"], encoding="utf-8")) if now_time - redis_time < 6000: # 如果token没过期 access_token = str(redis_token[b"token"], encoding="utf-8") else: # 如果已过期,重新获取 access_token = http_get_token(conn, now_time) else: # 如果没有缓存,创建一个 access_token = http_get_token(conn, now_time) return access_token def get_jsapiticket(access_token): "获取 jsapi_ticket" def http_get_jsapi(conn, now_time): try: url2 = u'https://api.weixin.qq.com/cgi-bin/ticket/getticket' params = { 'access_token': access_token, 'type': 'jsapi' } res2 = requests.get(url2, params=params).json() if res2["errmsg"] == "ok": jsapi_ticket = res2["ticket"] conn.hmset("jsapi", {"ticket": jsapi_ticket, "timestamp": now_time}) return jsapi_ticket except Exception as e: traceback.print_exc() return HttpResponse(json.dumps('jsapi获取失败:%s' % e)) # 首先取缓存 conn = redis.StrictRedis(host='127.0.0.1', port='6379') redis_jsapi = conn.hgetall("jsapi") now_time = time.time() if redis_jsapi: # 如果有token缓存 redis_time = float(str(redis_jsapi[b"timestamp"], encoding="utf-8")) if now_time - redis_time < 6000: # 如果token没过期 jsapi_ticket = str(redis_jsapi[b"ticket"], encoding="utf-8") else: # 如果已过期,重新获取 jsapi_ticket = http_get_jsapi(conn, now_time) else: # 如果没有缓存,创建一个 jsapi_ticket = http_get_jsapi(conn, now_time) return jsapi_ticket def share(url): "自定义分享" appid = '***********' # 获取access_token access_token = get_accesstoken() # 获取jsapi_ticket jsapi_ticket = get_jsapiticket(access_token) if jsapi_ticket is not None: # 签名算法 class Sign: def __init__(self, jsapi_ticket, url): self.ret = { 'nonceStr': self.__create_nonce_str(), 'jsapi_ticket': jsapi_ticket, 'timestamp': self.__create_timestamp(), 'url': url } def __create_nonce_str(self): return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(15)) def __create_timestamp(self): return int(time.time()) def sign(self): string = '&'.join(['%s=%s' % (key.lower(), self.ret[key]) for key in sorted(self.ret)]).encode( encoding="utf-8") self.ret['signature'] = hashlib.sha1(string).hexdigest() return self.ret sign = Sign(jsapi_ticket, url) we_share = sign.sign() we_share.setdefault("appid", appid) return we_share else: print("jsapi为空,share没有返回值")
前端:
<script> // 自定义数据 var title = "{{ news.0.N_Title }}"; var link = window.location.href.split("?")[0]; var imgUrl = "http://www.smcic.cn/static/static_img/share_icon.png"; var desc = $(".post-content").text().replace(/\s+/g, "").replace(/[\r\n]/g,"").substr(0,50); if(link !== window.location.href) // 二次分享处理 { window.location.href = link; } wx.config({ debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: "{{ we_share.appid }}", // 必填,公众号的唯一标识 timestamp: parseInt("{{ we_share.timestamp }}"), // 必填,生成签名的时间戳 nonceStr: "{{ we_share.nonceStr }}", // 必填,生成签名的随机串 signature: "{{ we_share.signature }}", // 必填,签名 jsApiList: [ //使用的JS接口列表,如果需要其他的功能,再添加对应api 'updateAppMessageShareData', 'updateTimelineShareData', 'onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ', 'onMenuShareQZone', 'onMenuShareWeibo', ] }); wx.ready(function () { wx.updateAppMessageShareData({ title: title, // 分享标题 link: link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致 desc: desc, imgUrl: imgUrl, // 分享图标 success: function () { // 用户确认分享后执行的回调函数 }, cancel: function () { // 用户取消分享后执行的回调函数 } }); wx.updateTimelineShareData({ title: title, // 分享标题 desc: desc, // 分享描述 link: link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致 imgUrl: imgUrl, // 分享图标 type: '', // 分享类型,music、video或link,不填默认为link dataUrl: '', // 如果type是music或video,则要提供数据链接,默认为空 success: function () { // 用户确认分享后执行的回调函数 }, cancel: function () { // 用户取消分享后执行的回调函数 } }); }); wx.error(function (res) { console.log("失败",res); }); </script>