用go封裝和實現掃碼登錄

来源:https://www.cnblogs.com/weloe/archive/2023/10/17/17770329.html
-Advertisement-
Play Games

本篇會講講token-go掃碼登錄的封裝和實現,給庫/框架增加新的功能,最後說明使用方法,源碼:https://github.com/weloe/token-go ...


用go封裝和實現掃碼登錄

本篇為用go設計開發一個自己的輕量級登錄庫/框架吧 - 秋玻 - 博客園 (cnblogs.com)的掃碼登錄業務篇,會講講掃碼登錄的實現,給庫/框架增加新的功能,最後說明使用方法

Github:https://github.com/weloe/token-go

掃碼登錄流程

首先我們需要知道掃碼登錄流程

  1. 打開登錄頁面,展示一個二維碼,同時輪詢二維碼狀態(web)
  2. 打開APP掃描該二維碼後,APP顯示確認、取消按鈕(app)
  3. 登錄頁面展示被掃描的用戶頭像等信息(web)
  4. 用戶在APP上點擊確認登錄(app)
  5. 登錄頁面從輪詢二維碼狀態得知用戶已確認登錄,並獲取到登錄憑證(web)
  6. 頁面登錄成功,併進入主應用程式頁面(web)

我們可以知道登錄的二維碼有一下幾種狀態:

  1. 等待掃碼
  2. 已掃碼,等待用戶確認
  3. 已掃碼,用戶同意授權
  4. 已掃碼,用戶取消授權
  5. 已過期

而我們掃碼的客戶端(一般是手機App)可以修改二維碼的狀態,

  1. 確認已掃碼
  2. 同意授權
  3. 取消授權

實現思路

我們封裝的主要是二維碼的狀態維護,不包括生成二維碼,二維碼的生成交由使用者來實現。

而二維碼的狀態的常用的幾個方法如下。

// QRCode api
// 初始化二維碼狀態
CreateQRCodeState(QRCodeId string, timeout int64) error
// 獲取二維碼剩餘時間
GetQRCodeTimeout(QRCodeId string) int64
// 獲取二維碼信息
GetQRCode(QRCodeId string) *model.QRCode
// 獲取二維碼狀態
GetQRCodeState(QRCodeId string) model.QRCodeState
// 確認已掃碼
Scanned(QRCodeId string, loginId string) (string, error)
// 同意授權
ConfirmAuth(QRCodeTempToken string) error
// 取消授權
CancelAuth(QRCodeTempToken string) error

QRCodeId用於我們作為二維碼狀態的唯一標識。

在創建二維碼時我們要傳入QRCodeId以及timeout來設定二維碼的超時時間,畢竟二維碼總不能永久使用。

確認已掃碼當然前提是在登錄狀態才能確認,因此我們用loginId作為參數用來跟QRCodeId來綁定。

對於同意授權和取消授權我們使用確認掃碼的api返回的臨時Token去進行操作。

而對信息的存儲和獲取則是使用框架內部的Adapter去獲取。

代碼實現

二維碼狀態和信息

首先我們要先設定一下二維碼狀態

等待掃碼——1

已掃碼,等待用戶確認——2

已掃碼,用戶同意授權——3

已掃碼,用戶取消授權——4

已過期——5

package model

type QRCodeState int

// QRCode State
const (
	WaitScan    QRCodeState = 1
	WaitAuth    QRCodeState = 2
	ConfirmAuth QRCodeState = 3
	CancelAuth  QRCodeState = 4
	Expired     QRCodeState = 5
)

維護二維碼需要的信息,也就是二維碼的唯一id,二維碼當前狀態,二維碼對於的用戶唯一id


type QRCode struct {
	id      string
	State   QRCodeState
	LoginId string
}

func NewQRCode(id string) *QRCode {
	return &QRCode{id: id, State: WaitScan}
}

初始化二維碼狀態

https://github.com/weloe/token-go/blob/b85a297b4eae1ee730059be277d7aa83658c1fe4/enforcer_manager_api.go#L229

