5

用go封装和实现扫码登录 - 秋玻

 11 months ago
source link: https://www.cnblogs.com/weloe/p/17770329.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

用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. 已扫码,用户取消授权

而我们扫码的客户端(一般是手机App)可以修改二维码的状态,

  1. 确认已扫码

实现思路

我们封装的主要是二维码的状态维护,不包括生成二维码,二维码的生成交由使用者来实现。

而二维码的状态的常用的几个方法如下。



// 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|0二维码状态和信息

首先我们要先设定一下二维码状态

等待扫码——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} }

1|0初始化二维码状态

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的拼接方法。

1|0获取二维码的剩余时间

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)) }

1|0获取二维码信息

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) }

1|0获取二维码状态

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 }

1|0删除二维码信息

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)) }

1|0确认扫码

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 }

1|0同意授权

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 }

1|0取消授权

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。

__EOF__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK