[gogf/gf]服务器重启后sessionId会改变,造成session数据丢失,用户需要重新登录

2024-06-25 820 views
4

服务器重启后刷新浏览器sessionId会改变,获取不到session数据,造成用户需要重新登录

重启服务器是避免不了的,造成用户需要重新登录,这个影响太大了,这个有办法解决吗?

回答

8

@trensy 这个是由于默认情况下session是存放在内存中的,重启就没了。解决的办法是将session持久化存储,可以考虑对接redis。后续session会增加持久化存储的功能解决这个问题。

1

@trensy 原本我有计划改进持久化的session存储,下一个版本发布吧。

8

我按照官方文档做了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)
                    }
                }

            }
        },
    })
8

@trensy 我给你写个例子,骚等。

3

好的,感谢

3

@trensy 你写的GF SessionRedis集成的问题在于没有使用序列化/反序列化,Redis Server是无法识别Golang的变量类型,底层的Redis客户端也只是简单地将Golang变量转换为了字符串(使用fmt+%v),也并没有做序列化操作。存储需要序列化转换为[]byte类型存储,反序列化即将[]byte转换为Golang变量,这里我们的Session底层存储结构为map类型。

我给你一个完整SessionRedis集成的例子,本地如果有安装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请不要关闭,后续有问题欢迎随时反馈。

0

@trensy 对了,得给你说一下这里集成的一些细节。

  1. 统一使用内存来管理和操作Session数据,这样效率非常高效。
  2. 只有当内存中的Session不存在时(例如服务重启),才会去Redis中读取Session数据并反序列化到内存中的Session对象上。只会再服务重启时读取一次,效率非常高效。
  3. 内存中的Session对象内容发生改变时(IsDirtytrue)才会将内存中的数据序列化保存到Redis中,否则只是更新Session再Redis的有效时间。这样的数据同步效率非常高效。
  4. 当内存中的Session被删除后,Redis中的相关数据也会自动被删除。
7

@johngcn 辛苦,感谢 我有个疑问,比如这句话“只有当内存中的Session不存在时(例如服务重启),才会去Redis中读取Session数据并反序列化到内存中的Session对象上”,我的服务器不止一台,有可能一次http请求到一台服务器上,这台服务器session数据有更改,如果不是每次都从redis拉数据更新到内存,其他服务器内存的session数据我怎么同步(分布式架构数据一致性的问题)。

7

@johngcn s.dirty 这个变量,需要一个设置为false的方法,我将SESSION数据存储到Redis中后,需要操作s.dirty=false

9

如果是这样的话,那你把这行删除即可: QQ截图20190728153650 这种方式的缺点是每一次带SESSION ID的请求都会去Redis中查询一次;另外也可以使用订阅者/消费者模式的话,当有更新时往另外一个Redis消息队列中丢一个消息,由应用服务器通过goroutine开一个长连接保持消费,消费并更新Server的Sessions缓存对象。订阅消费模式的话后续我写插件的时候尝试一下,你可以先使用这次每次查询的方式。

@johngcn s.dirty 这个变量,需要一个设置为false的方法,我将SESSION数据存储到Redis中后,需要操作s.dirty=false

你不用设置s.dirty=false,每一次请求时的SESSION对象都是新的,只是里面的数据s.data是从Redis中读取出来的。

9

@trensy 另外你注意一个细节,就是Golang使用json方式的"反序列化"其实没有保留变量类型信息的(像PHP的话反序列化会直接生成对应序列化前的PHP对象变量)。也就是说假如你往SESSION中存入一个对象,当你用Get方式读取出来时,无法使用断言来执行转换,而应当改用GetStruct方法。这一点的话,也许会有些改进的办法,你可以研究一下给个建议。

1

数据同步,保持数据一致性处理起来很麻烦,是不是可以根据github.com/gorilla/securecookie 这个包将session数据放入cookie然后加密,当然jwt也可以不过jwt比较麻烦,不适合网页项目

0

@trensy 按照我先前说的方式,就不会有数据一致性的问题了。你指的加密COOKIE的方式严格上来说无法替代SESSION,因为COOKIE的大小是受限制的。有的应用是有对COOKIE加密后存储到客户端的,这种情况比较常见,我之前的项目中也有处理过,如果需要对COOKIE加密,你可以自己封装COOKIE操作。

6

我倒是觉得现在直接写session的比较少了... 至少我遇见的项目都是前后端分离,前端api使用jwt认证的. jwt不复杂,gf-jwt就能够满足大部分需求.

0

没有做前后端分离的项目要用到,建议参考iris session redis 这块的代码

9

@trensy 目前GFSession功能单独剥离出来gsession模块进行管理,并且为方便开发者使用,gsession模块提供了基于内存、文件、Redis三种方式的Session存储方式,详情请参考开发文档:https://goframe.org/os/gsession/index

该功能将会在v1.10版本中发布。

9

666,感谢