go Session的实现

简介: 众所周知,http协议是无状态的,这对于服务器确认是哪一个客户端在发请求是不可能的,因此为了能确认到,通常方法是让客户端发送请求时带上身份信息。容易想到的方法就是客户端在提交信息时,带上自己的账户和密码。但是这样存在着严重的安全问题,可以改进的方法就是,服务器给一个确定的客户端返回一个唯一 id,客户端将这个 id 保存在本地,每次发送请求时只需要携带着这个 id,就可以做到较好的验证。

〇、前言

众所周知,http协议是无状态的,这对于服务器确认是哪一个客户端在发请求是不可能的,因此为了能确认到,通常方法是让客户端发送请求时带上身份信息。容易想到的方法就是客户端在提交信息时,带上自己的账户和密码。但是这样存在着严重的安全问题,可以改进的方法就是,服务器给一个确定的客户端返回一个唯一 id,客户端将这个 id 保存在本地,每次发送请求时只需要携带着这个 id,就可以做到较好的验证(当然也存在着安全问题,这个后面再说)。

这个方法就是 现今很成熟的 session、cookie 技术。session和cookie的目的相同,都是为了克服http协议无状态的缺陷,但完成的方法不同。session通过cookie,在客户端保存session id,而将用户的其他会话消息保存在服务端的session对象中。与此相对的,cookie需要将所有信息都保存在客户端。因此cookie存在着一定的安全隐患,例如本地cookie中保存的用户名密码被破译,或cookie被其他网站收集。

本文将尝试着实现一个成熟的 go session,从而实现会话保持。

一、架构设计

1、管理器

type Manager struct {
   
    cookieName  string
    lock        sync.Mutex
    provider    Provider
    maxLifeTime int64
}

其中 Provider 是一个接口:

// Provider 接口
type Provider interface {
   
    SessionInit(sid string) (Session, error) // SessionInit函数实现Session的初始化,操作成功则返回此新的Session变量
    SessionRead(sid string) (Session, error) // SessionRead函数返回sid所代表的Session变量.如果不存在,那么将以sid为参数调用SessionInit函数创建并返回一个新的Session变量
    SessionDestroy(sid string) error         // SessionDestroy函数用来销毁sid对应的Session变量
    SessionGC(maxLifeTime int64)             // SessionGC根据maxLifeTime来删除过期的数据
}

这里又定义了一个Provider 结构体,它实现了 Provider 接口:

// Provider 实现接口 Provider

func (pder *Provider) SessionInit(sid string) (session.Session, error) {
   
    // 根据 sid 创建一个 SessionStore
    pder.lock.Lock()
    defer pder.lock.Unlock()
    v := make(map[interface{
   }]interface{
   })
    // 同时更新两个字段
    newsess := &SessionStore{
   sid: sid, timeAccessed: time.Now(), value: v}
    // list 用于GC
    element := pder.list.PushBack(newsess)
    // 存放 kv
    pder.sessions[sid] = element
    return newsess, nil
}

func (pder *Provider) SessionRead(sid string) (session.Session, error) {
   
    if element, ok := pder.sessions[sid]; ok {
   
        return element.Value.(*SessionStore), nil
    } else {
   
        sess, err := pder.SessionInit(sid)
        return sess, err
    }
}

// 服务端 session 销毁

func (pder *Provider) SessionDestroy(sid string) error {
   
    if element, ok := pder.sessions[sid]; ok {
   
        delete(pder.sessions, sid)
        pder.list.Remove(element)
        return nil
    }
    return nil
}

// 回收过期的 cookie

func (pder *Provider) SessionGC(maxlifetime int64) {
   
    pder.lock.Lock()
    defer pder.lock.Unlock()

    for {
   
        element := pder.list.Back()
        if element == nil {
   
            break
        }
        if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() {
   
            // 更新两者的值

            // 垃圾回收
            pder.list.Remove(element)
            // 删除 map 中的kv
            delete(pder.sessions, element.Value.(*SessionStore).sid)
        } else {
   
            break
        }
    }
}

func (pder *Provider) SessionUpdate(sid string) error {
   
    pder.lock.Lock()
    defer pder.lock.Unlock()
    if element, ok := pder.sessions[sid]; ok {
   
        // 这里更新也就更新了个时间,这意味着 session 的生命得到了延长
        element.Value.(*SessionStore).timeAccessed = time.Now()
        pder.list.MoveToFront(element)
        return nil
    }
    return nil
}

