公众号开发精品教程(5)——获取用户基本信息与网页授权

系列文章传送门

公众号开发精品教程(1)——绪论及环境搭建

公众号开发精品教程(2)——将项目接入微信及简单交互

公众号开发精品教程(3)——创建菜单

公众号开发精品教程(4)——生成带参数的二维码及合成海报

公众号开发精品教程(5)——获取用户基本信息与网页授权

整个项目的源码已经上传到百度网盘(博主的Git在维护,就不拿出来丢人了),永久有效,免费,在ChatConf类中填写自己的APPID和开发者密钥,在相关地方替换一下外网域名,即可使用,如有任何问题,欢迎在下方评论:

链接:百度网盘传送门
提取码:03eb

目录

 

前言

阅读文档,理清思路

第一种:直接调用相关接口,通过openid拉取

第二种:通过网页授权的方式拉取

方式1——直接获取用户的基本信息

方式2——通过网页授权获取用户信息

优化小细节

总结


前言

在上一篇文章中,我们生成了带参数的二维码,合成了海报,并且模拟了扫码关注时的场景。但是这些依然不够,我们的公众号上线之后,总要知道谁关注了、以及每个关注者的信息,用于后期的数据消费。

回首我之前写的四篇文章,再加上今天的这一篇,其实我们已经涵盖微信公众号开发的95%以上的场景了:

关注时自动回复、发送消息时自动回复(图文、图片、视频等都可以,只不过之前的例子是回复文本消息)、上传素材、生成带参数的二维码、合成海报、创建菜单、获取用户的基本信息、网页授权的方式获取用户信息、扫码事件

以上的功能点虽然没有涵盖所有的微信API接口,但是已经满足大部分的场景需要了,剩下的功能和消费就靠我们项目中的设计了。而且,各位如果学会了微信开发的套路和流程,调几个API接口来新增功能,还不是探囊取物……

好了,话不多说,开始正题!

请大家将自己的主机映射到外网、登录到自己的测试号、打开微信公众平台文档。

阅读文档,理清思路

对于获取用户的基本信息,微信提供了两种方式:

第一种:直接调用相关接口,通过openid拉取

这是最简单的一种方式,我们只需要将access_token和用户的openid传入到接口参数中,就可获取该用户的基本信息。但是这个接口是有限制的,只有当用户关注了你的公众号,你才可以拉取到相关信息,否则只返回一个字段来通知你该用户未关注公众号。

看一下文档的对应部分:

 

第二种:通过网页授权的方式拉取

这种方式是需要网页授权的,它又分为两种形式:静默授权和非静默授权。

无论是静默授权还是非静默授权,用户授权之后会跳到我们设置的回调页面,并且微信会在回调页面传一个URL参数——code,我们拿到code之后,再调用微信的这个接口换取access_token和openid:

注意,这里的access_token,跟之前的access_token不一样,之前的是调用接口的凭证,这里的是换取用户信息的凭证。可以说,之前的access_token是共用的,是一个调用凭证,调取接口的地方都可以使用它 ;这里的access_token是私有的,只对应某一个用户,只可获得某个指定用户的信息,是一个用户凭证,而且一般的习惯是用完即丢,因为它只能获取某一个用户的信息,并且没有调用上限,完全没必要存起来

我们获取到access_token和openid之后,调用如下接口,就可以拉取用户的信息了:

 微信指出,当静默授权时,只能获取到用户的openid,要拉取用户的所有信息,scope需为snsapi_userinfo,也就是非静默授权,但其实这里有一个bug,不知道算不算漏洞(前提是你的公众号必须是正式号,并且已经是企业认证):

当静默授权时获取到code之后,调用上述接口,即使用户未关注公众号,仍然可以获取到用户信息!!!

静默授权,用户是无感知的,只是感觉页面跳了一下(因为先访问了微信的授权页面,静默方式下微信会直接跳到我们设置的回调页面);非静默授权,会弹一个框出来,提示用户是否同意授权,相信大家都见过。

总结一下两种方式的使用场景(一己之见)

直接获取用户信息的方式,一般发生在公众号内的场景,即用户已经关注的情况下,比如关注之后我们通过openid获取他的信息,然后写入到我们的数据库,用于后期数据消费;

网页授权的方式,一般用于在公众号中的网页,比如你的用户将一个活动链接分享到了朋友圈,你想统计都有谁看过这个活动,那么你就可以在这个页面进行网页授权,一旦有人进来就获取他的信息,然后入库(当然有点贱……,就说那个意思吧!)。

最后提醒一下大家,需要获取他人信息的地方,如果不是在公众号内的场景,最好还是使用非静默授权,明确告诉用户我要获取你的信息,不要恶意营销、恶意消费,做技术要有底线,小心吃官司……

方式1——直接获取用户的基本信息

其实就是调用一个接口,我这里就在用户关注公众号的时候,拉取一下用户的信息,各位可以根据自己的产品逻辑和业务场景,决定在哪些地方使用。

另外,获取用户信息的时候,我们依然采用阻塞队列异步实现,线上的话可以考虑RabbitMQ、Kafka等消息中间件。因为对于用户而言,信息入库这件事他并不关心,甚至写入时出错了,跟用户有什么关系呢?用户期待的就是立马得到响应,所以我们在用户关注公众号时,把他的openid写入到一个队列,然后主线程就直接返回消息。

题外话:大家一定要有良好的系统优化思维、架构思维,要想着怎样提高系统的吞吐量。我经常见到这种场景,一些业务的执行对于用户来说并不重要,但是好多人就是要把这些业务与用户的请求同步执行,导致用户的请求等待、挂起,系统的吞吐量根本上不去。

老规矩,把接口地址写到我们的配置类中:

//直接获取用户基本信息的接口
public static final String GET_INFO_URL = "https://api.weixin.qq.com/cgi-bin/user/info";

然后我们写一个工具类,负责获取用户的基本信息:

package com.blog.wechat.utils;

import com.blog.wechat.conf.ChatConf;
import com.blog.wechat.conf.SSLConf;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

import java.io.IOException;

/**
 *  获取用户基本信息的工具类
 * @author 秋枫艳梦
 * @date 2019-06-09
 * */
public class UserInfoUtil {

    /**
     *  根据openid直接获取用户的基本信息
     * @param openId 用户的openid
     * @return 获取到的信息,是一个JSON字符串
     * */
    public static String getInfoById(String openId){
        OkHttpClient client = null;
        Request request = null;
        Response response = null;
        String infoStr = "";

        try {
            client = new OkHttpClient.Builder()
                    .sslSocketFactory(SSLConf.getSslSocketFactory(),new SSLConf.TrustAllManager())
                    .hostnameVerifier(new SSLConf.TrustAllHost()).build();

            request = new Request.Builder()
                    .url(ChatConf.GET_INFO_URL+"?access_token="+ChatConf.getToken()+"&openid="+openId+"&lang=zh_CN")
                    .build();

            response = client.newCall(request).execute();
            if (response.isSuccessful()){
                infoStr = response.body().string();
            }
        }catch (IOException e){

        }finally {
            if (response!=null){
                response.close();
            }
            client.dispatcher().executorService().shutdown();
        }

        return infoStr;
    }
}

然后写一个阻塞队列的类,跟我们上一篇生成海报的阻塞队列差不多:

package com.blog.wechat.queue;

import com.blog.wechat.utils.UserInfoUtil;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

/**
 *  异步获取用户信息的阻塞队列类
 * @author 秋枫艳梦
 * @date 2019-06-09
 * */
public class UserInfoQueue {
    //存放openid的阻塞队列.openid即微信推送的数据包中的FromUserName
    public static BlockingQueue<String> userQueue = new LinkedBlockingDeque<>();
    //监听队列的线程数量,这里我们开启15个线程去处理(并不是越多越好),提高吞吐量
    public static final int THREADS = 15;

    /**
     *  监听阻塞队列,执行相关业务
     * */
    public static void startListen(){
        for (int i = 0; i < THREADS; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    while (true){
                        try {
                            String openId = userQueue.take();
                            String info = UserInfoUtil.getInfoById(openId);
                            System.out.println("获取到的用户信息:\n"+info);
                            //这里模拟一下即可
                            System.out.println("已写入数据库......");
                        }catch (Exception e){

                        }
                    }
                }
            };
            new Thread(runnable).start();
        }
    }
}

然后配置一下监听器,项目一启动就开始监听这个队列,这里就不再列出代码了,上一章已经列出过了。

最后,我们在用户关注的时候,往队列中写入一个openid,通知阻塞队列开始工作,修改部分代码:

