源码解析gtoken替换jwt实现sso登录

目录
  • jwt的问题
    • jwt的请求流程图
  • gtoken的优势
  • 注意问题
  • 演示demo
  • 分析源码
    • 刷新token
    • GfToken结构体
  • 思考题
  • 总结

jwt的问题

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

  • jwt无法在服务端主动退出的问题
  • jwt无法作废已颁布的令牌,只能等到令牌过期问题
  • jwt携带大量用户扩展信息导致降低传输效率问题

jwt的请求流程图

gtoken的优势

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

gtoken的优势就是能帮助我们解决jwt的问题,另外还提供好用的特性,比如:

  • gtoken支撑单点应用测试使用内存存储,支持个人项目文件存储,也支持企业集群使用redis存储;完全适用于企业生产级使用;
  • 有效的避免了jwt服务端无法退出问题;
  • 解决jwt无法作废已颁布的令牌,只能等到令牌过期问题;
  • 通过用户扩展信息存储在服务端,有效规避了jwt携带大量用户扩展信息导致降低传输效率问题;
  • 有效避免jwt需要客户端实现续签功能,增加客户端复杂度;支持服务端自动续期,客户端不需要关心续签逻辑;

注意问题

  • 支持服务端缓存自动续期功能,不需要通过refresh_token刷新token,简化了客户端的操作
  • 版本问题千万注意:在gtoken v1.5.0全面适配GoFrame v2.0.0 ; GoFrame v1.X.X 请使用GfToken v1.4.X相关版本

TIPS:下面我的演示demo和源码阅读都是基于v1.4.x版本的。

演示demo

下面的演示demo可以复制到本地main.go文件中执行,更新依赖的时候千万注意版本。

重点说一下踩的坑:

Login方法会要求我们返回两个值:

第一个值对应userKey,后续我们可以根据userKey获得token 第二个值对应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()
}

启动项目:

访问不认证接口:返回成功

未登录时访问认证接口:返回错误

请求登录接口:返回token

携带token再次访问认证接口:返回成功

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

分析源码

tips:下面带大家看的是 v1.4.1

下面带大家分析一下源码,学习一下作者是如何设计的。

刷新token

首先我认为gtoken很好的设计思想是不使用refresh_token来刷新token,而是服务端主动刷新。

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

更新createTime为当前时间,refreshTime为当前时间+自定义的刷新时间。

如下图所示,getToken方法在每次执行validToken校验token的时候都会调用,即每次校验token有效性时,如果符合刷新token有效期的条件,就会进行刷新操作(刷新token的过期时间,token值不变)

这样就实现了无感刷新token。

GfToken结构体

我们再来看一下GfToken的结构体,更好的理解一下作者的设计思路:

  • 因为CacheMode支持gredis,也就意味着支持集群模式。
  • 我们在启动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支持非常方便的重写返回值)

总结

我们项目之前是使用jwt实现sso登录,在刚刚拿到需求要重写时,自己可以说是一头雾水。

在没有认真阅读gtoken源码之前,我已经设计了refresh_token刷新的策略。

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

这次经历最大的收获是:带着需求去阅读源码是非常高效的学习方式。

试问自己一下:

如果让我设计的话我会怎么设计?

为什么作者这么要设计?为什么作者能用560行代码就设计出了gtoken?

最后向作者Jflyfox致敬,这是官方文档

以上就是源码解析gtoken替换jwt实现sso登录的详细内容,更多关于gtoken替换jwt登录sso的资料请关注我们其它相关文章!

(0)

