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

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

其创建方法为:

//创建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数据进行读写操作,这部分根据存储方式不同而不同,但方法签名是一致的,可以定义其接口类型为:

//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的方法,可根据请求头信息生成,这里只是举一个例子:

//生成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:

//读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存储器对应的方法即可:

//创建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文件还需要对应一个读写锁,所以其结构可设计为:

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

对应的创建方法:

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

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

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

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

//将数据类型转换为字符串
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文件进行读写操作,可以将读和写抽取出来成为两个共用方法:

//创建/重写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个方法:

//创建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参数,即可实现不同存储方式的快速切换。

go web编程——session管理机制设计与实现的更多相关文章

  1. Tomcat的Session管理机制

    >>Session和Cookie请求的过程 Http连接本身是无状态的,即前一次发起的连接跟后一次没有任何关系,是属于两次独立的连接请求,但是互联网访问基本上都是需要有状态的,即服务器需要 ...

  2. C++服务器设计(四):超时管理机制设计

    前四章介绍了系统层的设计,从这一章开始进入服务层的设计. 连接断开 在常见的服务器场景中,客户端断开连接的方式为被动关闭.即作为客户端请求完服务器的服务后,选择主动关闭同服务器的连接.在服务器的角度看 ...

  3. 微软与开源干货对比篇_PHP和 ASP.NET在 Session实现和管理机制上差异

    微软与开源干货对比篇_PHP和 ASP.NET在 Session实现和管理机制上差异 前言:由于开发人员要靠工具吃饭,可能和开发工具.语言.环境呆的时间比和老婆孩子亲人在一起的时间还多,所以每个人或多 ...

  4. 细说shiro之六:session管理

    官网:https://shiro.apache.org/ 我们先来看一下shiro中关于Session和Session Manager的类图. 如上图所示,shiro自己定义了一个新的Session接 ...

  5. Spring aop与HibernateTemplate——session管理(每事务一次 Session)

    一.HibernateTemplate与Spring aop简介 参见http://bbs.csdn.net/topics/340207475中网友blueram的发言.(感谢blueram) 二.在 ...

  6. Tomcat中session的管理机制

    1.       请求过程中的session操作: 简述:在请求过程中首先要解析请求中的sessionId信息,然后将sessionId存储到request的参数列表中.然后再从 request获取s ...

  7. web状态管理机制

    引入:b/s(浏览器/服务器模式)区别于winform的是winform中只加载一次页面构造函数,而b/s中只要点击按钮或者其他涉及后台的操作都会调用后台代码.一般情况下为了防止服务器过载,b/s不会 ...

  8. atitit. access token是什么??微信平台公众号开发access_token and Web session保持状态机制

    atitit. access token是什么??微信平台公众号开发access_token and Web session保持状态机制 1. token机制and  session保持状态机制 1 ...

  9. 008-shiro与spring web项目整合【二】认证、授权、session管理

    一.认证 1.添加凭证匹配器 添加凭证匹配器实现md5加密校验. 修改applicationContext-shiro.xml: <!-- realm --> <bean id=&q ...

随机推荐

  1. TCP窗口

    一.窗口移动1.在建立TCP连接时,接收端会告诉发送端自己的接收窗口.2.发送端在发送数据时,会先为数据分包,编号,然后先发送窗口大小的数据(数据大于窗口大小),小于则全部发送了,窗口后的不允许发送. ...

  2. pwd 显示当前所在的工作路径

    1.功能说明 pwd命令是“print working directory ”首字母缩写,显示当前目录的绝对路径. 2.语法格式 pwd [option] pwd 选项 3.命令参数 参数 参数说明 ...

  3. 01.python对象

    标准类型 数字 Integer 整型 Boolean 布尔型 Long integer 长整型 (python2) Floating point real number 浮点型 Complex num ...

  4. crt无法修改背景

    当会话选项 里面的终端类型选择为Linux时,是无法修改外观颜色方案的.可以选择为vt100,就可以修改颜色了

  5. Java面向对象(一) 类和对象

    一.软件开发进化史 摘自<从零开始学架构> 机器语言(1940年) 最早的软件开发使用的是“机器语言”,直接使用二进制码0和1来表示机器可以识别的指令和数据. 汇编语言(20世纪40年代) ...

  6. 【leetcode】1034. Coloring A Border

    题目如下: Given a 2-dimensional grid of integers, each value in the grid represents the color of the gri ...

  7. No module named 'requests'---问题解决记录

    今天在用Pycharm执行脚本时,报错.如下: 问题排查: 1,检查是否安装了requests cmd输入命令:pip install requests 提示有新版本可以升级,那 我就升级了. 然后输 ...

  8. (转)Kubernetes 配置Pod和容器(十七) 使用Secrets管理安全证书

    转:https://www.jianshu.com/p/530b3642c642 本章节展示了如何把密码秘钥等敏感数据安全的注入到Pod里面. 转换安全数据成base-64表示 假设你有两个秘密数据: ...

  9. Java内置多线程框架Executor

    JDK1.5之后,增加了一个Executor让我们能更好的使用多线程. 它位于java.util.concurrent包下 因为是JDK内置类库,我们不需要导入任何第三方jar包. 代码实例: imp ...

  10. Java常用工具——java包装类

    一.包装类和基本数据类型 装箱:基本数据类型——包装类 拆箱:包装类——基本数据类型 package com.imooc.wrap; public class WrapTestOne { public ...