系列文章传送门
整个项目的源码已经上传到百度网盘(博主的Git在维护,就不拿出来丢人了),永久有效,免费,在ChatConf类中填写自己的APPID和开发者密钥,在相关地方替换一下外网域名,即可使用,如有任何问题,欢迎在下方评论:
链接:百度网盘传送门
提取码:03eb
目录
前言
在上一篇文章中,我们生成了带参数的二维码,合成了海报,并且模拟了扫码关注时的场景。但是这些依然不够,我们的公众号上线之后,总要知道谁关注了、以及每个关注者的信息,用于后期的数据消费。
回首我之前写的四篇文章,再加上今天的这一篇,其实我们已经涵盖微信公众号开发的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();
}
}
重新创建菜单,再次点击“我的订单”,就会是一种不错的效果了!
总结
微信开发之旅,到这里已经进行的差不多了,可以满足大家的需要了。
如果博主发现新的大坑、或者有价值的东西,将持续更新!
欢迎关注、转载!