今天和咱们同享一下运用GoFrame的gtoken替换jwt完成sso登录的阅历。期间我也踩了一些坑,最终是通过阅览源码处理了项目中遇到的问题。
觉得这个阅历比较有意思,收拾一篇文章同享给咱们。
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版别的。
后面会更新视频教程,带咱们完成最新版goframe和gtoken的教程,有需求的小伙伴可以重视我一下。
演示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 := >oken.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 := >oken.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再次拜访认证接口:回来成功
以上就跑通了主体流程,便是这么简略。
分析源码
下面带咱们分析一下源码,学习一下作者是如何规划的。
改写token
首先我以为gtoken很好的规划思维是不运用refresh_token来改写token,而是服务端主动改写。
在源码的getToken
办法中做了更新refreshTime和createTime的处理。
更新createTime为当时时刻,refreshTime为当时时刻+自界说的改写时刻。
如下图所示,getToken
办法在每次履行validToken
校验token的时分都会调用,即每次校验token有用性时,假如契合改写token有用期的条件,就会进行改写操作(改写token的过期时刻,token值不变)
这样就完成了无感改写token。
GfToken结构体
咱们再来看一下GfToken的结构体,更好的了解一下作者的规划思路:
-
因为CacheMode支撑redis,也就意味着支撑集群形式。
-
咱们在发动gtoken的时分,只需求设置登录和登出途径,别的登录和登出都供给了
BeforeFunc
和AfterFunc
,让咱们能清晰的界定运用场景。
// 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改写的策略。
在仔细阅览源码之后,发现真香。
这次阅历最大的收获是:碰到不好处理的问题时,带着问题去阅览源码是十分高效的方式。
一起学习
公众号:程序员升职加薪之旅
微信号:wangzhongyang1993
B站视频:王中阳Go