服务器重启后刷新浏览器sessionId会改变,获取不到session数据,造成用户需要重新登录
重启服务器是避免不了的,造成用户需要重新登录,这个影响太大了,这个有办法解决吗?
服务器重启后刷新浏览器sessionId会改变,获取不到session数据,造成用户需要重新登录
重启服务器是避免不了的,造成用户需要重新登录,这个影响太大了,这个有办法解决吗?
@trensy 这个是由于默认情况下session是存放在内存中的,重启就没了。解决的办法是将session持久化存储,可以考虑对接redis。后续session会增加持久化存储的功能解决这个问题。
@trensy 原本我有计划改进持久化的session存储,下一个版本发布吧。
我按照官方文档做了redis的持久化,没用啊
keyPre:=g.Config().GetString("system.setting.key_pre")
g.Server().BindHookHandlerByMap("/*", map[string]ghttp.HandlerFunc{
ghttp.HOOK_BEFORE_SERVE : func(r *ghttp.Request){
//判断是否文件请求
//session 写入redis
if !r.IsFileRequest(){
sessionId := r.Cookie.SessionId()
sessionKey := keyPre+"session_"+sessionId
conn := g.Redis().Conn()
defer conn.Close()
sessionData, err := conn.Do("GET", sessionKey)
if err != nil {
panic(err)
}
if sessionData != nil{
if sessionDataNew ,ok:= sessionData.(map[string]interface{});ok{
r.Session.Sets(sessionDataNew)
}
}
}
},
ghttp.HOOK_AFTER_SERVE: func(r *ghttp.Request){
//判断是否文件请求
//session 写入redis
if !r.IsFileRequest(){
sessionData := r.Session.Map()
sessionId := r.Cookie.SessionId()
conn := g.Redis().Conn()
defer conn.Close()
sessionKey := keyPre+"session_"+sessionId
if len(sessionData) == 0{
_, err := conn.Do("DEL", sessionKey)
if err != nil {
panic(err)
}
}else{
_, err := conn.Do("SET", sessionKey, sessionData)
if err != nil {
panic(err)
}
}
}
},
})
@trensy 我给你写个例子,骚等。
好的,感谢
@trensy 你写的GF Session
与Redis
集成的问题在于没有使用序列化/反序列化,Redis Server
是无法识别Golang的变量类型,底层的Redis
客户端也只是简单地将Golang变量转换为了字符串(使用fmt+%v
),也并没有做序列化操作。存储需要序列化转换为[]byte
类型存储,反序列化即将[]byte
转换为Golang变量,这里我们的Session
底层存储结构为map
类型。
我给你一个完整Session
与Redis
集成的例子,本地如果有安装Redis Server
的话可直接运行:
https://github.com/gogf/gf/blob/master/geg/net/ghttp/server/session/session_redis.go
package main
import (
"github.com/gogf/gf/g"
"github.com/gogf/gf/g/net/ghttp"
"github.com/gogf/gf/g/os/gtime"
)
// 测试,SESSION写入
func SessionSet(r *ghttp.Request) {
r.Session.Set("time", gtime.Second())
r.Response.WriteJson("ok")
}
// 测试,SESSION读取
func SessionGet(r *ghttp.Request) {
r.Response.WriteJson(r.Session.Map())
}
// 请求处理之前将Redis中的数据读取出来并存储到SESSION对象中。
func RedisHandlerGet(r *ghttp.Request) {
if !r.IsFileRequest() {
id := r.Cookie.GetSessionId()
if id == "" {
return
}
// 当内存中的SESSION存在时不需要读取Redis
if r.Session.Size() > 0 {
return
}
// SESSION不存在时,例如服务重启,自动从Redis读取并恢复数据
value, err := g.Redis().DoVar("GET", id)
if err != nil {
panic(err)
}
if !value.IsNil() {
if err := r.Session.Restore(value.Bytes()); err != nil {
panic(err)
}
}
}
}
// 请求结束时将SESSION数据存储到Redis中,或者在SESSION删除时也删除Redis中的数据。
func RedisHandlerSet(r *ghttp.Request) {
if !r.IsFileRequest() {
id := r.Cookie.GetSessionId()
if id == "" {
return
}
err := (error)(nil)
value := ([]byte)(nil)
if r.Session.Size() > 0 {
if value, err = r.Session.Export(); err == nil {
if len(value) == 0 {
return
} else if !r.Session.IsDirty() {
// 更新过期时间
_, err = g.Redis().Do("EXPIRE", id, r.Server.GetSessionMaxAge())
} else {
// 更新Redis数据
_, err = g.Redis().Do("SETEX", id, r.Server.GetSessionMaxAge(), value)
}
}
} else {
// 清空SESSION后自动删除Redis数据
_, err = g.Redis().Do("DEL", id)
}
if err != nil {
panic(err)
}
}
}
func main() {
s := g.Server()
s.BindHandler("/set", SessionSet)
s.BindHandler("/get", SessionGet)
s.BindHookHandlerByMap("/*", map[string]ghttp.HandlerFunc{
ghttp.HOOK_BEFORE_SERVE: RedisHandlerGet,
ghttp.HOOK_AFTER_SERVE: RedisHandlerSet,
})
s.SetPort(8199)
s.Run()
}
这里的序列化/反序列化使用的是标准库的json
包。后续我会写一个Session与Redis集成的插件,方便大家使用。此外,为了代码实现看起来更加优雅,我稍微调整了一下ghttp.Session
的一些方法,请更新到v1.8.3
后试试看。
这个issue请不要关闭,后续有问题欢迎随时反馈。
@trensy 对了,得给你说一下这里集成的一些细节。
IsDirty
为true
)才会将内存中的数据序列化保存到Redis中,否则只是更新Session再Redis的有效时间。这样的数据同步效率非常高效。@johngcn 辛苦,感谢 我有个疑问,比如这句话“只有当内存中的Session不存在时(例如服务重启),才会去Redis中读取Session数据并反序列化到内存中的Session对象上”,我的服务器不止一台,有可能一次http请求到一台服务器上,这台服务器session数据有更改,如果不是每次都从redis拉数据更新到内存,其他服务器内存的session数据我怎么同步(分布式架构数据一致性的问题)。
@johngcn s.dirty 这个变量,需要一个设置为false的方法,我将SESSION数据存储到Redis中后,需要操作s.dirty=false
如果是这样的话,那你把这行删除即可: 这种方式的缺点是每一次带SESSION ID的请求都会去Redis中查询一次;另外也可以使用订阅者/消费者模式的话,当有更新时往另外一个Redis消息队列中丢一个消息,由应用服务器通过goroutine开一个长连接保持消费,消费并更新Server的Sessions缓存对象。订阅消费模式的话后续我写插件的时候尝试一下,你可以先使用这次每次查询的方式。
@johngcn s.dirty 这个变量,需要一个设置为false的方法,我将SESSION数据存储到Redis中后,需要操作s.dirty=false
你不用设置s.dirty=false
,每一次请求时的SESSION对象都是新的,只是里面的数据s.data
是从Redis中读取出来的。
@trensy 另外你注意一个细节,就是Golang使用json方式的"反序列化"其实没有保留变量类型信息的(像PHP的话反序列化会直接生成对应序列化前的PHP对象变量)。也就是说假如你往SESSION中存入一个对象,当你用Get
方式读取出来时,无法使用断言来执行转换,而应当改用GetStruct
方法。这一点的话,也许会有些改进的办法,你可以研究一下给个建议。
数据同步,保持数据一致性处理起来很麻烦,是不是可以根据github.com/gorilla/securecookie 这个包将session数据放入cookie然后加密,当然jwt也可以不过jwt比较麻烦,不适合网页项目
@trensy 按照我先前说的方式,就不会有数据一致性的问题了。你指的加密COOKIE的方式严格上来说无法替代SESSION,因为COOKIE的大小是受限制的。有的应用是有对COOKIE加密后存储到客户端的,这种情况比较常见,我之前的项目中也有处理过,如果需要对COOKIE加密,你可以自己封装COOKIE操作。
我倒是觉得现在直接写session的比较少了... 至少我遇见的项目都是前后端分离,前端api使用jwt认证的. jwt不复杂,gf-jwt就能够满足大部分需求.
没有做前后端分离的项目要用到,建议参考iris session redis 这块的代码
@trensy 目前GF
将Session
功能单独剥离出来gsession
模块进行管理,并且为方便开发者使用,gsession
模块提供了基于内存、文件、Redis三种方式的Session
存储方式,详情请参考开发文档:https://goframe.org/os/gsession/index
该功能将会在v1.10
版本中发布。
666,感谢