在APP掃碼前我們要先創建一個二維碼狀態,設置為WaitScan,也就是1。而創建二維碼信息,也就是使用我們框架內部的Adapter介面來存儲

func (e *Enforcer) CreateQRCodeState(QRCodeId string, timeout int64) error {
	return e.createQRCode(QRCodeId, timeout)
}
func (e *Enforcer) createQRCode(id string, timeout int64) error {
	return e.adapter.Set(e.spliceQRCodeKey(id), model.NewQRCode(id), timeout)
}

e.spliceQRCodeKey是對存儲的key的拼接方法。

獲取二維碼的剩餘時間

https://github.com/weloe/token-go/blob/b85a297b4eae1ee730059be277d7aa83658c1fe4/enforcer_manager_api.go#L319

通過QRCodeId使用我們的Adapter去獲取

func (e *Enforcer) GetQRCodeTimeout(QRCodeId string) int64 {
    return e.getQRCodeTimeout(QRCodeId)
}
func (e *Enforcer) getQRCodeTimeout(id string) int64 {
	return e.adapter.GetTimeout(e.spliceQRCodeKey(id))
}

獲取二維碼信息

https://github.com/weloe/token-go/blob/b85a297b4eae1ee730059be277d7aa83658c1fe4/enforcer_manager_api.go#L301

使用Adapter獲取

func (e *Enforcer) GetQRCode(QRCodeId string) *model.QRCode {
	return e.getQRCode(QRCodeId)
}

獲取二維碼狀態

https://github.com/weloe/token-go/blob/b85a297b4eae1ee730059be277d7aa83658c1fe4/enforcer_manager_api.go#L311

同樣使用Adapter獲取

// GetQRCodeState
//	WaitScan   = 1
//	WaitAuth   = 2
//	ConfirmAuth  = 3
//	CancelAuth = 4
//	Expired    = 5
func (e *Enforcer) GetQRCodeState(QRCodeId string) model.QRCodeState {
	qrCode := e.getQRCode(QRCodeId)
	if qrCode == nil {
		return model.Expired
	}
	return qrCode.State
}

刪除二維碼信息

https://github.com/weloe/token-go/blob/b85a297b4eae1ee730059be277d7aa83658c1fe4/enforcer_manager_api.go#L323

func (e *Enforcer) DeleteQRCode(QRCodeId string) error {
	return e.deleteQRCode(QRCodeId)
}
func (e *Enforcer) deleteQRCode(id string) error {
	return e.adapter.Delete(e.spliceQRCodeKey(id))
}

確認掃碼

https://github.com/weloe/token-go/blob/b85a297b4eae1ee730059be277d7aa83658c1fe4/enforcer_manager_api.go#L234

確認掃碼要先判斷二維碼是否存在,接著校驗二維碼的狀態是否是等待掃描WaitScan也就是1。校驗完之後綁定用戶唯一loginId,最後創建一個value值為QRCodeId的臨時token返回。這個臨時token用於同意授權和取消授權。

// Scanned update state to constant.WaitAuth, return tempToken
func (e *Enforcer) Scanned(QRCodeId string, loginId string) (string, error) {
	qrCode := e.getQRCode(QRCodeId)
	if qrCode == nil {
		return "", fmt.Errorf("QRCode doesn't exist")
	}
	if qrCode.State != model.WaitScan {
		return "", fmt.Errorf("QRCode state error: unexpected state value %v, want is %v", qrCode.State, model.WaitScan)
	}
	qrCode.State = model.WaitAuth
	qrCode.LoginId = loginId

	err := e.updateQRCode(QRCodeId, qrCode)
	if err != nil {
		return "", err
	}
	tempToken, err := e.CreateTempToken(e.config.TokenStyle, "qrCode", QRCodeId, e.config.Timeout)
	if err != nil {
		return "", err
	}
	return tempToken, nil
}

同意授權

https://github.com/weloe/token-go/blob/b85a297b4eae1ee730059be277d7aa83658c1fe4/enforcer_manager_api.go#L257

