原生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. bzoj3123 [Sdoi2013]森林 树上主席树+启发式合并

    题目传送门 https://lydsy.com/JudgeOnline/problem.php?id=3123 题解 如果是静态的查询操作,那么就是直接树上主席树的板子. 但是我们现在有了一个连接两棵 ...

  2. windows命令整理

    本文只是作为知识整理,尽可能的收集一些常用的内网指令.本人原伸手党一枚,希望这些内容对新人有用,大牛可自行忽略. 0x00 内网信息收集 一.单机基础信息收集 如果是获得第一台初始主机的权限的话,我们 ...

  3. __getattr__属性查找

    from datetime import date """ __getattr__ : 在查找不到对象的属性时调用 __getattribute__ : 在查找属性之前调 ...

  4. java扫描仪上传文件

    问题: 项目中有一个功能,原来是用ckfinder做的,可以选择本地图片上传至服务器,然后将服务器的图片显示在浏览器中,并可以将图片地址保存到数据库:现在客户觉得麻烦,提出连接扫描仪扫描后直接上传至服 ...

  5. Android逆向之旅---解析编译之后的Dex文件格式

    一.前言 新的一年又开始了,大家是否还记得去年年末的时候,我们还有一件事没有做,那就是解析Android中编译之后的classes.dex文件格式,我们在去年的时候已经介绍了: 如何解析编译之后的xm ...

  6. Android多线程方案

    为主线程减轻负的多线程方案有哪些呢?这些方案分别适合在什么场景下使用? Android系统为我们提供了若干组工具类来帮助解决这个问题. AsyncTask: 为UI线程与工作线程之间进行快速的切换提供 ...

  7. Python分布式爬虫打造搜索引擎完整版-基于Scrapy、Redis、elasticsearch和django打造一个完整的搜索引擎网站

    Python分布式爬虫打造搜索引擎 基于Scrapy.Redis.elasticsearch和django打造一个完整的搜索引擎网站 https://github.com/mtianyan/Artic ...

  8. 设置Select下拉多选框功能,赋值与绑定问题

    项目需要所以更改select为多选下拉的菜单选项. 我用的是后台直接绑定 在前台aspx页面直接写一个 <div id="dropsxs" runat="serve ...

  9. HTTP 协议解析

    目录 目录 HTTP 协议 HTTP 协议工作原理 HTTP Request 请求行 Request Header HTTP Response 状态行 Response Header Body HTT ...

  10. 网络设备MIB浏览器ifType、ifDescr、ifMtu、ifInOctets等的含义(Zabbix SNMP)

    1.ifType 接口的类型 取值117表示接口为GigabitEthernet (取值62表示接口为 FastEnthernet) 2.ifDescr 接口类型的描述 有GigabitEtherne ...