相关推荐

  • gtoken替换jwt实现sso登录的问题小结

    目录 jwt的问题 jwt的请求流程图 gtoken的优势 注意问题 演示demo 今天和大家分享一下使用gtoken替换jwt实现sso登录的经验,为了让大家更好的理解会带大家读一下重点的源码. jwt的问题 首先说明一个jwt存在的问题,也就是要替换jwt的原因: jwt无法在服务端主动退出的问题 jwt无法作废已颁布的令牌,只能等到令牌过期问题 jwt携带大量用户扩展信息导致降低传输效率问题 jwt的请求流程图 gtoken的优势 gtoken的请求流程和jwt的基本一致. gtoken的

  • 源码解析gtoken替换jwt实现sso登录

    目录 jwt的问题 jwt的请求流程图 gtoken的优势 注意问题 演示demo 分析源码 刷新token GfToken结构体 思考题 总结 jwt的问题 首先说明一个jwt存在的问题,也就是要替换jwt的原因: jwt无法在服务端主动退出的问题 jwt无法作废已颁布的令牌,只能等到令牌过期问题 jwt携带大量用户扩展信息导致降低传输效率问题 jwt的请求流程图 gtoken的优势 gtoken的请求流程和jwt的基本一致. gtoken的优势就是能帮助我们解决jwt的问题,另外还提供好用的

  • gtoken替换jwt实现sso登录的排雷避坑

    目录 前言 gtoken gtoken替换jwt解决的问题 兼容JWT gtoken实现原理 踩坑之旅 1 gtoken版本 2 gtoken存储问题 3 不能跨环境使用token 4 测试账号不规范问题 总结 前言 前段时间整理的文章:gtoken替换jwt实现sso登录 | 带你读源码 收到了大家积极的反馈,gtoken替换jwt实现sso登录的开发过程是比较稳健的,但是在我们测试联调的过程中暴露出了很多问题. 如果大家也想使用gtoken替换jwt实现sso登录,那么这篇文章可以减少很多大

  • .properties文件读取及占位符${...}替换源码解析

    前言 我们在开发中常遇到一种场景,Bean里面有一些参数是比较固定的,这种时候通常会采用配置的方式,将这些参数配置在.properties文件中,然后在Bean实例化的时候通过Spring将这些.properties文件中配置的参数使用占位符"${}"替换的方式读入并设置到Bean的相应参数中. 这种做法最典型的就是JDBC的配置,本文就来研究一下.properties文件读取及占位符"${}"替换的源码,首先从代码入手,定义一个DataSource,模拟一下JDB

  • Java源码解析之TypeVariable详解

    TypeVariable,类型变量,描述类型,表示泛指任意或相关一类类型,也可以说狭义上的泛型(泛指某一类类型),一般用大写字母作为变量,比如K.V.E等. 源码 public interface TypeVariable<D extends GenericDeclaration> extends Type { //获得泛型的上限,若未明确声明上边界则默认为Object Type[] getBounds(); //获取声明该类型变量实体(即获得类.方法或构造器名) D getGenericDe

  • java TreeMap源码解析详解

    java TreeMap源码解析详解 在介绍TreeMap之前,我们来了解一种数据结构:排序二叉树.相信学过数据结构的同学知道,这种结构的数据存储形式在查找的时候效率非常高. 如图所示,这种数据结构是以二叉树为基础的,所有的左孩子的value值都是小于根结点的value值的,所有右孩子的value值都是大于根结点的.这样做的好处在于:如果需要按照键值查找数据元素,只要比较当前结点的value值即可(小于当前结点value值的,往左走,否则往右走),这种方式,每次可以减少一半的操作,所以效率比较高

  • Vue源码解析之数组变异的实现

    力有不逮的对象 众所周知,在 Vue 中,直接修改对象属性的值无法触发响应式.当你直接修改了对象属性的值,你会发现,只有数据改了,但是页面内容并没有改变. 这是什么原因? 原因在于: Vue 的响应式系统是基于Object.defineProperty这个方法的,该方法可以监听对象中某个元素的获取或修改,经过了该方法处理的数据,我们称其为响应式数据.但是,该方法有一个很大的缺点,新增属性或者删除属性不会触发监听,举个栗子: var vm = new Vue({ data () { return

  • Java源码解析之超级接口Map

    前言 我们在前面说到的无论是链表还是数组,都有自己的优缺点,数组查询速度很快而插入很慢,链表在插入时表现优秀但查询无力.哈希表则整合了数组与链表的优点,能在插入和查找等方面都有不错的速度.我们之后要分析的HashMap就是基于哈希表实现的,不过在JDK1.8中还引入了红黑树,其性能进一步提升了. 今天我们来说一说超级接口Map. 一.接口Map Map是基于Key-Value的数据格式,并且key值不能重复,每个key对应的value值唯一.Map的key也可以为null,但不可重复. 在看Ma

  • Java源码解析之Iterable接口

    这里我们给定一个集合strings 一.写法1–循环 for (int i = 0, len = strings.size(); i < len; i++) { System.out.println(strings.get(i)); } 二.写法2–foreach循环 for (String var : strings) { System.out.println(var); } 三.写法3–Iterator Iterator iterator = strings.iterator(); whil

  • Java源码解析之LinkedHashMap

    一.成员变量 先来看看存储元素的结构吧: static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } } 这个Entry在HashMap中被引用过,主要是为了能让LinkedHashMap也支持树化.

随机推荐