同意授權要使用我們在確認掃碼的時候返回的臨時token,首先我們要校驗這個臨時token,這個ParseTempToken方法就是校驗臨時token,獲取token對應的值的介面。在校驗token後獲取到QRCodeId,再去校驗QRCodeId對應的狀態,應該要是WaitAuth等待授權,也就是2。最後就是修改二維碼狀態為ConfirmAuth也就3。當然不能忘記刪除臨時token。

// ConfirmAuth update state to constant.ConfirmAuth
func (e *Enforcer) ConfirmAuth(tempToken string) error {
	qrCodeId := e.ParseTempToken("qrCode", tempToken)
	if qrCodeId == "" {
		return fmt.Errorf("confirm failed, tempToken error: %v", tempToken)
	}
	qrCode, err := e.getAndCheckQRCodeState(qrCodeId, model.WaitAuth)
	if err != nil {
		return err
	}

	qrCode.State = model.ConfirmAuth
	err = e.updateQRCode(qrCodeId, qrCode)
	if err != nil {
		return err
	}
	err = e.DeleteTempToken("qrCode", tempToken)
	if err != nil {
		return err
	}
	return err
}

取消授權

https://github.com/weloe/token-go/blob/b85a297b4eae1ee730059be277d7aa83658c1fe4/enforcer_manager_api.go#L280

取消授權也要使用我們在確認掃碼的時候返回的臨時token,首先我們要校驗這個臨時token,這個ParseTempToken方法就是校驗臨時token的方法,通過這個方法獲取到token對應的QRCodeId值。在校驗token後獲取到QRCodeId,再去校驗QRCodeId對應的狀態,應該要是WaitAuth等待授權,也就是2。最後就是修改二維碼狀態為CancelAuth也就4。同樣不能忘記刪除臨時token。

// CancelAuth update state to constant.CancelAuth
func (e *Enforcer) CancelAuth(tempToken string) error {
	qrCodeId := e.ParseTempToken("qrCode", tempToken)
	if qrCodeId == "" {
		return fmt.Errorf("confirm failed, tempToken error: %v", tempToken)
	}
	qrCode, err := e.getAndCheckQRCodeState(qrCodeId, model.WaitAuth)
	if err != nil {
		return err
	}
	qrCode.State = model.CancelAuth
	err = e.updateQRCode(qrCodeId, qrCode)
	if err != nil {
		return err
	}
	err = e.DeleteTempToken("qrCode", tempToken)
	if err != nil {
		return err
	}
	return err
}

測試

func TestEnforcer_ConfirmQRCode(t *testing.T) {
	enforcer, _ := NewTestEnforcer(t)
	// in APP
	loginId := "1"
	token, err := enforcer.LoginById(loginId)
	if err != nil {
		t.Fatalf("Login failed: %v", err)
	}
	t.Logf("login token: %v", token)

	qrCodeId := "q1"

	err = enforcer.CreateQRCodeState(qrCodeId, -1)
	if err != nil {
		t.Fatalf("CreateQRCodeState() failed: %v", err)
	}
	t.Logf("After CreateQRCodeState(), current QRCode state: %v", enforcer.GetQRCodeState(qrCodeId))
	loginIdByToken, err := enforcer.GetLoginIdByToken(token)
	if err != nil {
		t.Fatalf("GetLoginIdByToken() failed: %v", err)
	}
	tempToken, err := enforcer.Scanned(qrCodeId, loginIdByToken)
	if err != nil {
		t.Fatalf("Scanned() failed: %v", err)
	}
	if state := enforcer.GetQRCodeState(qrCodeId); state != model.WaitAuth {
		t.Fatalf("After Scanned(), QRCode should be %v", model.WaitAuth)
	}
	t.Logf("After Scanned(), current QRCode state: %v", enforcer.GetQRCodeState(qrCodeId))
	t.Logf("tempToken: %v", tempToken)
	err = enforcer.ConfirmAuth(tempToken)
	if err != nil {
		t.Fatalf("ConfirmAuth() failed: %v", err)
	}
	if state := enforcer.GetQRCodeState(qrCodeId); state != model.ConfirmAuth {
		t.Fatalf("After ConfirmAuth(), QRCode should be %v", model.ConfirmAuth)
	}
	t.Logf("After ConfirmAuth(), current QRCode state: %v", enforcer.GetQRCodeState(qrCodeId))
	if enforcer.GetQRCodeState(qrCodeId) == model.ConfirmAuth {
		loginId := enforcer.getQRCode(qrCodeId).LoginId
		t.Logf("id: [%v] QRCode login successfully.", loginId)
	}
}

