本篇將講講登錄庫中的三種登錄模式的實現: 同一用戶只能登錄一次, 同一用戶多次登錄多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
生成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
這個操作對應我們配置的TokenConfig的IsReadCookie
、IsWriteHeader
和CookieConfig
// 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
}
}
}
}
測試
同一用戶只能登錄一次
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
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
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())
}
}
至此,我們就實現了三種登錄模式,