系列文章傳送門
整個項目的源碼已經上傳到百度網盤(博主的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();
}
}
重新創建菜單,再次點擊“我的訂單”,就會是一種不錯的效果了!
總結
微信開發之旅,到這裏已經進行的差不多了,可以滿足大家的需要了。
如果博主發現新的大坑、或者有價值的東西,將持續更新!
歡迎關注、轉載!