如何使用

https://github.com/weloe/token-go/blob/master/examples/qrcode/qrcode-server.go

安裝token-go, go get github.com/weloe/token-go

package main

import (
	"fmt"
	tokenGo "github.com/weloe/token-go"
	"github.com/weloe/token-go/model"
	"log"
	"net/http"
)

var enforcer *tokenGo.Enforcer

func main() {
	var err error
	// use default adapter
	adapter := tokenGo.NewDefaultAdapter()
	enforcer, err = tokenGo.NewEnforcer(adapter)
	// enable logger
	enforcer.EnableLog()
	if err != nil {
		log.Fatal(err)
	}

	http.HandleFunc("/qrcode/create", create)
	http.HandleFunc("/qrcode/scanned", scanned)
	http.HandleFunc("/qrcode/confirmAuth", confirmAuth)
	http.HandleFunc("/qrcode/cancelAuth", cancelAuth)
	http.HandleFunc("/qrcode/getState", getState)

	log.Fatal(http.ListenAndServe(":8081", nil))
}

func create(w http.ResponseWriter, request *http.Request) {
	// you should implement generate QR code method, returns QRCodeId to CreateQRCodeState
	// called generate QR code, returns QRCodeId to CreateQRCodeState
	//
	QRCodeId := "generatedQRCodeId"
	err := enforcer.CreateQRCodeState(QRCodeId, 50000)
	if err != nil {
		fmt.Fprintf(w, "CreateQRCodeState() failed: %v", err)
		return
	}
	fmt.Fprintf(w, "QRCodeId = %v", QRCodeId)
}

func scanned(w http.ResponseWriter, req *http.Request) {
	loginId, err := enforcer.GetLoginId(tokenGo.NewHttpContext(req, w))
	if err != nil {
		fmt.Fprintf(w, "GetLoginId() failed: %v", err)
		return
	}
	QRCodeId := req.URL.Query().Get("QRCodeId")
	tempToken, err := enforcer.Scanned(QRCodeId, loginId)
	if err != nil {
		fmt.Fprintf(w, "Scanned() failed: %v", err)
		return
	}
	fmt.Fprintf(w, "tempToken = %v", tempToken)
}
func getState(w http.ResponseWriter, req *http.Request) {
	QRCodeId := req.URL.Query().Get("QRCodeId")
	state := enforcer.GetQRCodeState(QRCodeId)
	if state == model.ConfirmAuth {
		qrCode := enforcer.GetQRCode(QRCodeId)
		if qrCode == nil {
			fmt.Fprintf(w, "login error. state = %v, code is nil", state)
			return
		}
		loginId := qrCode.LoginId
		token, err := enforcer.LoginById(loginId)
		if err != nil {
			fmt.Fprintf(w, "Login error: %s\n", err)
		}
		fmt.Fprintf(w, "%v login success. state = %v, token = %v", loginId, state, token)
		return
	} else if state == model.CancelAuth {
		fmt.Fprintf(w, "QRCodeId be cancelled: %v", QRCodeId)
		return
	}
	fmt.Fprintf(w, "state = %v", state)
}

func cancelAuth(w http.ResponseWriter, req *http.Request) {
	tempToken := req.URL.Query().Get("tempToken")
	err := enforcer.CancelAuth(tempToken)
	if err != nil {
		fmt.Fprintf(w, "CancelAuth() failed: %v", err)
		return
	}
	fmt.Fprint(w, "ConfirmAuth() success")
}

func confirmAuth(w http.ResponseWriter, req *http.Request) {
	tempToken := req.URL.Query().Get("tempToken")
	err := enforcer.ConfirmAuth(tempToken)
	if err != nil {
		fmt.Fprintf(w, "ConfirmAuth() failed: %v", err)
		return
	}
	fmt.Fprint(w, "ConfirmAuth() success")
}

從最開始的流程和測試方法中也可以知道

首先我們需要在Web端(需要掃碼登錄的客戶端)生成二維碼後攜帶參數二維碼id請求後端/qrcode/create,後端調用生成二維碼的方法(需要自己實現),然後調用enforcer.CreateQRCodeState()方法初始化二維碼狀態。

從APP端掃碼二維碼,請求後端/qrcode/scanned,後端先校驗一下APP傳來的token判斷(使用框架的enforcer.isLoginByToken()方法來判斷)是否在登錄態,使用enforcer.GetLoginId()獲取對應的loginId,再調用enforcer.Scanned()方法。之後返回臨時token。

APP端收到臨時token後,選擇同意或者取消授權,也就是傳臨時token到後端/qrcode/confirmAuth或者/qrcode/cancelAuth,後端調用enforcer.ConfirmAuth()或者enforcer.CancelAuth()方法同意或者取消授權。

而Web端在初始化二維碼狀態後要持續請求後端/qrcode/getState,後端調用GetQRCodeState方法去獲取二維碼狀態,如果二維碼狀態為超時也就是Expired前端就刪除二維碼信息,提示二維碼過期,重新生成二維碼,如果獲取到狀態等於確認授權ConfirmAuth就進行登錄操作enforcer.LoginById(),返回登錄憑證token。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文通過介紹常見的池式結構(資料庫連接池、線程池)和IO多路復用結構(redis),對比其中的作用以及原理,探索其中底層的設計思路的共同點 ...
  • 很多同學技術能力很強,架構設計也做得很好,但是在給別人講解的時候,總感覺像是“茶壺裡煮餃子,有貨倒不出”。 其實,在為新員工培訓系統架構、給領導彙報技術規劃、上技術大會做演講或者向晉升評委介紹工作貢獻的時候,如果你能畫出一張優秀的 軟體系統架構圖,就可以大大提升自己的講解效果,讓對方輕鬆地理解你想表 ...
  • 原本主要是參考了這兩篇文章 https://blog.csdn.net/weixin_45821811/article/details/116211724 https://cloud.tencent.com/developer/article/1670205 但是由於都是基於linux系統下的,可能 ...
  • Windows中文用戶名導致的Dart AOT編譯失敗 我的微軟賬戶一直使用中文用戶名,Windows會把這個用戶名作為用戶文件夾的名稱,而且很難修改. 這就導致凡是放在這個路徑下的文件都得有一個帶中文的絕對路徑. Dart 編譯時可能因為這裡的中文字元而出錯. 問題出現時的操作系統及Dart SD ...
  • 職責鏈模式 案例引入 OA(Office Automation)系統的採購審批項目,需求是 1.採購員採購教學用品。 2.price >= 0 && price <= 5000 由教學主任審批 3.price > 5000 && price <= 10000 由原則審批 4.price > 1000 ...
  • $ {\scr \color {Orchid}{\text{生於塵埃,溺於人海,死於理想高臺。}}} $ 題目鏈接:Colorful Slimes $ {\scr \color {Cyan}{\text{Solution}}} $ 分析 思路:挺神奇的$dp$ 一個比較顯然的結論:最小值的方案中第$ ...
  • 【引言】 在信息時代,數據作為一種寶貴的資產,被廣泛應用於各行各業。但是,數據的獲取和處理需要付出巨大的人力物力,而且往往要通過多種渠道進行查詢,效率低下。為此,我們需要一款強大的查詢系統來滿足我們的多種查詢需求,提高工作效率。微查系統就是這樣一款系統。 【微查系統的介紹】 微查系統是一款集各色查詢 ...
  • 比賽:Japan Registry Services (JPRS) Programming Contest 2023 (AtCoder Beginner Contest 324) A-same 1.常規方法 int main() { int n; cin >> n; vector<int> s(n) ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...