本文收錄於 Github.com/niumoo/JavaNotes,Java 系列文件,資料結構與演算法!
本文收錄於網站:https://www.wdbyte.com/,我的公眾號:程式猿阿朗
引言
想象一下,週五晚上,你開啟電腦,打算刷一刷最新上線的劇集,突然彈出網站登入,哎呀,那個超級複雜的密碼是什麼來著?那一堆數字、字母和符號的大雜燴在我腦海中有好幾個版本?能不能有一種簡單的方式,不用密碼就可以認真登入,這簡直不要太棒?這時掃碼登入出現了,它不僅方便而且更加安全。好比你向安保亮了一下你的 VIP 通行證,便放你透過。
微信作為國民級應用,微信掃碼登入再常見不過了,它就像你的口袋裡的萬能鑰匙,去哪兒都不怕。不過微信掃碼登入也有多種方式,如掃碼授權登入,掃碼關注公眾號登入等。這篇文章一起聊聊微信公眾號二維碼登入是怎麼回事,它的工作流程是什麼,它怎麼保證你的身份安全。以及,如果你是一個開發者,如何在自己的網站上增加掃碼登入。
公眾號掃碼登入優勢
快捷方便
使用者只需開啟微信掃一掃,幾秒鐘內就能完成登入。這簡化了傳統的輸入使用者名稱和密碼的繁瑣過程,不過前提是你已經安裝了必要的 APP。
增強安全性
掃碼登入的身份認證在服務端完成,而且如微信公眾號掃碼登入這種方式,網站只需要拿到一個使用者身份標識用於識別使用者,不需要儲存使用者的額外資訊。非常安全。
提升使用者粘性
透過掃描二維碼登入把使用者引入公眾號關注,在登入的同時還可以為公眾號引流,提升使用者粘性,同時公眾號是一個非常方便的使用者觸達方式,未來新功能的釋出可以及時送達使用者。
公眾號掃碼登入實現原理
要想理清公眾號掃碼登入的實現原理,首先要知道在掃碼登入過程中,有哪些參與方,它們之間的工作流程是怎麼樣的。這裡的參與方有使用者、瀏覽器、網站服務端、微信服務端四個參與方,總體的工作流程如下圖,下面會進行詳細介紹。
使用者:使用者是掃碼登入的發起方,點選登入,然後掃描登入二維碼。
瀏覽器:瀏覽器為使用者展示二維碼,然後不斷的輪詢掃碼狀態。
服務端:網站服務端需要向微信服務端獲取攜帶 Ticket 資訊的公眾號二維碼,在微信服務端回撥時繫結使用者身份資訊。
微信服務端:使用者掃碼後,會請求到微信服務端,微信服務端會攜帶掃描的二維碼的 Ticket 和使用者身份標識回撥網站服務端。
微信服務端回撥網站服務端時,攜帶的使用者身份資訊其實只是一串無意義字串,但是微信可以保證的是同一個微信使用者掃碼時攜帶的身份資訊字元是相同的,以此識別使用者。也因此公眾號掃碼登入用作身份認證非常安全。
開發準備工作
公眾號
首先你要有用於掃碼登入的微信公眾號,微信公眾平臺提供了測試平臺,可以直接生成測試公眾號。
微信測試號:https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index
開發者文件
獲取公眾號二維碼的過程需要參考微信公眾號官方文件,下面幾篇內容需要重點關注。
-
公眾號介面接入指南
連結:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html
-
獲取 Access Token
Access Token 是公眾號的全域性唯一介面呼叫憑據,公眾號呼叫各介面時都需使用 Access Token 。每次獲取有效期目前為2個小時,需定時重新整理,重複獲取將導致上次獲取的 Access Token 失效。
連結:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
-
生成帶 Ticket 二維碼
使用該介面可以獲得多個帶不同場景值的二維碼,使用者掃描後,公眾號可以接收到事件推送。
連結:https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html
-
接收事件推送
在使用者掃碼後微信服務端會回撥網站服務端,開發者需要按照指定訊息格式對訊息進行驗證處理。如獲取二維碼的 Ticket。
連結:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html
-
回覆文字訊息
如果想要在使用者掃碼完成後自動響應如 “登入成功” 之類的提示語,需要參考此文件。
連結:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Passive_user_reply_message.html
網站服務端
想要微信服務端成功回撥,服務端必須外網可以訪問,同時微信限制了埠只能是 80 或 443 ,因此只有兩種選擇 。
- 擁有自己的雲伺服器(這裡推薦我司阿里雲伺服器,如果只是學習體驗,搶佔式例項低配置一小時也就2毛錢左右,可以用於測試)。
- 用內網穿透軟體生成外網代理(一般 80 埠都需要收費)。
具體開發
配置微信公眾號
可以在微信測試號平臺上配置用於測試。
連結:https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index
專案總體結構
專案使用 SpringBoot 3.2.3 + Java 21
進行開發,這裡為了方便演示,在一個 Maven 專案中完成所有程式碼。本文步驟中只會給出關鍵程式碼部分,完整程式碼可以在文末的 GitHub 地址中找到。
下面是專案總體結構:
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── wdbyte
│ └── weixin
│ ├── SpringBootApp.java
│ ├── config
│ │ └── JwtFilter.java # JWT 身份認證攔截器
│ ├── controller
│ │ ├── WeixinServerController.java # 微信服務端呼叫介面
│ │ └── WeixinUserController.java # 瀏覽器呼叫介面
│ ├── model
│ │ ├── ApiResult.java
│ │ ├── ReceiveMessage.java # 微信訊息封裝類
│ │ └── WeixinQrCode.java # 微信二維碼 Ticket 封裝類
│ ├── service
│ │ ├── WeixinUserService.java # 微信呼叫處理類
│ │ └── impl
│ │ └── WeixinUserServiceImpl.java
│ └── util
│ ├── AesUtils.java # AES 加密工具類
│ ├── ApiResultUtil.java
│ ├── HttpUtil.java # HTTP 工具類
│ ├── JwtUtil.java # JWT 工具類
│ ├── KeyUtils.java
│ ├── WeixinApiUtil.java # 微信 API 工具類,如獲取 AccessToken
│ ├── WeixinMsgUtil.java # 微信訊息工具類
│ ├── WeixinQrCodeCacheUtil.java # 微信二維碼Ticket快取
│ └── XmlUtil.java
└── resources
├── application.properties #配置檔案
├── static
└── templates
WeixinServerController
和 WeixinUserController
暴漏了三個 API。
/weixin/check
:用於對接微信服務端,接收微信服務端的呼叫。
/user/qrcode
: 用於獲取二維碼圖片資訊
/user/login/qrcode
: 用於校驗是否掃描成功,成功則返回身份認證後的 JWT 字串。
專案公眾號資訊配置
在 application.properties
中配置公眾號所需的配置。
server.port=
weixin.appid=
weixin.appsecret=
weixin.token=
ase.util.secret=
key.jwt.secret=
驗證簽名
開發者提交資訊後,微信伺服器向填寫的 URL 傳送 Get 請求,攜帶引數如下:
引數 | 描述 |
---|---|
signature | 微信加密簽名,signature結合了開發者填寫的token引數和請求中的timestamp引數、nonce引數。 |
timestamp | 時間戳 |
nonce | 隨機數 |
echostr | 隨機字串 |
開發者需要對 signature 進行校驗,判斷是否來自微信伺服器,公眾號相關的其他事件如訊息、關注、掃碼等一樣會回撥配置的 URL ,只不過這時是 POST 請求。
驗籤邏輯檢視微信文件:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html。
Java 實現如下:
// com.wdbyte.weixin.service.impl.WeixinUserServiceImpl.java
@Value("${weixin.token}")
private String token;
@Override
public void checkSignature(String signature, String timestamp, String nonce) {
String[] arr = new String[] {token, timestamp, nonce};
Arrays.sort(arr);
StringBuilder content = new StringBuilder();
for (String str : arr) {
content.append(str);
}
String tmpStr = DigestUtils.sha1Hex(content.toString());
if (tmpStr.equals(signature)) {
log.info("check success");
return;
}
log.error("check fail");
throw new RuntimeException("check fail");
}
獲取 Access Token
獲取帶有 Ticket 的公眾號二維碼之前,需要先獲取公眾號的 Access Token,這是呼叫微信公眾號所有介面的前提。 Access Token 每日呼叫次數有限,應該進行快取。
// com.wdbyte.weixin.util.WeixinApiUtil.java
@Value("${weixin.appid}")
public String appId;
@Value("${weixin.appsecret}")
public String appSecret;
private static String ACCESS_TOKEN = null;
private static LocalDateTime ACCESS_TOKEN_EXPIRE_TIME = null;
/**
* 獲取 access token
*
* @return
*/
public synchronized String getAccessToken() {
if (ACCESS_TOKEN != null && ACCESS_TOKEN_EXPIRE_TIME.isAfter(LocalDateTime.now())) {
return ACCESS_TOKEN;
}
String api = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appId + "&secret="
+ appSecret;
String result = HttpUtil.get(api);
JSONObject jsonObject = JSON.parseObject(result);
ACCESS_TOKEN = jsonObject.getString("access_token");
ACCESS_TOKEN_EXPIRE_TIME = LocalDateTime.now().plusSeconds(jsonObject.getLong("expires_in") - 10);
return ACCESS_TOKEN;
}
生成登入二維碼
使用 Access Token 獲取二維碼 Ticket 用來換取二維碼圖片。
// com.wdbyte.weixin.util.WeixinApiUtil.java
private static String QR_CODE_URL_PREFIX = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=";
/**
* 二維碼 Ticket 過期時間
*/
private static int QR_CODE_TICKET_TIMEOUT = 10 * 60;
/**
* 獲取二維碼 Ticket
*
* https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html
*
* @return
*/
public WeixinQrCode getQrCode() {
String api = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + getAccessToken();
String jsonBody = String.format("{\n"
+ " \"expire_seconds\": %d,\n"
+ " \"action_name\": \"QR_STR_SCENE\",\n"
+ " \"action_info\": {\n"
+ " \"scene\": {\n"
+ " \"scene_str\": \"%s\"\n"
+ " }\n"
+ " }\n"
+ "}", QR_CODE_TICKET_TIMEOUT, KeyUtils.uuid32());
String result = HttpUtil.post(api, jsonBody);
log.info("get qr code params:{}", jsonBody);
log.info("get qr code result:{}", result);
WeixinQrCode weixinQrCode = JSON.parseObject(result, WeixinQrCode.class);
weixinQrCode.setQrCodeUrl(QR_CODE_URL_PREFIX + URI.create(weixinQrCode.getTicket()).toASCIIString());
return weixinQrCode;
}
class WeixinQrCode {
private String ticket;
private Long expireSeconds;
private String url;
private String qrCodeUrl;
}
響應內容格式如下:
{
"ticket": "gQH47joAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL2taZ2Z3TVRtNzJXV1Brb3ZhYmJJAAIEZ23sUwMEmm3sUw==",
"expire_seconds": 60,
"url": "http://weixin.qq.com/q/kZgfwMTm72WWPkovabbI"
}
其中 Ticket 就是二維碼憑證,使用者掃碼後微信會把此 Ticket 回撥給網站服務端。可以在下面的連結後面拼上 Ticket 換取二維碼圖片。
https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=
掃碼回撥與身份繫結
使用者掃碼後,微信服務端會把二維碼鞋帶的 Ticket 和使用者的身份標識作為訊息內容回撥到網站伺服器。
格式如下:
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[subscribe]]></Event>
<EventKey><![CDATA[qrscene_123123]]></EventKey>
<Ticket><![CDATA[TICKET]]></Ticket>
</xml>
其中 FromUserName
是使用者身份標識,EventKey
值 qrscene_
標識掃碼,Ticket
則是二維碼的 Ticket。至此,服務端就可以識別出二維碼是被哪個使用者掃碼了。繫結 Ticket 和使用者身份標識。
注:FromUserName 是唯一的使用者身份標識,同一個使用者每次掃描的 FromUserName 相同。
瀏覽器輪詢掃描狀態
瀏覽器鞋帶 Ticket 資訊不斷的輪詢 /user/login/qrcode
介面檢視 Ticket 是否被掃描成功,如果透過 Ticket 可以查到使用者身份標識,說明二維碼被掃描成功,返回使用者資訊。登入完成。
掃碼登入測試
瀏覽器不斷的輪詢 https://api.wdbyte.com/user/login/qrcode?ticket=Ticket值
獲取掃碼狀態。
二維碼尚未掃描,則返回:
{
"code": -1,
"data": "check faild",
"message": "error"
}
微信掃碼關注公眾號。
掃碼成功後輪詢介面會響應 JWT 格式的身份資訊,這裡使用了 AES 對 JWT 進行了加密。
{
"code": 200,
"data": "mihzE8Z1Y9t2EoppNSzzytV4TOgn+Nc50ORZjsW/oVkxchL4EzGA6rr1tQ0Q7J24Ipm4otjCYf95Nu8JbV31Q/ImKvlta3f5bgvOdWSlO2tNvOwqgzBSItABohbCLVLxjGCci4VtNaEFgQjoDjc1uhwP/GCSohVFc7csO9SxpOm8HKtlRhATjwPrtiQ9iLErfsUs27I0k5OHp55AzuQOYCvza//i3wk8nlv/MDkk7y1nvsZkllyKQGHPB4Ulcraz",
"message": "success"
}
至此,登入完成。
完整程式碼: github.com/niumoo/JavaNotes/tree/master/springboot/springboot-weixin-qrcode-login
一如既往,文章中程式碼存放在 Github.com/niumoo/javaNotes.
本文收錄於 Github.com/niumoo/JavaNotes,Java 系列文件,資料結構與演算法!
本文收錄於網站:https://www.wdbyte.com/,我的公眾號:程式猿阿朗