今天和咱们同享一下运用GoFrame的gtoken替换jwt完成sso登录的阅历。期间我也踩了一些坑,最终是通过阅览源码处理了项目中遇到的问题。

觉得这个阅历比较有意思,收拾一篇文章同享给咱们。

jwt的问题

首先阐明一个jwt存在的问题,也便是要替换jwt的原因:

  1. jwt无法在服务端主动退出的问题
  2. jwt无法报废已公布的令牌,只能比及令牌过期问题
  3. jwt携带很多用户扩展信息导致下降传输效率问题

jwt的恳求流程图

【带你读源码】GToken替换JWT实现SSO单点登录

gtoken的优势

gtoken的恳求流程和jwt的基本一致。

gtoken的优势便是能帮助咱们处理jwt的问题,别的还供给好用的特性,比方:

  1. gtoken支撑单点运用运用内存存储,支撑个人项目文件存储,也支撑企业集群运用redis存储,完全适用于个人和企业生产级运用;
  2. 有用的防止了jwt服务端无法退出问题
  3. 处理jwt无法报废已公布的令牌,只能比及令牌过期问题
  4. 通过用户扩展信息存储在服务端,有用规避了jwt携带很多用户扩展信息导致下降传输效率问题
  5. 有用防止jwt需求客户端完成续签功用,添加客户端复杂度;支撑服务端主动续期,客户端不需求关心续签逻辑;

留意问题

  1. 支撑服务端缓存主动续期功用,不需求通过refresh_token改写token,简化了客户端的操作

  2. 版别问题千万留意:在gtoken v1.5.0全面适配GoFrame v2.0.0 ; GoFrame v1.X.X 请运用GfToken v1.4.X相关版别

TIPS:下面我的演示demo和源码阅览都是根据v1.4.x版别的。

后面会更新视频教程,带咱们完成最新版goframe和gtoken的教程,有需求的小伙伴可以重视我一下。

演示demo

下面的演示demo可以复制到本地main.go文件中履行,更新依靠的时分千万留意版别。

要点说一下踩的坑,Login办法会要求咱们回来两个值:

  1. 第一个值对应userKey,后续咱们可以根据userKey取得token
  2. 第二个值对应data,是interface{}类型,咱们可以在这里界说例如userid、username等数据。

先有这个概念即可,为了让咱们更好的了解,文章最后会带咱们读源码。

入门示例

代码段的要害逻辑,已经添加了注释。

package main
import (
   "github.com/goflyfox/gtoken/gtoken"
   "github.com/gogf/gf/frame/g"
   "github.com/gogf/gf/net/ghttp"
   "github.com/gogf/gf/os/glog"
)
var TestServerName string
//var TestServerName string = "gtoken"
func main() {
   glog.Info("########service start...")
   g.Cfg().SetPath("example/sample")
   s := g.Server(TestServerName)
   initRouter(s)
   glog.Info("########service finish.")
   s.Run()
}
var gfToken *gtoken.GfToken
/*
统一路由注册
*/
func initRouter(s *ghttp.Server) {
   // 不认证接口
   s.Group("/", func(group *ghttp.RouterGroup) {
      group.Middleware(CORS)
      // 调试路由
      group.ALL("/hello", func(r *ghttp.Request) {
         r.Response.WriteJson(gtoken.Succ("hello"))
      })
   })
   // 认证接口
   loginFunc := Login
   // 发动gtoken
   gfToken := &gtoken.GfToken{
      ServerName:       TestServerName,
      LoginPath:        "/login",
      LoginBeforeFunc:  loginFunc,
      LogoutPath:       "/user/logout",
      AuthExcludePaths: g.SliceStr{"/user/info", "/system/user/info"}, // 不阻拦途径 /user/info,/system/user/info,/system/user,
      MultiLogin:       g.Config().GetBool("gToken.MultiLogin"),
   }
   s.Group("/", func(group *ghttp.RouterGroup) {
      group.Middleware(CORS)
      gfToken.Middleware(group)
      group.ALL("/system/user", func(r *ghttp.Request) {
         r.Response.WriteJson(gtoken.Succ("system user"))
      })
      group.ALL("/user/data", func(r *ghttp.Request) {
         r.Response.WriteJson(gfToken.GetTokenData(r))
      })
      group.ALL("/user/info", func(r *ghttp.Request) {
         r.Response.WriteJson(gtoken.Succ("user info"))
      })
      group.ALL("/system/user/info", func(r *ghttp.Request) {
         r.Response.WriteJson(gtoken.Succ("system user info"))
      })
   })
   // 发动gtoken
   gfAdminToken := &gtoken.GfToken{
      ServerName: TestServerName,
      //Timeout:         10 * 1000,
      LoginPath:        "/login",
      LoginBeforeFunc:  loginFunc,
      LogoutPath:       "/user/logout",
      AuthExcludePaths: g.SliceStr{"/admin/user/info", "/admin/system/user/info"}, // 不阻拦途径 /user/info,/system/user/info,/system/user,
      MultiLogin:       g.Config().GetBool("gToken.MultiLogin"),
   }
   s.Group("/admin", func(group *ghttp.RouterGroup) {
      group.Middleware(CORS)
      gfAdminToken.Middleware(group)
      group.ALL("/system/user", func(r *ghttp.Request) {
         r.Response.WriteJson(gtoken.Succ("system user"))
      })
      group.ALL("/user/info", func(r *ghttp.Request) {
         r.Response.WriteJson(gtoken.Succ("user info"))
      })
      group.ALL("/system/user/info", func(r *ghttp.Request) {
         r.Response.WriteJson(gtoken.Succ("system user info"))
      })
   })
}
func Login(r *ghttp.Request) (string, interface{}) {
   username := r.GetString("username")
   passwd := r.GetString("passwd")
   if username == "" || passwd == "" {
      r.Response.WriteJson(gtoken.Fail("账号或密码错误."))
      r.ExitAll()
   }
   return username, "1"
   /**
   回来的第一个参数对应:userKey
   回来的第二个参数对应:data
   {
       "code": 0,
       "msg": "success",
       "data": {
           "createTime": 1652838582190,
           "data": "1",
           "refreshTime": 1653270582190,
           "userKey": "王中阳",
           "uuid": "ac75676efeb906f9959cf35f779a1d38"
       }
   }
   */
}
// 跨域
func CORS(r *ghttp.Request) {
   r.Response.CORSDefault()
   r.Middleware.Next()
}

运行效果

发动项目:

【带你读源码】GToken替换JWT实现SSO单点登录

拜访不认证接口:回来成功

【带你读源码】GToken替换JWT实现SSO单点登录

未登录时拜访认证接口:回来错误提示

【带你读源码】GToken替换JWT实现SSO单点登录

恳求登录接口:回来token

【带你读源码】GToken替换JWT实现SSO单点登录

携带token再次拜访认证接口:回来成功

【带你读源码】GToken替换JWT实现SSO单点登录

以上就跑通了主体流程,便是这么简略。

分析源码

下面带咱们分析一下源码,学习一下作者是如何规划的。

改写token

首先我以为gtoken很好的规划思维是不运用refresh_token来改写token,而是服务端主动改写

在源码的getToken办法中做了更新refreshTime和createTime的处理。

更新createTime为当时时刻,refreshTime为当时时刻+自界说的改写时刻。

【带你读源码】GToken替换JWT实现SSO单点登录

如下图所示,getToken办法在每次履行validToken校验token的时分都会调用,即每次校验token有用性时,假如契合改写token有用期的条件,就会进行改写操作(改写token的过期时刻,token值不变)

【带你读源码】GToken替换JWT实现SSO单点登录

这样就完成了无感改写token。

GfToken结构体

咱们再来看一下GfToken的结构体,更好的了解一下作者的规划思路:

  1. 因为CacheMode支撑redis,也就意味着支撑集群形式。

  2. 咱们在发动gtoken的时分,只需求设置登录和登出途径,别的登录和登出都供给了BeforeFuncAfterFunc,让咱们能清晰的界定运用场景。

// GfToken gtoken结构体
type GfToken struct {
   // GoFrame server name
   ServerName string
   // 缓存形式 1 gcache 2 gredis 默许1
   CacheMode int8
   // 缓存key
   CacheKey string
   // 超时时刻 默许10天(毫秒)
   Timeout int
   // 缓存改写时刻 默许为超时时刻的一半(毫秒)
   MaxRefresh int
   // Token分隔符
   TokenDelimiter string
   // Token加密key
   EncryptKey []byte
   // 认证失利中文提示
   AuthFailMsg string
   // 是否支撑多端登录,默许false
   MultiLogin bool
   // 是否是全局认证,兼容历史版别,已废弃
   GlobalMiddleware bool
   // 中间件类型 1 GroupMiddleware 2 BindMiddleware  3 GlobalMiddleware
   MiddlewareType uint
   // 登录途径
   LoginPath string
   // 登录验证办法 return userKey 用户标识 假如userKey为空,完毕履行
   LoginBeforeFunc func(r *ghttp.Request) (string, interface{})
   // 登录回来办法
   LoginAfterFunc func(r *ghttp.Request, respData Resp)
   // 登出地址
   LogoutPath string
   // 登出验证办法 return true 继续履行,不然完毕履行
   LogoutBeforeFunc func(r *ghttp.Request) bool
   // 登出回来办法
   LogoutAfterFunc func(r *ghttp.Request, respData Resp)
   // 阻拦地址
   AuthPaths g.SliceStr
   // 阻拦扫除地址
   AuthExcludePaths g.SliceStr
   // 认证验证办法 return true 继续履行,不然完毕履行
   AuthBeforeFunc func(r *ghttp.Request) bool
   // 认证回来办法
   AuthAfterFunc func(r *ghttp.Request, respData Resp)
}

思考题

我有N个子体系如何用gtoken完成sso登录呢?即完成一个子体系登录,其他各个子体系都主动登录,而无需二次登录呢?

想一想

.

.

.

我想到的处理方案是配合cookie完成:各个子体系二级域名不一致,可是主域名一致。

我在登录之后把token写入主域名的cookie中进行同享,前端网站通过cookie取得token恳求服务接口。

一起在改写token之后,也要改写cookie的有用期,防止cookie失效导致获取不到token。

进一步阅览源码

在通过又一次仔细阅览源码之后,找到了改写cookie有用期的适宜场景:AuthAfterFunc,咱们可以重写这个办法,来完成验证通往后的操作:

假如token验证有用则改写cookie有用期;假如验证无效则自界说回来信息。(往往咱们自己项目中的code码和gtoken界说的不一致,可是gtoken支撑十分便利的重写回来值)

【带你读源码】GToken替换JWT实现SSO单点登录

总结

咱们项目之前是运用jwt完成sso登录,在刚刚拿到需求要重写时,自己也是一头雾水。

在没有仔细阅览gtoken源码之前,我已经规划了refresh_token改写的策略。

在仔细阅览源码之后,发现真香。

【带你读源码】GToken替换JWT实现SSO单点登录

这次阅历最大的收获是:碰到不好处理的问题时,带着问题去阅览源码是十分高效的方式。

一起学习

【带你读源码】GToken替换JWT实现SSO单点登录

公众号:程序员升职加薪之旅

微信号:wangzhongyang1993

B站视频:王中阳Go