【杏仁】SpringBoot接口跨域实现微信分享链接自定义小图

前言


【杏仁】写在前面的话

有缘人:
  您好!
  首先,请原谅我把模块化思维用到写文章上,前言部分独立抽离出来写了篇文章,这样【杏仁】系列的文章就可以重复引用了。
  偷懒,始终是程序员最棒的优点。
  偷懒,是一门很高深的艺术,很容易误入歧途,请君三思而行!


趁青春未逝
无问东西
离开这里
没有对错
才能遇见
往后余生
你要的世界
                           ——杏仁奶昔随笔


列车带着我向前 是离你更近的距离

目录


一、业务描述
二、准备工作
三、具体实现
四、相关拓展

注:菜单设置有锚点,点击可直达相关内容

正文


一、业务描述[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+"&timestamp="+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搭建个人静态网站》


1.gif

三、具体实现[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+"&timestamp="+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突然就关了,想想我里面还存着很多相片。。。

以前的时光总是美好的!

如今的惨淡也是后来的美好!

所以我安慰自己

后来人

你们太惨了!

www.siner.vip

作者 @杏仁奶昔
2019 年 04月 03 日


  1. 业务描述部分

  2. 准备工作部分

  3. 具体实现部分

  4. 相关拓展部分

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335