用go封裝和實現掃碼登入
本篇為用go設計開發一個自己的輕量級登入庫/框架吧 - 秋玻 - 部落格園 (cnblogs.com)的掃碼登入業務篇,會講講掃碼登入的實現,給庫/框架增加新的功能,最後說明使用方法
Github:https://github.com/weloe/token-go
掃碼登入流程
首先我們需要知道掃碼登入流程
- 開啟登入頁面,展示一個二維碼,同時輪詢二維碼狀態(web)
- 開啟APP掃描該二維碼後,APP顯示確認、取消按鈕(app)
- 登入頁面展示被掃描的使用者頭像等資訊(web)
- 使用者在APP上點選確認登入(app)
- 登入頁面從輪詢二維碼狀態得知使用者已確認登入,並獲取到登入憑證(web)
- 頁面登入成功,並進入主應用程式頁面(web)
我們可以知道登入的二維碼有一下幾種狀態:
- 等待掃碼
- 已掃碼,等待使用者確認
- 已掃碼,使用者同意授權
- 已掃碼,使用者取消授權
- 已過期
而我們掃碼的客戶端(一般是手機App)可以修改二維碼的狀態,
- 確認已掃碼
- 同意授權
- 取消授權
實現思路
我們封裝的主要是二維碼的狀態維護,不包括生成二維碼,二維碼的生成交由使用者來實現。
而二維碼的狀態的常用的幾個方法如下。
// 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}
}
初始化二維碼狀態
在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的拼接方法。
獲取二維碼的剩餘時間
透過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))
}
獲取二維碼資訊
使用Adapter獲取
func (e *Enforcer) GetQRCode(QRCodeId string) *model.QRCode {
return e.getQRCode(QRCodeId)
}
獲取二維碼狀態
同樣使用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
}
刪除二維碼資訊
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))
}
確認掃碼
確認掃碼要先判斷二維碼是否存在,接著校驗二維碼的狀態是否是等待掃描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
}
同意授權
同意授權要使用我們在確認掃碼的時候返回的臨時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
}
取消授權
取消授權也要使用我們在確認掃碼的時候返回的臨時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。