用go設計開發一個自己的輕量級登錄庫/框架吧(業務篇)

来源:https://www.cnblogs.com/weloe/archive/2023/05/13/17397071.html
-Advertisement-
Play Games

本篇將講講登錄庫中的三種登錄模式的實現: 同一用戶只能登錄一次, 同一用戶多次登錄多token,同一用戶多次登錄共用一個token,源碼:weloe/token-go: a light login library (github.com) ...


用go設計開發一個自己的輕量級登錄庫/框架吧(業務篇)

本篇會講講框架的登錄業務的實現。實現三種登錄模式:

  • 同一用戶只能登錄一次
  • 同一用戶多次登錄多token
  • 同一用戶多次登錄共用一個token

源碼:weloe/token-go: a light login library (github.com)

存儲結構

首先從我們要考慮是底層該怎麼存儲登錄信息來去達成這三種登錄模式

  • 同一用戶只能登錄一次
  • 同一用戶多次登錄多token
  • 同一用戶多次登錄共用一個token

我們不能使用無狀態token模式,要有狀態,在後端存儲會話信息才能達成想要實現的一些邏輯,因此,存儲會話信息是必要的。

對於每個請求,我們會存儲一個token-loginId的k-v結構。

對於整個會話,我們會存儲一個loginId-session的k-v結構。

基於這個存儲結構我們就可以方便的實現這三種模式。

Session結構體

session包括了多個tokenValue,這就是我們用來實現同一用戶多次登錄多token,或者同一用戶多次登錄共用一個token的關鍵點

type TokenSign struct {
   Value  string
   Device string
}

type Session struct {
   Id            string
   TokenSignList *list.List
}

總之,我們實現的業務將基於這兩種k-v結構

功能實現

源碼:https://github.com/weloe/token-go/blob/f58ba4d93f0f012972bf6a35b9127229b5c328fe/enforcer.go#L167

我們再來梳理一些功能和配置的對應關係

同一用戶只能登錄一次:IsConcurrent == false

同一用戶多次登錄多token: IsConcurrent == true && IsShare == false這時候配置MaxLoginCount才生效

同一用戶多次登錄共用一個token: IsConcurrent == true && IsShare == true

接著我們再講講登錄的具體流程:

我們大致將它分為幾個階段:

  • 生成token

  • 生成session

  • 存儲token-id , id-session

  • 返回信息

  • 調用watcher和logger

  • 檢測登錄人數

生成token

https://github.com/weloe/token-go/blob/f58ba4d93f0f012972bf6a35b9127229b5c328fe/enforcer_internal_api.go#L12

生成token的時候,我們要判斷他是否是可多次登錄,也就是isConcurrent是否為false

如果可多次登錄並且共用token即IsConcurrent == true && IsShare == true,就判斷能否復用之前的token

這裡我們還允許用戶自定義token。

loginModel *model.Login是為了支持自定義這幾個參數

type model.Login struct {
	Device          string
	IsLastingCookie bool
	Timeout         int64
	Token           string
	IsWriteHeader   bool
}
// createLoginToken create by config.TokenConfig and model.Login
func (e *Enforcer) createLoginToken(id string, loginModel *model.Login) (string, error) {
	tokenConfig := e.config
	var tokenValue string
	var err error
	// if isConcurrent is false,
	if !tokenConfig.IsConcurrent {
		err = e.Replaced(id, loginModel.Device)
		if err != nil {
			return "", err
		}
	}

	// if loginModel set token, return directly
	if loginModel.Token != "" {
		return loginModel.Token, nil
	}

	// if share token
	if tokenConfig.IsConcurrent && tokenConfig.IsShare {
		// reuse the previous token.
		if v := e.GetSession(id); v != nil {
			tokenValue = v.GetLastTokenByDevice(loginModel.Device)
			if tokenValue != "" {
				return tokenValue, nil
			}

		}
	}

	// create new token
	tokenValue, err = e.generateFunc.Exec(tokenConfig.TokenStyle)
	if err != nil {
		return "", err
	}

	return tokenValue, nil
}

生成session

https://github.com/weloe/token-go/blob/f58ba4d93f0f012972bf6a35b9127229b5c328fe/enforcer.go#L183

先判斷是否已經存在session,如果不存在需要先創建,避免空指針

	// add tokenSign
	if session = e.GetSession(id); session == nil {
		session = model.NewSession(e.spliceSessionKey(id), "account-session", id)
	}
	session.AddTokenSign(&model.TokenSign{
		Value:  tokenValue,
		Device: loginModel.Device,
	})

存儲

https://github.com/weloe/token-go/blob/f58ba4d93f0f012972bf6a35b9127229b5c328fe/enforcer.go#L192

