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参数,即可实现不同存储方式的快速切换。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 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)