管理器 Manager 实现的方法:

// 创建 Session

func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) {
   
    manager.lock.Lock()
    defer manager.lock.Unlock()
    cookie, err := r.Cookie(manager.cookieName)
    if err != nil || cookie.Value == "" {
   
        // 查看是否为当前客户端注册过名为 gosessionid 的 cookie,如果没有注册过,就为客户端创建一个该 cookie

        // 创建 sessionID
        sid := manager.sessionID()
        // 创建一个 session 接口,这其实是一个 创建完成的 SessionStore ,SessionStore 实现了该接口
        session, _ = manager.provider.SessionInit(sid)
        // 创建 cookie
        cookie := http.Cookie{
   Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(manager.maxLifeTime)}
        http.SetCookie(w, &cookie)
    } else {
   
        sid, _ := url.QueryUnescape(cookie.Value)
        session, _ = manager.provider.SessionRead(sid)
    }
    return
}

// Session 重置

func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request) {
   
    cookie, err := r.Cookie(manager.cookieName)
    if err != nil || cookie.Value == "" {
   
        return
    } else {
   
        manager.lock.Lock()
        defer manager.lock.Unlock()
        err := manager.provider.SessionDestroy(cookie.Value)
        if err != nil {
   
            return
        }
        expiration := time.Now()
        cookie := http.Cookie{
   Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1}
        http.SetCookie(w, &cookie)
    }
}

// Session 回收

func (manager *Manager) GC() {
   
    manager.lock.Lock()
    defer manager.lock.Unlock()
    manager.provider.SessionGC(manager.maxLifeTime)
    // 每 20秒触发一次
    time.AfterFunc(time.Second*20, func() {
    manager.GC() })
}

2、sessions存放

在 Provider 结构体中:

sessions map[string]*list.Element // 存放 sessionStores
list     *list.List               // 用来做gc

sessions 中存放不同客户端的 session,而 list 中也会同时刷新,它用来回收过期的 session。
每一个session用 SessionStore 结构体来存储。

Session 接口:

// Session 接口
type Session interface {
   
    Set(key, value interface{
   }) error // 设置 session 的值
    Get(key interface{
   }) interface{
   }  // 获取 session 的值
    Delete(key interface{
   }) error     // 删除 session 的值
    SessionID() string                // 返回当前 session 的 ID
}

这个接口,由 SessionStore 实现:

// SessionStore 结构体

type SessionStore struct {
   
    sid          string                      // session id唯一标识
    timeAccessed time.Time                   // 最后访问时间
    value        map[interface{
   }]interface{
   } // 值
}
// SessionStore 实现 Session 接口

func (st *SessionStore) Set(key, value interface{
   }) error {
   
    st.value[key] = value
    err := pder.SessionUpdate(st.sid)
    if err != nil {
   
        return err
    }
    return nil
}

func (st *SessionStore) Get(key interface{
   }) interface{
   } {
   
    err := pder.SessionUpdate(st.sid)
    if err != nil {
   
        return nil
    }
    if v, ok := st.value[key]; ok {
   
        return v
    } else {
   
        return nil
    }
}

func (st *SessionStore) Delete(key interface{
   }) error {
   
    delete(st.value, key)
    err := pder.SessionUpdate(st.sid)
    if err != nil {
   
        return err
    }
    return nil
}

func (st *SessionStore) SessionID() string {
   
    return st.sid
}

二、实现细节

1、provider 注册表

// provider 注册表
var provides = make(map[string]Provider)

任何一个 Maneger 在创建之前,都需要在 provider 注册表中注册。因此在创建一个全局注册表pder,并注册,这应该是 init 的:

// 创建全局 pder
var pder = &Provider{
   list: list.New()}
func init() {
   
    pder.sessions = make(map[string]*list.Element)
    session.Register("memory", pder)
}

注册器:

func Register(name string, provider Provider) {
   
    if provider == nil {
   
        panic("session: Register provide is nil")
    }
    if _, dup := provides[name]; dup {
   
        panic("session: Register called twice for provide " + name)
    }
    provides[name] = provider
}

2、全局管理器

var globalSessions *session.Manager
func init() {
   
    globalSessions, _ = session.NewManager("memory", "gosessionid", 3600)
    go globalSessions.GC()
}

这个管理器就是一个 cookie 管理器,它只对cookie名字为gosessionid的 cookie 负责。

