總體結構
效果圖
效果圖
開發背景
為什麼想用微信掃碼登入呢? 起因是自己開發了一個搜題網站,内容很簡單,但是沒有登陸,是以遊客可以随便使用,當然也不是不讓遊客通路,隻是沒有登陸的話,不能很好的統計使用的使用者,也能減少些一些濫用的使用者。
起初,我是想設計成賬号密碼登入網站的,但是想了下,我自己平常碰到一些需要注冊的網站,我往往會直接跳過,就不會對這個網站感興趣了,能讓我感興趣的網站一般都是支援直接掃碼登入或者可以以第三方賬号直接注冊登入,是以能吸引更多的使用者,必須要把這個門檻給打下來,提高使用者體驗!
尋找方案,以及選擇哪種方案
于是,我開始踏上了百度之旅,經過數次的查閱資料,發現有三種方式實作微信掃碼登入
- 第三方網站(https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html)
- 公衆号
- 小程式
第三方網站
第三方網站方式是直接請求api,https://open.weixin.qq.com/connect/qrconnect帶上下面的參數 例子:攜帶參數的連結
頁面會重定向到一個附帶臨時code的位址
重定向連結
使用者掃碼成功後,頁面會自動跳轉到redirect_uri指定的連結,這樣就完成了掃碼登入
總之,微信開放平台的方式應該是最舒服的微信掃碼登陸了,但是前提需要交認證300元的認證費用,網站的話還需要送出《微信開放平台網站資訊登記表》,也是稽核最麻煩的方式,隻好先pass啦,
公衆号
必要條件:公衆号是已認證好的服務号 流程:
- 利用WxLogin擷取到code,再用code通過https://api.weixin.qq.com/sns/oauth2/access_token擷取access_token;
- 拿到access_token之後,可以利用生成二維碼api(可攜帶scene值,就是自己定義的uuid之類的,可以用來區分是哪個網頁端發起的掃描)生成微信二維碼;
生成二維碼api
- 當使用者掃描生成好的二維碼時,微信會根據推送事件到服務端(這個需要自己在背景配置),并且會攜帶好之前的scene值,背景就可以判斷是哪個網頁端掃了這個二維碼,背景進行登入操作,
- 網頁端可以通過拿着scene值輪訓背景的接口,查詢是否登入成功 或者利用websocket,背景主動通知你給網頁,是否登入成功。
生成二維碼文檔位址:https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html
這種方式還是對個人開發者要求太高了,個人就不能申請服務号,認證也要一筆費用,也勸退我了,pass!
小程式
條件:已經上線的小程式(個人/公司) 流程:
- 背景調用微信接口https://api.weixin.qq.com/cgi-bin/token擷取access_token,access_token隻有兩個小時,因為有調用頻率限制,access_token最好是儲存在緩存中;
- 使用者在網頁端點選擷取小程式碼,前端攜帶随機數請求背景,背景利用access_token擷取小程式碼https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=(其中需要攜帶一個參數scene需要注意,scene值就是一個随機數,需要在前端就生成好,用來背景區分網頁端用),傳回小程式碼二進制資料;
- 前端接收背景傳回的二進制資料,(此時前端需要不停地輪訓背景接口查詢是否登入成功,或者利用websocket,背景主動通知前端)展示小程式碼,使用者拿出手機用微信掃碼之後,類似下面截圖,
登入截圖
- 使用者主動點選登入按鈕,登入按鈕需要綁定WxLogin事件,擷取使用者的微信code,再調用背景提供的登入接口進行登入操作,當登陸成功完畢,通知前端完成登入
擷取小程式碼官方文檔 https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/qr-code/getQRCode.html
最後,整個過程全程免費,對于我來說也是比較簡單的,我先說明下,我之前是開發了一個小程式的,是以對我來說是比較友善的,如果大家沒有已經上線的小程式,為了一個掃碼登入,确實有點費力
原理
原理圖
原理圖
開發步驟
這裡我就不細講了,知道了思路,其實開發就是最簡單的事了,這裡貼一些關鍵的代碼
網頁端
// 擷取二維碼
getQrcode = () => {
// 建立websocket連接配接,為了背景能主動通知到前端
if (this.state.ws == null) {
this.setState({
ws: this.createWebSocket(this)
});
}
// 發送請求 擷取二維碼
this.setState({
qrcodeBase64: ''
});
// 攜帶uuid到背景,背景将uuid和socket對象關聯,友善通知到哪個網頁端
getUnlimitedQrCode({ "uuid": this.state.uuid }).then((res) => {
if (res == undefined || res.data == undefined || res.data.data == undefined) {
console.log("擷取二維碼失敗");
return null;
}
this.setState({
qrcodeBase64: res.data.data
});
}).catch((error) => {
console.error(error);
});
}
背景
// 擷取不限制的小程式碼接口
func (h *handler) GetUnlimitedQRCode() core.HandlerFunc {
return func(c core.Context) {
param := new(RequestQrCodeParam)
c.ShouldBindJSON(¶m)
// 擷取accessToekn
accesstokenInfo, err := h.getAccessToken()
if err != nil {
c.AbortWithError(core.Error(
http.StatusBadRequest,
500,
"擷取微信access_token失敗").WithError(err),
)
return
}
qrCodeBytes := postUnlimitedQRCode(accesstokenInfo, param.Uuid)
// c.ResponseWriter().Write(qrCodeBytes)
var q = QrcodeRes{Data: qrCodeBytes, Errcode: 0, Errmsg: "success"}
c.Payload(q)
}
}
// websocket連接配接回調函數
var (
err error
server socket.Server
SocketManager = make(map[string]*websocket.Conn) // 存儲uuid和socket對象的關系
)
func (h *handler) Connect() core.HandlerFunc {
return func(ctx core.Context) {
server, err = socket.New(h.logger, h.db, h.cache, ctx.ResponseWriter(), ctx.Request(), nil)
if err != nil {
return
}
// 儲存uuid到map,後續背景主動回調前台,需要根據uuid找到對應的websocket連接配接
uuid := ctx.Request().URL.Query().Get("uuid")
log.Println(uuid)
conn, err := server.GetConn()
if err != nil {
return
}
SocketManager[uuid] = conn
go server.OnMessage()
}
}
// 登入接口
func (h *handler) Wx_web_login() core.HandlerFunc {
return func(c core.Context) {
param := new(RequestWebLoginParam)
c.ShouldBindJSON(¶m)
// 登入操作
ticketRes, err := registerAndExportToken(h, c, &WxLoginParams{Code: param.Code})
if err != nil {
c.AbortWithError(core.Error(
400,
500,
"微信登陸失敗").WithError(err),
)
}
log.Print(ticketRes)
// 登陸成功後,主動調用websocket的接口,傳回token
res := new(webmessage.WebSocketResponse).Success(webmessage.LOGIN_SUCCESS_TYPE, ticketRes)
// 根據uuid找到對應socket
socket := webmessage.SocketManager[param.Uuid]
socket.WriteJSON(res)
c.Payload(res)
}
}
小程式端
// 登入按鈕綁定的登入事件
handleLogin = () => {
// 處理登入邏輯
console.log('登入按鈕被點選')
// 擷取code
Taro.login({
success: (res) => {
console.log(res)
if (res.code != '') {
// 請求登入操作
webWxLogin({ "code": res.code, "uuid": this.state.scene }).then((res) => {
console.log(res.data)
if (res.data.code == 200 && res.data.type == 20000) {
// 登入成功
Taro.showToast({
title: '登入成功',
icon: 'success'
})
// 跳轉首頁
Taro.switchTab({
url: '/pages/home/index'
})
} else {
Taro.showToast({
title: '登入失敗',
icon: 'none'
})
}
}).catch((err) => {
console.log(err)
Taro.showToast({
title: '登入失敗',
icon: 'none'
})
})
} else {
Taro.showToast({
title: '登入失敗',
icon: 'none'
})
}
},
fail: (err) => {
console.log(err)
Taro.showToast({
title: '登入失敗',
icon: 'none'
})
}
})
}
// 這段代碼是用GitHub Copilot 寫的,真的是把所有情況都寫出來了
總結
這次掃碼登入前前後後花了我五天的時間(下班後弄得),其中也碰了不少坑,前端二維碼不顯示、背景不傳回二進制資料、nginx代理的https域名網站websocket也要做特殊處理,真的是在裡面爬了很久,雖然這個手段不是很正規,但是過程能學到一些知識,實作了自己想要的效果,還是挺滿意的,謝謝看到這裡,如果哪裡有說的有問題,歡迎指正