注意:接口测试号是不能搜索到的,只能通过在测试号系统扫码关注,所以我们把这个事件放在扫码关注时

//如果是被关注事件,向用户回复内容,只需要将整理好的XML文本参数返回给微信即可
            if (map.get("Event").equals("subscribe")){
                //如果没有EventKey,说明是普通关注,否则是扫码关注事件
                String eventKey = map.get("EventKey");
                if (eventKey==null){
                    content = "欢迎关注秋枫艳梦的测试公众号!";
                }else {
                    String param = eventKey.substring(eventKey.indexOf("_")+1);
                    //为了简单,这里直接返回一句话,实际业务场景要更复杂
                    content = "您是由openid为"+param+"的用户引进来的,我们已对其进行了奖励,您也可以生成海报,分享给朋友,可获得奖励";
                    //写入到阻塞队列
                    try {
                        UserInfoQueue.userQueue.put(fromUser);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

运行一下项目,我们重新关注公众号:

可以看到,我们已经获取到用户的基本信息了。

方式2——通过网页授权获取用户信息

前面提到,网页授权有静默和非静默两种,博主这里就演示非静默方式的拉取,静默的跟这个一样,非静默只是弹一个窗口让用户去选择。其实,公众号内的操作大多是静默授权,我们有很多种方式去获取用户的信息,比如直接调用获取信息的接口,或者从我们的数据库查询(用户关注的时候我们已经入库了),这里只是做一下演示。

首先我们需要有一个HTML文件,网页授权嘛,首先得有网页啊。这里就继续使用我们之前的订单页面——home.html。

另外,微信的API接口是不支持跨域的,所以你没办法直接通过Ajax访问其接口拉取信息,微信也说了,必须在服务器端发起请求,所以我们的做法是前端拿到code之后,通过Ajax调用后端的接口,后端根据code去换取用户的信息,再返回给前端

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <!--不要忘了引jQuery-->
    <script src="../statics/js/jquery-3.3.1.js" type="text/javascript"></script>
</head>
<script>
    /**
     *
     *  获取URL中的参数
     * */
    function getUrlParam(url,name){
        var pattern = new RegExp("[?&]"+name+"\=([^&]+)", "g");
        var matcher = pattern.exec(url);
        var items = null;
        if(null != matcher){
            try{
                items = decodeURIComponent(decodeURIComponent(matcher[1]));
            }catch(e){
                try{
                    items = decodeURIComponent(matcher[1]);
                }catch(e){
                    items = matcher[1];
                }
            }
        }
        return items;
    }

    //app_id
    var APPID = "wx98166c786c5e760b";
    //回调页面即是当前页
    var destUrl = decodeURIComponent(location.href);
    //微信返回来的code
    var code = getUrlParam(location.href,"code");
    //如果当前URL参数中没有code,说明用户刚进来,还没有走微信授权
    if (code==null||code==""){
        //引导用户到授权页面,这里采用静默授权,用户是无感知的
        location.href = "https://open.weixin.qq.com/connect/oauth2/authorize?appid="+APPID+"&redirect_uri="+destUrl+"&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect";
    }else {
        //访问我们自己的接口,传入code,获取用户的信息之后展示到页面上
        $.ajax({
            url:"http://vxye92.natappfree.cc/user/get/info/"+code,
            dataType:"json",
            success:function (str) {
                $("#headImg").attr("src",str.headimgurl);
                $("#nickName").html(str.nickname);
            },
            error:function () {
                alert("错误");
            }
        });
    }
</script>
<body>
秋枫艳梦,公众号——订单页面
获取到您的基本信息:
头像:<img src="" id="headImg">
暱称:<span id="nickName"></span>
其余省略
</body>
</html>

现在前端写好了,我们把后端的部分补上。

首先,先把需要用到的两个接口保存起来:

//通过code换取网页授权access_token的接口地址
public static final String GET_USER_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid="+ChatConf.APPID+"&secret="+ChatConf.SECRET+"&code={code}&grant_type=authorization_code";
//根据网页授权access_token和openid换取用户信息的接口地址
public static final String GET_CODE_INFO_URL = "https://api.weixin.qq.com/sns/userinfo";

然后在我们刚才创建的UserInfoUtil类中,扩展一个方法,根据code获取用户的基本信息:

/**
     *  网页授权,根据code获取用户的信息
     * @param code 微信回调页面时传来的code
     * @return 获取到的信息
     * */
    public static String getInfoByCode(String code){
        OkHttpClient client = null;
        Request request = null;
        Response response = null;
        String infoStr = "";

        try {
            client = new OkHttpClient.Builder()
                    .sslSocketFactory(SSLConf.getSslSocketFactory(),new SSLConf.TrustAllManager())
                    .hostnameVerifier(new SSLConf.TrustAllHost()).build();

            //先根据code换取access_token和openid
            request = new Request.Builder().url(ChatConf.GET_USER_TOKEN_URL.replace("{code}",code)).get().build();
            response = client.newCall(request).execute();
            JSONObject jsonObject = JSON.parseObject(response.body().string());
            String accessToken = jsonObject.getString("access_token");
            String openId = jsonObject.getString("openid");

            //再根据access_token和openid,获取用户的基本信息
            request = new Request.Builder()
                    .url(ChatConf.GET_CODE_INFO_URL+"?access_token="+accessToken+"&openid="+openId+"&lang=zh_CN")
                    .build();
            response = client.newCall(request).execute();
            infoStr = response.body().string();
        }catch (IOException e){

        }finally {
            if (response!=null){
                response.close();
            }
            client.dispatcher().executorService().shutdown();
        }

        return infoStr;
    }

然后我们再创建一个控制器,供前端访问:

package com.blog.wechat.controller;

import com.alibaba.fastjson.JSON;
import com.blog.wechat.utils.UserInfoUtil;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping(value = "/user",produces = {"application/json;charset=utf-8"})
@ResponseBody
public class UserController {

    @RequestMapping(value = "/get/info/{code}")
    public Object getUserInfo(@PathVariable String code){
        return JSON.parseObject(UserInfoUtil.getInfoByCode(code));
    }
}

因为我内网穿透的域名变了,所以点击之前的菜单已经进入不到我的home.html了,所以我重新创建一下,大家也要注意一下这个问题。这里就不再贴出来代码了,不懂的可参考我之前的文章。

最后一步,需要在测试号配置回调域名,只有在这个域名下的连接,微信才能正确回调,如http://vxye92.natappfree.cc/home.html:

注意:这里不需要加http://。而且测试号直接这样配置就行,正式号还需要下载一个文本文件到你的网站根目录下,确保可以访问到才行,就行之前的http://vxye92.natappfree.cc/statics/img/back.jpg一样,确保定位到这个资源才行。

点击确认之后,我们重新运行项目,重新关注公众号,点击“我的订单”按钮:

因为我已经关注这个公众号了,所以哪怕指定非静默方式,微信也不会再弹出那个框了。由于测试号只能我们自己访问,大家可以试一下,把这个链接分享到你的朋友圈,然后你取消关注,再从朋友圈点击这个链接,就会提示你授权,你同意授权之后,一样可以拿到信息。

优化小细节

细心的朋友可以发现,当我们进入到“我的订单”页面后,点击返回按钮想要退出页面时,发现又重新授权然后进入到了这个页面,需要快速连续点击两次才能退出。

为什么?因为可以我们授权登录的操作,是通过在home.html页面通过location.href打开了微信的授权页面,但是微信回调之后,对于浏览器来说,上一级页面就是微信的授权页面,所以点击返回按钮浏览器就退回到了授权页面,然后授权页面再回调我们的home.html,如此往复……博主当时脑袋抽抽了,这么简单的问题都没想到,差点通宵……

那怎么办呢?因为我们这是公众号的一个菜单,菜单要跳转的页面肯定是不变的吧?那我们在创建菜单的时候,直接把“我的订单”按钮的url属性设置成微信的授权页不就行了么?

代码如下:

//我的订单URL,跳向当前项目中的页面
    private static String ORDER;
    static {
        try {
            ORDER = "https://open.weixin.qq.com/connect/oauth2/authorize?appid="+ChatConf.APPID+"&redirect_uri="+ URLEncoder.encode("http://vxye92.natappfree.cc/home.html","GBK") +"&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect";
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

重新创建菜单,再次点击“我的订单”,就会是一种不错的效果了!

总结

微信开发之旅,到这里已经进行的差不多了,可以满足大家的需要了。

如果博主发现新的大坑、或者有价值的东西,将持续更新!

欢迎关注、转载!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章