在存儲的時候,需要拼接key防止與其他的key重覆

	// reset session
	err = e.SetSession(id, session, loginModel.Timeout)
	if err != nil {
		return "", err
	}

	// set token-id
	err = e.adapter.SetStr(e.spliceTokenKey(tokenValue), id, loginModel.Timeout)
	if err != nil {
		return "", err
	}

返回token

https://github.com/weloe/token-go/blob/f58ba4d93f0f012972bf6a35b9127229b5c328fe/enforcer_internal_api.go#L51

這個操作對應我們配置的TokenConfig的IsReadCookieIsWriteHeaderCookieConfig

// responseToken set token to cookie or header
func (e *Enforcer) responseToken(tokenValue string, loginModel *model.Login, ctx ctx.Context) error {
   if ctx == nil {
      return nil
   }
   tokenConfig := e.config

   // set token to cookie
   if tokenConfig.IsReadCookie {
      cookieTimeout := tokenConfig.Timeout
      if loginModel.IsLastingCookie {
         cookieTimeout = -1
      }
      // add cookie use tokenConfig.CookieConfig
      ctx.Response().AddCookie(tokenConfig.TokenName,
         tokenValue,
         tokenConfig.CookieConfig.Path,
         tokenConfig.CookieConfig.Domain,
         cookieTimeout)
   }

   // set token to header
   if loginModel.IsWriteHeader {
      ctx.Response().SetHeader(tokenConfig.TokenName, tokenValue)
   }

   return nil
}

調用watcher和logger

https://github.com/weloe/token-go/blob/f58ba4d93f0f012972bf6a35b9127229b5c328fe/enforcer.go#L210

在事件發生後回調,提供擴展點

	// called watcher
	m := &model.Login{
		Device:          loginModel.Device,
		IsLastingCookie: loginModel.IsLastingCookie,
		Timeout:         loginModel.Timeout,
		JwtData:         loginModel.JwtData,
		Token:           tokenValue,
		IsWriteHeader:   loginModel.IsWriteHeader,
	}

	// called logger
	e.logger.Login(e.loginType, id, tokenValue, m)

	if e.watcher != nil {
		e.watcher.Login(e.loginType, id, tokenValue, m)
	}

檢測登錄人數

https://github.com/weloe/token-go/blob/f58ba4d93f0f012972bf6a35b9127229b5c328fe/enforcer.go#L227