func NewManager(provideName, cookieName string, maxlifetime int64) (*Manager, error) {
   
    provider, ok := provides[provideName]
    if !ok {
   
        return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName)
    }
    return &Manager{
   provider: provider, cookieName: cookieName, maxLifeTime: maxlifetime}, nil
}

3、案例演示

现在已经初始化好了,就等着客户端访问了。
现在我们写一个很简单的计数器,前端访问的时候,自动+1:

func count(c *gin.Context) {
   
    sess := globalSessions.SessionStart(c.Writer, c.Request)
    ct := sess.Get("countnum")
    if ct == nil {
   
        err := sess.Set("countnum", 1)
        if err != nil {
   
            return
        }
    } else {
   
        // 更新
        err := sess.Set("countnum", ct.(int)+1)
        if err != nil {
   
            return
        }
    }
    t, err := template.ParseFiles("template/count.html")
    if err != nil {
   
        fmt.Println(err)
    }
    c.Writer.Header().Set("Content-Type", "text/html")
    err = t.Execute(c.Writer, sess.Get("countnum"))
    if err != nil {
   
        return
    }
}

当中的count.html这样写:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Count</title>
</head>

<body>
  <h1>Hi. Now count:{
  {.}}</h1>
</body>

</html>

main.go这样写:

package main

import (
    _ "Go_Web/memory"
    "Go_Web/session"
    "fmt"
    "github.com/gin-gonic/gin"
    "html/template"
    "net/http"
)

// 全局 sessions 管理器
var globalSessions *session.Manager

// init 初始化

func init() {
   
    globalSessions, _ = session.NewManager("memory", "gosessionid",20)
    go globalSessions.GC()
}

func count(c *gin.Context) {
   
    sess := globalSessions.SessionStart(c.Writer, c.Request)
    ct := sess.Get("countnum")
    if ct == nil {
   
        err := sess.Set("countnum", 1)
        if err != nil {
   
            return
        }
    } else {
   
        // 更新
        err := sess.Set("countnum", ct.(int)+1)
        if err != nil {
   
            return
        }
    }
    t, err := template.ParseFiles("template/count.html")
    if err != nil {
   
        fmt.Println(err)
    }
    c.Writer.Header().Set("Content-Type", "text/html")
    err = t.Execute(c.Writer, sess.Get("countnum"))
    if err != nil {
   
        return
    }
}

func main() {
   
    r := gin.Default()
    r.GET("/count", count)
    err := r.Run(":9000")
    if err != nil {
   
        return
    }

}

我们把 session 的过期时间设为 20 秒,这样可以 更快的看到过期效果。
现在把服务器启动,来看看整个过程。
编译运行之后,在浏览器访问 count。
可以继续点击,这个只要在 20 秒之内点击,cookie 就不回过期,因为每次发送请求都会更新 sessionStore:

err := sess.Set("countnum", ct.(int)+1)
// SessionStore 实现 Session 接口

func (st *SessionStore) Set(key, value interface{
   }) error {
   
    st.value[key] = value
    err := pder.SessionUpdate(st.sid)
    if err != nil {
   
        return err
    }
    return nil
}
func (pder *Provider) SessionUpdate(sid string) error {
   
    pder.lock.Lock()
    defer pder.lock.Unlock()
    if element, ok := pder.sessions[sid]; ok {
   
        // 这里更新也就更新了个时间,这意味着 session 的生命得到了延长
        element.Value.(*SessionStore).timeAccessed = time.Now()
        pder.list.MoveToFront(element)
        return nil
    }
    return nil
}

可以看到 sessionId 并没有变,这是因为就算本地 cookie过期,当发送请求时,服务器依然会拿到这个 cookie。
session 过期的时候,服务器会执行:

// 回收过期的 cookie

func (pder *Provider) SessionGC(maxlifetime int64) {
   
    pder.lock.Lock()
    defer pder.lock.Unlock()

    for {
   
        element := pder.list.Back()
        if element == nil {
   
            break
        }
        if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() {
   
            // 更新两者的值

            // 垃圾回收
            pder.list.Remove(element)
            // 删除 map 中的kv
            delete(pder.sessions, element.Value.(*SessionStore).sid)
        } else {
   
            break
        }
    }
}

这意味着,pder 中的list 和 sessions 中都不存在 键为countnumsessionStore。但是依然会执行:

    sid, _ := url.QueryUnescape(cookie.Value)
    session, _ = manager.provider.SessionRead(sid)

