用go封裝和實現掃碼登入

秋玻發表於2023-10-17

用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。

相關文章