要註意的是檢測登錄人數需要配置IsConcurrent == true && IsShare == false,也就是:同一用戶多次登錄多token模式,提供一個特殊值-1,如果為-1就認為不對登錄數量進行限制,不然就開始強制退出,就是刪除一部分的token

	// if login success check it
	if tokenConfig.IsConcurrent && !tokenConfig.IsShare {
		// check if the number of sessions for this account exceeds the maximum limit.
		if tokenConfig.MaxLoginCount != -1 {
			if session = e.GetSession(id); session != nil {
				// logout account until loginCount == maxLoginCount if loginCount > maxLoginCount
				for element, i := session.TokenSignList.Front(), 0; element != nil && i < session.TokenSignList.Len()-int(tokenConfig.MaxLoginCount); element, i = element.Next(), i+1 {
					tokenSign := element.Value.(*model.TokenSign)
					// delete tokenSign
					session.RemoveTokenSign(tokenSign.Value)
					// delete token-id
					err = e.adapter.Delete(e.spliceTokenKey(tokenSign.Value))
					if err != nil {
						return "", err
					}
				}
				// check TokenSignList length, if length == 0, delete this session
				if session != nil && session.TokenSignList.Len() == 0 {
					err = e.deleteSession(id)
					if err != nil {
						return "", err
					}
				}
			}
		}

測試

同一用戶只能登錄一次

https://github.com/weloe/token-go/blob/f58ba4d93f0f012972bf6a35b9127229b5c328fe/enforcer_test.go#L295

IsConcurrent = false
IsShare = false
func TestEnforcerNotConcurrentNotShareLogin(t *testing.T) {
	err, enforcer, ctx := NewTestNotConcurrentEnforcer(t)
	if err != nil {
		t.Errorf("InitWithConfig() failed: %v", err)
	}

	loginModel := model.DefaultLoginModel()

	for i := 0; i < 4; i++ {
		_, err = enforcer.LoginByModel("id", loginModel, ctx)
		if err != nil {
			t.Errorf("Login() failed: %v", err)
		}
	}
	session := enforcer.GetSession("id")
	if session.TokenSignList.Len() != 1 {
		t.Errorf("Login() failed: unexpected session.TokenSignList length = %v", session.TokenSignList.Len())
	}

}

同一用戶多次登錄共用一個token

https://github.com/weloe/token-go/blob/f58ba4d93f0f012972bf6a35b9127229b5c328fe/enforcer_test.go#L335

IsConcurrent = true
IsShare = false
func TestEnforcer_ConcurrentNotShareMultiLogin(t *testing.T) {
	err, enforcer, ctx := NewTestConcurrentEnforcer(t)
	if err != nil {
		t.Errorf("InitWithConfig() failed: %v", err)
	}

	loginModel := model.DefaultLoginModel()
	for i := 0; i < 14; i++ {
		_, err = enforcer.LoginByModel("id", loginModel, ctx)
		if err != nil {
			t.Errorf("Login() failed: %v", err)
		}
	}
	session := enforcer.GetSession("id")
	if session.TokenSignList.Len() != 12 {
		t.Errorf("Login() failed: unexpected session.TokenSignList length = %v", session.TokenSignList.Len())
	}

}

同一用戶多次登錄多token

https://github.com/weloe/token-go/blob/f58ba4d93f0f012972bf6a35b9127229b5c328fe/enforcer_test.go#LL316C17-L316C17

IsConcurrent = true
IsShare = true
func TestEnforcer_ConcurrentShare(t *testing.T) {
	err, enforcer, ctx := NewTestEnforcer(t)
	if err != nil {
		t.Errorf("InitWithConfig() failed: %v", err)
	}

	loginModel := model.DefaultLoginModel()
	for i := 0; i < 5; i++ {
		_, err = enforcer.LoginByModel("id", loginModel, ctx)
		if err != nil {
			t.Errorf("Login() failed: %v", err)
		}
	}
	session := enforcer.GetSession("id")
	if session.TokenSignList.Len() != 1 {
		t.Errorf("Login() failed: unexpected session.TokenSignList length = %v", session.TokenSignList.Len())
	}

}

至此,我們就實現了三種登錄模式,


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

-Advertisement-
Play Games
更多相關文章
  • _Java 平臺標準版 HotSpot 虛擬機垃圾收集調整指南_ 描述了 Java HotSpot 虛擬機 (Java HotSpot VM) 中包含的垃圾收集方法,並幫助您確定最適合您需要的方法。 本文檔適用於希望提高應用程式性能的應用程式開發人員和系統管理員,尤其是那些處理大量數據、使用多線程... ...
  • 首先值得說明的是,在這個項目幾乎完成之際,筆者才愈發體會到了硬體思維和軟體思維的雲泥之別。不幸的是,在此項目的實現過程中,絕大部分代碼的思維仍然是軟體思維,因此該項目主要模塊的設計部分可能並不能體現硬體操作的獨到之處,不符合硬體工程師的基本設計思維,所以此主題文章僅用於學習交流以及記錄一次FPGA項 ...
  • 剪貼板是個啥就不用多介紹了,最直觀的功能是實現應用程式之間數據共用。就是咱們常說的“複製”、“粘貼”功能。 在 Qt 中,QClipboard 類提供了相關 API 讓應用程式具備讀/寫剪貼板的能力。數據通過 QMimeData 類包裝。該類使用 MIME 類型來標識數據。比如,要包裝的數據是純文本 ...
  • golang支持兩種隨機數生成方式: math/rand // 偽隨機 crypto/rand // 真隨機 math/rand的用法:rand.Intn(100)。這個起始位置是由一個seed決定的,預設是從1開始。為了儘量隨機性,那麼我們可以每次使用不同的seed來啟動程式,就可以保證每次啟動都 ...
  • 最近熟悉 go 項目時,發現項目中有用到 github.com/yuin/gopher-lua 這個包,之前並沒有接觸過,特意去看了官方文檔和找了些網上的資料,特此記錄下。 本次介紹計劃分為兩篇文章,這一次主要介紹 github.com/yuin/gopher-lua 這個包的介紹以及基礎使用,下一 ...
  • AutoWiredAnnotationBeanPostProcessor 執行依賴註入的時候(解析@Autowired)調用了postProcessProperties 方法 這個方法首先要找到哪些屬性,方法被標註了@Autowired註解,把這些數據添加到 InjectMetadata中,然後調用 ...
  • 運行命令 go run xx.go或者 go build xx.go + ./xx package main import ( "fmt" ) func main() { fmt.Println("hello world") } ​ 基礎語法 package main import ( "fmt" ...
  • 有時候需要造大量數據進行測試,或者是用於學習,當然了這個工具類的目的就是為了後面測試easyExcel與 easyPoi 兩者性能準備的 需要引入一個 hutool工具類 hutool 工具類在此工具類上的影響並不多,好像就一個隨機生成年齡的地方,才用到了,如果不想引入可以直接刪除即可 <depen ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...