• 用go设计开发一个自己的轻量级登录库/框架吧(业务篇)


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

    至此,我们就实现了三种登录模式,

  • 相关阅读:
    顺序读写函数的介绍:fputs & fgets
    Windows虚拟机访问网页证书错误问题
    罗丹明 800,CAS号:101027-54-7
    MySQL——数据类型
    jQuery入门
    01-JVM-类加载篇
    Postman核心功能解析 —— 参数化和测试报告
    Windows 安装 Docker Desktop 到其他盘、迁移虚拟硬盘映像文件、压缩虚拟硬盘映像占用空间
    阿里十年资深码农共享 SpringCloud 微服务架构实战文档
    JAVA 歌词解析 采用 TreeMap处理
  • 原文地址:https://www.cnblogs.com/weloe/p/17397071.html