用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的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
- }
- }
- }
- }
复制代码 测试
同一用户只能登录一次
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())
- }
- }
复制代码 至此,我们就实现了三种登录模式,
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |