session管理机制设计与实现

原生Go语言没有实现session管理机制,所以如果使用原生Go语言进行web编程,我们需要自己进行session管理机制的设计与实现,本文将就此进行详细介绍,并实现一个简单的session管理机制。
session信息可以使用内存、文件或数据库等方式进行存储,考虑到对不同存储方式的兼容,我们设计的session管理机制应该能很方便的在不同存储方式之间进行切换。所以,session管理机制可以分为两部分内容:session管理器和session存储器,session管理器主要负责多种存储方式的共同操作部分,例如,cookie的读取与设置、session ID的生成,以及一些共同需要的参数设置等等。session管理器结构可设置如下:

1
2
3
4
5
6
7
8
//session管理器
type SessionManager struct {
    cookieName    string          //cookie名称
    cookieExpire  int             //cookie有效期时间(单位:分钟,0表示会话cookie)
    sessionExpire int64           //session有效期时间(单位:分钟)
    gcDuration    int             //垃圾回收机制运行间隔时间(单位:分钟)
    provider      SessionProvider //session存储器
}

其创建方法为:

1
2
3
4
5
6
7
8
9
10
//创建session管理器
func NewManager(cookieName string, cookieExpire int, sessionExpire int64, gcDuration int, provider SessionProvider) *SessionManager {
    return &SessionManager{
        cookieName:    cookieName,
        cookieExpire:  cookieExpire,
        sessionExpire: sessionExpire,
        gcDuration:    gcDuration,
        provider:      provider,
    }
}

而session存储器则对应具体的存储方式,只需负责根据session ID对session数据进行读写操作,这部分根据存储方式不同而不同,但方法签名是一致的,可以定义其接口类型为:

1
2
3
4
5
6
7
8
9
//session存储器
type SessionProvider interface {
    create(sessionId string, data map[string]interface{}) error //创建session
    get(sessionId, key string) (string, error)                  //读取session键值
    getAll(sessionId string) (map[string]string, error)         //读取session所有键值对
    set(sessionId, key string, value interface{}) error         //设置session键值
    destroy(sessionId string) error                             //销毁session
    gc(expire int64) error                                      //垃圾回收:删除过期session
}

接下来定义session管理器生成session ID的方法,可根据请求头信息生成,这里只是举一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
//生成session ID
func (sm *SessionManager) createSessionId(req *http.Request) string {
    addr := req.RemoteAddr
    userAgent := req.Header.Get("User-Agent")
    rand.Seed(time.Now().UnixNano())
    n := rand.Intn(10000)
    str := addr + "_" + userAgent + "_" + strconv.Itoa(n)
    h := md5.New()
    h.Write([]byte(str))
    cipherStr := h.Sum(nil)
    return hex.EncodeToString(cipherStr)
}

session管理器的各个方法都需要读cookie,获取session ID:

1
2
3
4
5
6
7
8
9
10
11
//读cookie,获取session ID
func (sm *SessionManager) getSessionId(req *http.Request) (string, error) {
    c, err := req.Cookie(sm.cookieName)
    if err != nil {
        return "", errors.New("Reading cookie failed: " + err.Error())
    }
    if len(c.Value) == 0 { //尚未设置cookie
        return "", errors.New("Cookie does not exists: " + sm.cookieName)
    }
    return c.Value, nil
}

最后就是session管理器创建session、读取session、写入session、销毁session、session GC等方法的定义,这些方法比较简单,只是调用session存储器对应的方法即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
//创建session
func (sm *SessionManager) Create(writer *http.ResponseWriter, req *http.Request, data map[string]interface{}) error {
    sessionId, _ := sm.getSessionId(req)
    if len(sessionId) > 0 {
        data, _ := sm.provider.getAll(sessionId)
        if data != nil { //已有session,无需创建
            return nil
        }
    }
    sessionId = sm.createSessionId(req)
    if len(sessionId) == 0 {
        return errors.New("Length of sessionId is 0")
    }
    err := sm.provider.create(sessionId, data)
    if err != nil {
        return err
    }
    if sm.cookieExpire == 0 { //会话cookie
        http.SetCookie(*writer, &http.Cookie{
            Name:     sm.cookieName,
            Value:    sessionId,
            Path:     "/", //一定要设置为根目录,才能在所有页面生效
            HttpOnly: true,
        })
    } else { //持久cookie
        expire, _ := time.ParseDuration(strconv.Itoa(sm.cookieExpire) + "m")
        http.SetCookie(*writer, &http.Cookie{
            Name:     sm.cookieName,
            Value:    sessionId,
            Path:     "/", //一定要设置为根目录,才能在所有页面生效
            Expires:  time.Now().Add(expire),
            HttpOnly: true,
        })
    }
    return nil
}
 
//获取session键值
func (sm *SessionManager) Get(writer *http.ResponseWriter, req *http.Request, key string) (string, error) {
    sessionId, _ := sm.getSessionId(req)
    if len(sessionId) == 0 {
        return "", errors.New("Length of sessionId is 0")
    }
    return sm.provider.get(sessionId, key)
}
 
//读取session所有键值对
func (sm *SessionManager) GetAll(writer *http.ResponseWriter, req *http.Request) (map[string]string, error) {
    sessionId, _ := sm.getSessionId(req)
    if len(sessionId) == 0 {
        return nil, errors.New("Length of sessionId is 0")
    }
    return sm.provider.getAll(sessionId)
}
 
//设置session键值
func (sm *SessionManager) Set(writer *http.ResponseWriter, req *http.Request, key string, value interface{}) error {
    sessionId, _ := sm.getSessionId(req)
    if len(sessionId) == 0 {
        return errors.New("Length of sessionId is 0")
    }
    return sm.provider.set(sessionId, key, value)
}
 
//销毁session
func (sm *SessionManager) Destroy(req *http.Request) error {
    sessionId, _ := sm.getSessionId(req)
    if len(sessionId) == 0 {
        return errors.New("Length of sessionId is 0")
    }
    return sm.provider.destroy(sessionId)
}
 
//垃圾回收:删除过期session
func (sm *SessionManager) Gc() error {
    err := sm.provider.gc(sm.sessionExpire)
    duration, _ := time.ParseDuration(strconv.Itoa(sm.gcDuration) + "m")
    time.AfterFunc(duration, func() { sm.Gc() }) //设置下次运行时间
    return err
}

至此,我们已经实现session管理器!

接下来,不管使用什么方式存储session信息,只要实现SessionProvider接口,关心session数据的读写操作即可。
这里,我们实现一个文件session存储器,除了session文件保存路径,为了并发安全,每个session文件还需要对应一个读写锁,所以其结构可设计为:

1
2
3
4
5
//文件session存储器
type FileProvider struct {
    savePath string                   //session文件保存路径
    muxMap   map[string]*sync.RWMutex //session文件锁
}

对应的创建方法:

1
2
3
4
5
6
7
//创建文件session存储器对象
func NewFileProvider(savePath string) *FileProvider {
    return &FileProvider{
        savePath: savePath,
        muxMap:   make(map[string]*sync.RWMutex),
    }
}

所有方法都需要根据session ID得到文件路径,可定义共用方法:

1
2
3
4
//返回session文件名称
func (fp FileProvider) filename(sessionId string) string {
    return fp.savePath + "/" + sessionId
}

写入session文件时,数据只能是字符串,而存入session的却不一定是字符串,所以需要一个将其他数据类型转换为字符串的共用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//将数据类型转换为字符串
func (fp FileProvider) toString(value interface{}) (string, error) {
    var str string
    vType := reflect.TypeOf(value)
    switch vType.Name() {
    case "int":
        i, _ := value.(int)
        str = strconv.Itoa(i)
    case "string":
        str, _ = value.(string)
    case "int64":
        i, _ := value.(int64)
        str = strconv.FormatInt(i, 10)
    default:
        return "", errors.New("Unsupported type: " + vType.Name())
    }
    return str, nil
}

文件session存储器的create、get、getAll、set等四个方法,本质上都是对session文件进行读写操作,可以将读和写抽取出来成为两个共用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
//创建/重写session文件
func (fp FileProvider) write(sessionId string, data map[string]string, newFile bool) error {
    _, exist := fp.muxMap[sessionId]
    if !exist { //内存中没有锁,先建锁
        fp.muxMap[sessionId] = new(sync.RWMutex)
    }
    fp.muxMap[sessionId].Lock()
    defer func() {
        fp.muxMap[sessionId].Unlock()
    }()
    fname := fp.filename(sessionId)
    _, err := os.Stat(fname)
    var f *os.File
    if newFile {
        if err == nil { //若session文件存在,则先删除
            os.Remove(fname)
        }
        f, err = os.Create(fname)
        if err != nil {
            return errors.New("Creating session file failed: " + err.Error())
        }
    } else {
        if err != nil { //session文件不存在
            return errors.New("Session file does not exists: " + fname)
        }
        f, err = os.OpenFile(fname, os.O_RDWR|os.O_TRUNC, 0644)
        if err != nil {
            return errors.New("Opening session file failed: " + err.Error())
        }
    }
    defer func() {
        os.Chtimes(fname, time.Now(), time.Now()) //更新文件最后访问时间
        f.Close()
    }()
    for key, value := range data {
        _, err = fmt.Fprintln(f, key+":"+value)
        if err != nil {
            return errors.New("Setting session key value failed: " + err.Error())
        }
    }
    return nil
}
 
//读取session文件
func (fp FileProvider) read(sessionId string) (map[string]string, error) {
    fname := fp.filename(sessionId)
    _, err := os.Stat(fname)
    if err != nil { //session文件不存在
        return nil, errors.New("Session file does not exists: " + fname)
    }
    _, exist := fp.muxMap[sessionId]
    if !exist { //内存中没有锁,先建锁
        fp.muxMap[sessionId] = new(sync.RWMutex)
    }
    fp.muxMap[sessionId].Lock()
    defer func() {
        fp.muxMap[sessionId].Unlock()
    }()
    f, err := os.Open(fname)
    if err != nil {
        return nil, errors.New("Opening session file failed: " + err.Error())
    }
    defer func() {
        os.Chtimes(fname, time.Now(), time.Now()) //更新文件最后访问时间
        f.Close()
    }()
    data := make(map[string]string)
    scaner := bufio.NewScanner(f)
    for scaner.Scan() {
        kv := strings.Split(scaner.Text(), ":")
        if len(kv) != 2 {
            continue
        }
        data[kv[0]] = kv[1]
    }
    if len(data) == 0 {
        return nil, errors.New("No data in session file")
    }
    return data, nil
}

 最后,实现SessionProvider接口的6个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
//创建session
func (fp FileProvider) create(sessionId string, data map[string]interface{}) error {
    strData := make(map[string]string)
    for key, value := range data {
        strValue, err := fp.toString(value)
        if err != nil {
            return err
        }
        strData[key] = strValue
    }
    return fp.write(sessionId, strData, true)
}
 
//读取session键值
func (fp FileProvider) get(sessionId, key string) (string, error) {
    data, err := fp.read(sessionId)
    if err != nil {
        return "", err
    }
    value, ok := data[key]
    if !ok {
        return "", errors.New("Session key does not exists: " + key)
    }
    return value, nil
}
 
//读取session所有键值对
func (fp FileProvider) getAll(sessionId string) (map[string]string, error) {
    return fp.read(sessionId)
}
 
//设置session键值
func (fp FileProvider) set(sessionId, key string, value interface{}) error {
    data, err := fp.read(sessionId)
    if data == nil {
        return err
    }
    str, err := fp.toString(value)
    if err != nil {
        return err
    }
    data[key] = str
    return fp.write(sessionId, data, false)
}
 
//销毁session:删除session文件
func (fp FileProvider) destroy(sessionId string) error {
    fname := fp.filename(sessionId)
    _, err := os.Stat(fname)
    if err != nil { //session文件不存在
        return errors.New("Session file does not exists: " + fname)
    }
    _, exist := fp.muxMap[sessionId]
    if !exist { //内存中没有锁,先建锁
        fp.muxMap[sessionId] = new(sync.RWMutex)
    }
    fp.muxMap[sessionId].Lock()
    err = os.Remove(fname)
    fp.muxMap[sessionId].Unlock()
    if err != nil {
        return errors.New("Removing session file failed: " + err.Error())
    }
    delete(fp.muxMap, sessionId)
    return nil
}
 
//垃圾回收:删除过期session文件
func (fp FileProvider) gc(expire int64) error {
    now := time.Now().Unix()
    for sessionId, mux := range fp.muxMap {
        fname := fp.filename(sessionId)
        if len(fname) == 0 {
            continue
        }
        mux.Lock()
        info, err := os.Stat(fname)
        if err != nil {
            mux.Unlock()
            continue
        }
        modTime := info.ModTime().Unix() //文件最后访问时间
        if modTime+expire*60 < now {     //已超出过期时间
            err = os.Remove(fname)
            mux.Unlock()
            if err != nil {
                delete(fp.muxMap, sessionId)
            }
        } else {
            mux.Unlock()
        }
    }
    return nil
}

 这样就完成了文件session存储器的实现。

当然我们也可以使用内存或数据库等其他方式进行session数据的存储,只需实现SessionProvider接口,并将其实例化对象赋值给session管理器创建方法的provider参数,即可实现不同存储方式的快速切换。

posted @   疯一样的狼人  阅读(587)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示