前言
【杏仁】写在前面的话
有缘人:
您好!
首先,请原谅我把模块化思维用到写文章上,前言部分独立抽离出来写了篇文章,这样【杏仁】系列的文章就可以重复引用了。
偷懒,始终是程序员最棒的优点。
偷懒,是一门很高深的艺术,很容易误入歧途,请君三思而行!
趁青春未逝
无问东西
离开这里
没有对错
才能遇见
往后余生
你要的世界
——杏仁奶昔随笔
目录
注:菜单设置有锚点,点击可直达相关内容
正文
一、业务描述[1]
1、为什么要写这篇文章
2017年3月,微信对接口进行了更新,不再自动获取分享链接里第一张图进行小图显示,导致在微信分享链接的时候,小图丢失。
2、解决方案
第一:通过第三方软件二次分享,包括APP开发时调用第三方集成分享,通过浏览器二次分享。
第二:引入微信JS-SDK,通过微信公众号提供的分享接口,实现自定义分享样式。注意,微信公众号必须是企业认证的,所以个人开发者看到这里可以放弃了。不过,个人开发者还是有一些基础权限,比如获取经纬度,拉取扫一扫,语音翻译之类的功能,所以还是值得研究一下,因为涉及到接口签名这类的知识。
3、主要内容
本文主要讲解解决方法二
后端使用SpringBoot+MySQL搭建签名接口,为什么要用到数据库?因为微信提供的token两小时有效,每日获取次数有上限,所以要保存起来,实现每两个小时换一次token。其实不用也可以,主要是能有地方存这个token就好,比如Redis或者内存缓存都可以。
前端就随意一个HTML页面,引入JQ,用ajax请求获取接口数据即可。因为业务场景是,存储在OSS的html格式的文章新闻,分享到微信的时候,小图丢失。这里还涉及到跨域问题,所以后端接口要设置成跨域,正文会详细讲解。可以自行扩展到自己的业务,vue和angular都有异步请求的方法,通过接口返回数据,再利用微信的JS-SDK进行接入,即可实现功能。
二、准备工作[2]
1、注册微信公众号
官方链接:https://mp.weixin.qq.com/
一个邮箱能注册一个微信公众号,类型有:订阅号(个人公众号)、服务号(企业公众号)、小程序、企业微信。
注意:一个邮箱注册一个号,注册的类型无法再次变更。
个人开发,建议直接注册订阅号,里面有小程序,也是归属个人范围的。
企业开发,一定要注册服务号,手续流程会复杂一点,需要对公账户认证,但是拥有微信支付,链接分享等功能,前面也提到了,不是企业,是没有权限设置分享接口的。
2、微信开发文档
官方链接:https://mp.weixin.qq.com/wiki
JS-SDK开发文档:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
要学会使用开发文档,教程毕竟是有年限的,可能很多方法微信已经放弃了,有新的方法代替,所以还是要根据文档调整。
设置分享有个坑,现在明明用的1.4版本的JS-SDK,开发文档注明分享方法即将废弃,有新的方法代替,但是新方法只在分享QQ和QQ空间时生效了,然后加上老方法后微信才生效,不得不感慨,开发路上处处都是坑啊。
3、公众号设置
这里的坑也很多,接口获取签名的流程:
第一步,通过AppID和Secret(在公众号左边菜单:开发》基本配置中可以获取到)这两个参数获取token。
获取链接:
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=&secret=
把appid=和secret=分别填上,你以为这样就能获取到?天真!
填上后用浏览器直接访问,提示一段带IP的错误,其实就是xx.xx.xx.xxIP没有权限获取token,然后你要在公众号设置IP白名单,把这个IP添加进去,因为自己测试的话,你的IP基本上一两天就变动一次,所以每次都要设置一下白名单,真正上线运行的时候,服务器的IP是不变的,所以就没这个问题。
成功获取到token后不要关闭浏览器页面,这个token生效时间是7200秒,也就是2小时,后面具体实现就存数据库,每天有获取上限,所以不要猛得刷新获取。
第二步,通过获取的token获取ticket
获取链接:
https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=&type=jsapi"
在access_token=后面填写第一步获取到的token
会返回ticket这个值,有效期目测只有几分钟吧,所以这个值是每次调用都要重新获取的
第三步,通过获取的token和ticket,拼接参数后,进行sha1加密获取签名
这里需要用到几个参数,jsapi_ticket(第二步获取到的ticket)、noncestr(随机字符串)、timestamp(10位长度的时间戳)、url(当前访问的页面链接),顺序不要改,当然你可以改了之后再试试。
//这里是用java代码做一下演示
String jsapi_ticket = xxx;//第二步获取到的ticket
String timestamp = String.valueOf(System.currentTimeMillis()).substring(0,10);//时间戳
String noncestr = "Siner"+timestamp.substring(7);//随机字符串,这里其实可以固定,随便填个自己喜欢的,但是不建议
String sha1Str = "jsapi_ticket="+jsapi_ticket+"&noncestr="+noncestr+"×tamp="+timestamp+"&url="+url;//拼接的参数
String signature = DigestUtils.sha1Hex(sha1Str.getBytes());//根据参数进行sha1签名
到此,你就能获取到最重要的参数signature,签名(在不用代码演示时,你可以手动拼接一下上面的参数,然后在线加密一下就能获取签名)
sha1在线加密链接:
http://tool.oschina.net/encrypt?type=2
签名完成后,用官网提供的在线校验工具测试一下你自己加密出来的和官方加密出来的签名是不是一样的,如果不一样,一定是你拼接参数时有问题,不用怀疑,就是你的拼接有问题,包括大小写,或者不注意的空格。
签名校验地址:
https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign
好了,在完全不用任何编程的情况下,能获取到正确的签名,说明公众号的设置都没什么问题了,就能开始我们的编程之旅了。
注:还有一个坑,在 设置》公众号设置》功能设置 里,要添加js安全域名,只能填三个域名,这个域名就是你网站的域名,你微信分享出去的html页面链接,必须是这三个安全域名里面的,包括图片的链接,所以建议先用阿里的OSS试试水,将下面用到的前端页面存在OSS里。不清楚的可以参考我的另一篇文章《【杏仁】零基础之OSS搭建个人静态网站》
三、具体实现[3]
1、前端
我们先挑软柿子捏,简单的东西放前面
新建HTML文件,复制以下代码,完成,简单吧!
<html>
<head>
<title>杏仁奶昔</title>
</head>
<body>
<img src="https://www.jogjo.vip/static/img/logo/siner300x300.png" alt="logo" style="position: absolute; visibility: hidden;">
跨域请求调用,实现微信分享小图显示测试页
</body>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="https://res2.wx.qq.com/open/js/jweixin-1.4.0.js"></script>
<script type="text/javascript">
$(function () {
var shareUrl = window.location.href.split("#")[0];
var appId;
var timestamp;
var nonceStr;
var signature;
var shareTitle = document.title;
var desc = "杏仁奶昔,您的在线万能好友!";
var imgUrl = document.getElementsByTagName("img")[0].getAttribute("src");
if(document.getElementsByTagName("img").length>1){
imgUrl = document.getElementsByTagName("img")[1].getAttribute("src");
}
$.ajax({
type: "GET",
url: "http://www.xxx.com:8080/getSignature",
data: { url: shareUrl },
dataType: "json",
success: function (data) {
//这里只需要提供当前页面链接的URL,剩下数据全部由后端签名返回,后端可相应调整接口参数
console.log(JSON.stringify(data.result));
appId = data.appId;
timestamp = data.timestamp;
nonceStr = data.nonceStr;
signature = data.signature;
wechatJSDKSignature (appId,timestamp,nonceStr,signature,shareTitle,desc,shareUrl,imgUrl);
}
}
);
});
function wechatJSDKSignature (appId,timestamp,nonceStr,signature,shareTitle,desc,shareUrl,imgUrl){
wx.config({
debug: true,//开启微信调试模式,微信成功分享带图后可关闭
appId: appId,
timestamp: timestamp,
nonceStr: nonceStr,
signature: signature,
jsApiList: [
"updateAppMessageShareData",
"updateTimelineShareData",
"onMenuShareTimeline",
"onMenuShareAppMessage",
"getLocation"
]
});
wx.ready(function () {
wx.updateAppMessageShareData({
title: shareTitle,
desc: desc,
link: shareUrl,
imgUrl: imgUrl,
success: function () {
console.log("调用分享给朋友成功");
}
});
wx.updateTimelineShareData({
title: shareTitle,
link: shareUrl,
imgUrl: imgUrl,
success: function () {
console.log("调用分享到朋友圈成功");
}
});
wx.onMenuShareTimeline({
title: shareTitle,
desc: desc,
link: shareUrl,
imgUrl: imgUrl,
success: function () {
console.log("调用即将废弃的分享给朋友成功");
}
});
wx.onMenuShareAppMessage({
title: shareTitle,
link: shareUrl,
imgUrl: imgUrl,
success: function () {
console.log("调用即将废弃的分享到朋友圈成功");
}
});
wx.getLocation({
type: "wgs84",
success: function (res) {
var latitude = res.latitude;
var longitude = res.longitude;
var speed = res.speed;
var accuracy = res.accuracy;
console.log("当前坐标经纬度:" + latitude + "-" + longitude + "速度:" + speed + "精度:" + accuracy);
}
});
});
wx.error(function (res) {
console.log("程序错误");
});
}
</script>
</html>
解释一下为什么我会放一张隐藏图到body里,一因为很多第三方浏览器或软件将你的链接分享出去的时候,默认拿你的第一张图片来显示。其次是因为我们这个分享出去的链接,一般是一篇文章或者广告,如果带有图片,取第二张做显示,如果不带图片,就可以默认引用这张图来做显示,建议该图大小像素300*300以上。
还有 debug: true这个是在用微信打开你这个页面时,会弹窗提示有什么错误用的,每个方法都会弹一个窗口,第一个窗口比较重要,如果提示ok,说明你的签名正确,已经连上微信的接口了,后面的窗口就是提示你有没有权限调用微信的方法。前面也说到了,个人开发者调用分享的方法,是会报没有接口权限的,所以分享只适用于企业微信。
这里有个坑:js后面加了两个即将废弃的微信接口方法,因为1.4虽然是最新的,文档也标注可以用,但是!实际上还是只能用旧方法调用,截止这篇文章发布,1.4新方法只能用于QQ和QQ空间的分享。
注意 :页面有两个参数需要替换成你自己的
一是小图链接,必须是在JS安全域名里面能调用到的
二是ajax请求的服务器链接,因为后端设置了允许跨域,只要是公网IP能访问到就行,直接用浏览器访问,就能获取到数据
最后:把这个页面放到你的OSS里面,用你的域名访问一下,看看能不能打开这个页面,或者把这个页面上传到你的网站服务器上,其实本地也能开来看,但是用微信测试的时候,只能在局域网环境内。
所以最简便的方法还是用OSS,关于OSS域名备案之类的环节,不清楚的可以参考我的另一篇文章:《【杏仁】零基础之OSS搭建个人静态网站》
2、后端
关键的地方来了,因为前端只是一个页面,请求后端接口后,后端就把准备工作里面说到的获取签名流程实现,然后返回签名参数给页面,页面通过JS-SDK就能成功打通微信,实现微信分享等功能。
不懂SpringBoot的话,恕我直言......你可以用其他的后端语言开发这个接口,只要前端请求能获取到签名就行。
大致流程:
第一创建一个SpringBoot项目,如果在原有项目上扩展请忽略
第二创建Controller层,创建一个最基础的方法,自己调用一下,看看能不能成功
第三创建接口,并创建实现方法,这里因为自己用到mybatis这些东西,所以代码量还是很少的,后面贴出
关键位置的代码:
我把很多扩展技术去掉了,怕读者乱,以最简单可实现的样子放出,需要导什么jar包自己加就行。
Controller层:
/**
* 微信公众号相关
* @author JogJo
* @date 2019/4/3 14:21
* */
@RestController
@RequiredArgsConstructor
public class WeChatPublicController {
private final WxPublicService wxPublicService;
/**
* 获取签名
* @author JogJo
* @date 2019/4/3 14:21
* */
@GetMapping("getSignature")
@CrossOrigin
public Object getSignature (String url) throws Exception {
return this.wxPublicService.getSignature(url);;
}
}
Service层:
public interface WxPublicService extends BdBaseService<WxPublic> {
/**
* 获取URL的签名
* @author JogJo
* @date 2019/4/3 14:21
* */
Map<String,Object> getSignature(String url);
}
ServiceImpl层:
因为用了Mybatis,所以这里直接复制是用不了的,用到的几个jar包放出,缺什么加什么,建议用maven,开发工具用IDEA,缺的依赖有提示,可以自动加的,IDEA参考我另一篇文章:【杏仁】Windows下IntelliJ IDEA的安装与配置
import com.bossdream.library.configure.wxpay.WxPublicProperties;
import com.bossdream.library.domain.defined.base.BdBaseServiceImpl;
import com.bossdream.library.domain.entity.wechat.BdWxPublic;
import com.bossdream.library.domain.mapper.wechat.BdWxPublicMapper;
import lombok.RequiredArgsConstructor;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.LinkedHashMap;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class WxPublicServiceImpl extends BdBaseServiceImpl<WxPublicMapper, WxPublic> implements WxPublicService{
private final BdWxPublicMapper wxPublicMapper;
private final WxPublicProperties wxPublicProperties;
@Override
public Map<String,Object> getSignature(String url){
Map<String,Object> map = new LinkedHashMap<>();
if(StringUtils.isNotBlank(url)){
BdWxPublic bdWP = new BdWxPublic();
bdWP.setStatus(1);
BdWxPublic wxp = this.wxPublicMapper.selectOne(bdWP);
if(wxp!=null){
String jsapi_ticket = wxp.getTicket();
//最后更新时间超过2小时就重新获取授权信息
if(System.currentTimeMillis() - wxp.getUpdateTime() > 7200000){
String getTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid="+wxPublicProperties.getAppid()+"&secret="+wxPublicProperties.getSecret()+"";//获取token的链接
Map<String,Object> tokenBackMap = new RestTemplate().getForObject(getTokenUrl,Map.class);
String access_token = String.valueOf(tokenBackMap.get("access_token"));//获取到的token
String getTicketUrl = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token="+access_token+"&type=jsapi";//获取ticket的链接
Map<String,Object> ticketBackMap = new RestTemplate().getForObject(getTicketUrl,Map.class);
jsapi_ticket = String.valueOf(ticketBackMap.get("ticket"));//获取到的ticket
if(access_token.equals("null")||StringUtils.isBlank(access_token)){
map.put("signature","获取签名失败,获取token的IP地址未授权");
return map;
}
wxp.setAccessToken(access_token);
wxp.setTicket(jsapi_ticket);
wxp.setUpdateTime(System.currentTimeMillis());
this.wxPublicMapper.updateById(wxp);//更新数据库的值
}
String timestamp = String.valueOf(System.currentTimeMillis()).substring(0,10);
String noncestr = "BD"+timestamp.substring(7);
String sha1Str = "jsapi_ticket="+jsapi_ticket+"&noncestr="+noncestr+"×tamp="+timestamp+"&url="+url;//拼接值
System.out.println(sha1Str);
String signature = DigestUtils.sha1Hex(sha1Str.getBytes());//根据参数进行sha1签名
map.put("appId",wxPublicProperties.getAppid());
map.put("timestamp",timestamp);
map.put("nonceStr",noncestr);
map.put("signature",signature);
}else{
map.put("signature","请创建数据库基础数据");
}
}else{
map.put("signature","获取签名失败");
}
return map;
}
}
数据库
可以用缓存或者其他方法实现
创建了一个表,字段有以下这些:
#实体类
@Data
@ApiModel(value = "微信公众号定时获取token专用")
public class WxPublic {
private Long id;
private String accessToken;
private String ticket;
private Integer status;
private Long updateTime;
}
#数据库表创建语句
CREATE TABLE `wx_public` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`access_token` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '根据appid和secret获取的token,微信规定两小时内有效,最后更新时间超过两小时重新请求',
`ticket` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '根据token获取到的ticket',
`update_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '最后更新时间,两小时内不执行更新',
`status` int(1) NOT NULL DEFAULT '0' COMMENT '状态:1生效',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1 COMMENT='微信公众号定时获取token专用';
这里解释一下:直接复制代码肯定会报错的,要理解原理,这个接口是干什么的,前端要获取到什么,就行了,具体解决可以参考我的。具体项目肯定还有什么乱七八糟的权限,安全,但是你懂得原理,解决这个问题就是易如反掌。
看到代码是不是有疑问,跨域你没解决啊,其实Controller层加了个注解@CrossOrigin就已经解决了单接口跨域问题,是不是很简单!而且不需要用jsonp!如果你用旧框架,或者其他框架,可能要配置各种拦截器,给接口加跨域头,重新拼接参数之类的,但是!在SpringBoot里解决跨域,就是一行代码!JAVA天下无敌!Spring牛批!
当然,这个位置可以留给怎么配置全局跨域的扩展内容>>《跨域相关知识点预留位》
好了,下面看看链接分享的效果:
还是要总结一下:
微信太惨无人道了有木有!为了这么个破图标!搞那么多东西!
过去
只需要一个<img>
现在
需要一个<B/S>
想回到过去
试着让接口继续
至少不再让图离我而去
分散时间的注意
这次更新得更难
这样挽留不知还来不来得及
想回到过去
四、相关拓展[4]
费了那么大劲!就这样?不不不
微信公众号还有很多好东西,个人开发者可以通过订阅号,发表自己的文章,那个编辑器还是很友好的,至少我用起来还不错,要不要试试微信搜索一下公众号:杏仁奶昔
其实公众号只有一篇文章233,我还是喜欢简书这种Markdown风的编辑器,在简书写文章也方便点,微信太多限制了,文章也不能随便插入外链,发表一些小说,鸡汤文还行,技术文章?算了吧,糟心!
当然公众号也有他的优势,用户群多,用来打广告啊,嘿嘿嘿!
另一个扩展就是,微信小程序,现在还是很严格的,个人开发者的小程序必须要有实在的功能,不然审核时是很难通过的。
还有一个很重要的拓展是,微信的支付,如果开发移动APP,那微信支付是少不了的,这个内容感觉可以放以后说,因为这个东西真的糟心,付款和收款还要不同账号,每笔单都交手续费还搞成这样,还是支付宝人性化。
腾讯很多对外开放平台其实都有很多问题,比如邮箱订阅,像死了一样,申请也没反应,估计是废弃掉了。然后各种业务也是说关就关了,真怕哪天QQ突然就关了,想想我里面还存着很多相片。。。
以前的时光总是美好的!
如今的惨淡也是后来的美好!
所以我安慰自己
后来人
你们太惨了!
作者 @杏仁奶昔
2019 年 04月 03 日