SessionRead():

func (pder *Provider) SessionRead(sid string) (session.Session, error) {
   
    if element, ok := pder.sessions[sid]; ok {
   
        return element.Value.(*SessionStore), nil
    } else {
   
        sess, err := pder.SessionInit(sid)
        return sess, err
    }
}

执行SessionRead()的时候,由于 session 已经被删除,只能执行pder.SessionInit(sid)了,因此,服务器会创建一个和原来一样的 sessionId。之后count()自然就会执行err := sess.Set("countnum", 1)

ct := sess.Get("countnum")
    if ct == nil {
   
        err := sess.Set("countnum", 1)
        if err != nil {
   
            return
        }
    } else {
   
        // 更新
        err := sess.Set("countnum", ct.(int)+1)
        if err != nil {
   
            return
        }
    }

全文完,感谢阅读。

目录
相关文章
|
存储 Go
Golang底层原理剖析之map
Golang底层原理剖析之map
444 1
|
11月前
|
人工智能 缓存 Cloud Native
DeepSeek-R1 来了,从 OpenAI 平滑迁移到 DeepSeek的方法
Higress 作为一款开源的 AI 网关工具,可以提供基于灰度+观测的平滑迁移方案。
2131 236
|
4月前
|
存储 前端开发 Java
如何开发合同管理系统中的合同台账板块?(附架构图+流程图+代码参考)
在企业运营中,合同管理至关重要。传统手工管理方式效率低下且易出错,开发合同管理系统成为提升效率、降低风险的关键。系统核心模块——合同台账板块,包含合同台账与合同跟进两大功能。合同台账用于存储合同基本信息,如合同编号、签署日期等;合同跟进则跟踪履约节点、风险预警及变更记录,确保合同高效执行。通过模块整合,企业可全面管理合同生命周期,提高合规性与执行力。开发推荐使用Vue.js或React作为前端,Java Spring Boot或Django作为后端,结合MySQL或PostgreSQL数据库,实现系统的高效与稳定。
|
6月前
|
Go
理解 Go 语言中的 select 用法
本文深入解析了Go语言中`select`的用法,它类似于`switch case`,但仅用于通道(channel)的操作。文章通过多个示例说明了`select`的基本用法、避免死锁的方法、随机性特点以及如何实现超时机制。同时总结了`select`与`switch`的区别:`select`专用于通道操作,case执行是随机的,需注意死锁问题,且不支持`fallthrough`和函数表达式。
276 1
理解 Go 语言中的 select 用法
|
9月前
|
存储 Go
Go中make和new的区别
在 Go 语言中,`make` 和 `new` 都用于分配内存,但功能不同。`make` 用于初始化切片、映射和通道,并返回初始化后的对象;`new` 分配内存并返回指向零值的指针,适用于任何类型。`make` 返回的是数据结构本身,而 `new` 返回指针。`make` 完整初始化特定数据结构,`new` 只初始化为零值。
364 0
|
存储 监控 NoSQL
【MongoDB 专栏】MongoDB 分片策略与最佳实践
【5月更文挑战第10天】MongoDB 分片是应对大数据量的扩展策略,涉及哈希和范围分片两种策略。分片架构包含分片服务器、配置服务器和路由服务器。最佳实践包括选择合适分片键、监控调整、避免热点数据等。注意数据分布不均和跨分片查询的挑战。通过实例展示了如何在电商场景中应用分片。文章旨在帮助理解并优化 MongoDB 分片使用。
613 3
【MongoDB 专栏】MongoDB 分片策略与最佳实践
|
安全 机器人 开发者
Sora的五大应用场景
【2月更文挑战第16天】Sora的五大应用场景
1292 2
Sora的五大应用场景
|
SQL 存储 关系型数据库
Presto【实践 01】Presto查询性能优化(数据存储+SQL优化+无缝替换Hive表+注意事项)及9个实践问题分享
Presto【实践 01】Presto查询性能优化(数据存储+SQL优化+无缝替换Hive表+注意事项)及9个实践问题分享
1790 0
Go语言实现try-catch
在许多编程语言中,try-catch是一种常见的错误处理机制,可以捕获和处理异常。然而,Go语言本身并没有提供类似的try-catch语法。本文将介绍如何在Go语言中实现类似的try-catch机制,以便更好地处理异常情况。
1011 0