概述
現在微信的使用使用者越來越多,如果網站新增上微信登入,就能節省很多使用者註冊時間,極大縮小了註冊流程。會讓使用者覺得特別方便。接下來我們就說一下怎麼來實現Web端微信掃碼登入。
準備工作
1.實現內網穿透,推薦工具:飛鴿,快速跳轉
實現內網穿透原因:微信無法訪問私有ip地址,同時我們在進行測試時,使用的家用的ip大多都是私有ip地址,所以要透過內網穿透來使用公網ip對映到我們的服務。
2.申請微信公眾平臺測試賬號並進行配置,點選檢視更多
Websocket
實時通訊
我們通常有三種方法實現實時通訊:
1.ajax輪詢
ajax
輪詢的原理非常簡單,讓瀏覽器每隔幾秒就像伺服器傳送一個請求,詢問伺服器是否有新的資訊.
2.http 長輪詢
長輪詢的機制和ajax
輪詢差不多,都是採用輪詢的方式,不過過去的是阻塞模型(一直打電話,沒收到就不掛電話),也就是說,客戶端發起連結後,如果沒有訊息,就一直不返回response
給客戶端。直到有新的訊息才返回,返回完之後,客戶端再此建立連線,週而復始.
3.WebSocketWebSocket
是HTML5
開始提供的一種在單個TCP連線上進行全雙工通訊的協議.在WebSocket API中,瀏覽器和伺服器只需要做一個握手的動作,然後,瀏覽器和伺服器之間就形成了一條快速通道。兩者之間就直接可以資料互相傳送,不需要繁瑣的詢問和等待.
對比:ajax輪詢和長輪詢都是非常耗費資源的,而WebSocket,只需要經過一次HTTP請求,就可以與服務端進行源源不斷的訊息收發了.
實現過程
sockjs
SockJS
是一個瀏覽器的JavaScript
庫,它提供了一個類似於網路的物件,SockJS
提供了一個連貫的,跨瀏覽器的JavaScriptAPI
,它在瀏覽器和Web伺服器之間建立了一個低延遲,全雙工,跨域通訊通道. SockJS提供了瀏覽器相容性,優先使用原生的WebSocket
,如果某個瀏覽器不支援WebSocket,SockJS
會自動降級為輪詢.
STOMP
STOMP
(Simple Text-Orientated Messaging Protocol) 面向訊息的簡單文字協議: WebSocket
是一個訊息架構,不強制使用任何特定的訊息協議,它依賴於應用層解釋訊息的含義.與HTTP
不同,WebSocket
是在傳輸層上進行資料實現和處理的,會將位元組流轉化為文字/二進位制訊息,因此,對於實際應用來說,WebSocket的通訊形式層級過低,因此,可以在WebSocket
之上使用STOMP
協議,來為瀏覽器 和 server間的 通訊增加適當的訊息語義。
STOMP與WebSocket 的關係:
1.HTTP
協議解決了web瀏覽器發起請求以及web伺服器響應請求的細節,假設HTTP協議不存在,只能使用TCP套接字來編寫web應用,通訊雙方在應用層的協議三要素便可能不一致。
2.直接使用WebSocket(SockJS)
就很類似於使用TCP套接字來編寫web應用,因為沒有高層協議,就需要我們定義應用間傳送訊息的語義,還需要確保連線的兩端都能遵循這些語義.
3.同HTTP
在TCP
套接字上新增請求-響應模型層一樣,STOMP
在WebSocket
之上提供了一個基於幀的線路格式層,用來定義訊息語義.
實戰
微信掃碼登陸過程:
1.實現前後臺WebSocket連線:
前臺首先安裝sockjs-client
和 stompjs
:
npm install sockjs-client
npm install stompjs
前臺建立websocket
連線簡單示例:
const socket = new SockJS('http://localhost:8080/demo-stomp-endpoint');
const stompClient = Stomp.over(socket);
stompClient.connect({
'ws-auth-token': this.uuid
}, (frame: any) => {
// 新增個uuid, 用於後續進行debug,看是否為單例.
stompClient.id = uuid();
this.stompClientSubject.next(stompClient);
});
前臺註冊路由,充當後臺主動訪問的介面:
/**
* 註冊路由
* @param router 路由
* @param subject 後臺回發webSocket時傳送資料流
*/
register<T>(router: string, subject: Subject<T>): void {
if (this.observables[router]) {
throw new Error('未能夠重複註冊關鍵字' + router);
}
console.log('register');
this.stompClient$.pipe(filter(v => v !== null), first()).subscribe(stompClient => {
stompClient.subscribe(this.getUrl(router), (data: any) => {
console.log(data);
subject.next(data);
});
});
}
後臺引入WebSocket
相關依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
後臺引入公眾號的相關依賴:
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
後臺定義與前臺連線點:
/**
* 定義一個連線點(處理第一次webSocket的握手)
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/demo-stomp-endpoint")
.setAllowedOriginPatterns("http://localhost:4200")
.withSockJS();
}
後臺定義出口字首和入口字首:
/**
* 配置訊息經紀人
* 配置一個入口字首,一個出口字首。
* 注意:出口需要保留/user字首,stomp主動向某個使用者傳送資料時,將以/user字首前頭(可配置)
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 設定入口字首,處理所有以app打頭的請求
config.setApplicationDestinationPrefixes("/app");
// 設定出口字首,處理所有以/stomp打頭的出口資料
config.enableSimpleBroker("/stomp");
}
邏輯實現
1.首先在開啟頁面時,進行路由註冊,便於後臺主動向前臺發起請求
// 註冊前臺掃碼繫結的路由,掃碼繫結後,後臺主動發起請求
this.websocketServer.register('/user/stomp/scanBindUserQrCode', this.onScanBindUserQrCode);
// 註冊前臺掃碼登陸的路由,掃碼登陸後,後臺主動發起請求
this.websocketServer.register('/user/stomp/scanLoginQrCode', this.onScanLoginQrCode);
隨後前臺發起獲取登陸二維碼請求,同時傳輸唯一標識碼,用於掃碼成功後後臺主動發起登陸成功請求給前臺:
/**
* 獲取登入二維碼
*/
getLoginQrCode(): Observable<string> {
return this.httpClient.get<string>(`${this.baseUrl}/getLoginQrCode/${this.websocketServer.uuid}`);
}
2.後臺響應獲取登陸二維碼同時對掃碼事件進行處理,:
@Override
public String getLoginQrCode(String wsLoginToken, HttpSession httpSession) {
try {
if (this.logger.isDebugEnabled()) {
this.logger.info("1. 生成用於回撥的uuid,請將推送給微信,微信當推送帶有UUID的二維碼,使用者掃碼後微信則會把帶有uuid的資訊回推過來");
}
// qrUuid用於換取微信跳轉的ticket,以根據ticket換取二維碼url
String qrUuid = UUID.randomUUID().toString();
WxMpQrCodeTicket wxMpQrCodeTicket = this.weChatMpService.getQrcodeService().qrCodeCreateTmpTicket(qrUuid, 10 * 60);
// 增加事務處理,對掃描事件等處理進行處理
this.weChatMpService.addHandler(qrUuid, new WeChatMpEventKeyHandler() {
long beginTime = System.currentTimeMillis();
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public boolean getExpired() {
return System.currentTimeMillis() - beginTime > 10 * 60 * 1000;
}
/**
* 掃碼後呼叫該方法
* @param wxMpXmlMessage 掃碼訊息
* @param weChatUser 掃碼使用者
* @return 輸出訊息
*/
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, WeChatUser weChatUser) {
if (this.logger.isDebugEnabled()) {
this.logger.info("2. 使用者掃描後觸發該方法, 傳送掃碼成功的同時,將wsUuid與微信使用者繫結在一起,用後面使用wsU");
}
String openid = wxMpXmlMessage.getFromUser();
if (openid == null) {
this.logger.error("openid is null");
}
if (weChatUser.getUser() != null) {
// 此處的隨機生成的uuid與使用者繫結,並將其返回給前臺,用uuid再次登陸
String uuid = UUID.randomUUID().toString();
System.out.println("uuid是" + uuid);
bindWsUuidToWeChatUser(uuid, weChatUser);
// 此處的wx-auth-token是前臺生成的uuid,用於標識唯一前臺
simpMessagingTemplate.convertAndSendToUser(wsLoginToken,
"/stomp/scanLoginQrCode",
uuid);
return new TextBuilder().build(String.format("登入成功,登入的使用者為: %s", weChatUser.getUser().getName()),
wxMpXmlMessage,
null);
} else {
// 掃碼後發現沒有繫結,返回給前臺空uuid,同時返回給微信使用者未繫結提示
// 此處的wx-auth-token是前臺生成的uuid,用於標識唯一前臺
simpMessagingTemplate.convertAndSendToUser(wx-auth-token,
"/stomp/scanLoginQrCode",
false);
return new TextBuilder().build(String.format("登入原則,原因:您尚未繫結微信使用者"),
wxMpXmlMessage,
null);
}
}
});
return this.weChatMpService.getQrcodeService().qrCodePictureUrl(wxMpQrCodeTicket.getTicket());
} catch (Exception e) {
this.logger.error("獲取臨時公眾號圖片時發生錯誤:" + e.getMessage());
}
return "";
}
3.前臺接收到uuid後,使用uuid作為使用者名稱和密碼進行正常登陸:
this.login({username: uuid, password: uuid});
4.後臺在登陸驗證方式中增加根據uuid進行驗證:
/**
* 校驗微信掃碼登入後的認證ID是否有效
* @param wsAuthUuid websocket認證ID
*/
@Override
public boolean checkWeChatLoginUuidIsValid(String wsAuthUuid) {
return this.map.containsKey(wsAuthUuid);
}
實現邏輯圖如下:
最後是demo的倉庫地址,請點選