session支持算是WEB框架的标配了,本篇先对session的原理进行简单说明,然后讲述框架是如何实现session支持,最后讲述自定义session的支持及实现方式。
session的基本原理
http协议是无状态的,所以,客户端都会使用cookie来标识请求来自同一个客户端。 而因为cookie是保存在客户端的,每次请求都要带上,所以不能放太多的数据,而且安全性容易受到威协。
相对地,session将用户会话数据保存在服务端,避免了敏感数据的传输,减少了传输的带宽要求,也提升了安全性。
会话数据保存在服务端,那服务器怎么与客户端一一对应呢?那就是sessionId,一般地,sessionId还是会通过cookie传输(也有人用自定义的header或者url方式), 服务端收到sessionId后,再去相应保存session的地方将会话数据读出来使用。在客户端还没有sessionId时,是由服务端生成一个唯一的id,并将用户会话数据以该id为主键保存,最后将sessionId响应给客户端。
session的操作方法及实现
先来看看操作session用到哪些方法:
-
调用this.SessionStart() 开启session,也可以在配置中增加session.auto_start=on 来让框架自动开启
-
使用this.Session["key"]的方式读取session值 (框架在SessionStart时会将所有的session值读入this.Session)
-
使用this.SessionSet("key",value) 设置一个session
-
使用this.SessionUnSet("key") 注销session的一个key
-
使用this.SessionDestroy() 注销session全部内容
-
在请求结束时,框架会自动调用this.sessionSave() 将更新数据保存回会话数据中,以便下次使用
接下来就是这些方法的实现:
func (this *App) SessionStart() {
if this.sessionOn { //已启动过,返回
return
}
sid := getSessionId() //从cookie中读取sessionID,如何没有, 则生成一个唯一的id,比如md5当前时间的纳秒数
this.sessHandler.Open(sid, this.Conf) //调用handler的open方法初始化 ,this.Conf是配置访问对象,用来读取必要的配置
this.Session = this.sessHandler.Read() //调用Read方法,将session读取全局的this.Session,以方便使用
//将sid放到cookie返回
cookie := &http.Cookie{Name: "sessionId", Value: sid, HttpOnly: true}
// 如有需要,设置cookie的生命周期
this.SetCookie(cookie)
this.sessionOn = true
//gc
go func() {
//按一定的机率调用handler的Gc方法,请理过期数据
}()
}
//设置一个session
func (this *App) SessionSet(key string, val interface{}) {
if !this.sessionOn {
return
}
this.sessHandler.Set(key, val)
this.Session[key] = val
}
//销毁一个或多个session,如果不传参数,则销毁全部
func (this *App) SessionUnset(keys ...interface{}) {
if !this.sessionOn {
return
}
if keys == nil { //key为空,删除全部
this.sessHandler.Destroy()
for k, _ := range this.Session {
delete(this.Session, k)
}
} else {
for _, k := range keys {
if key, ok := k.(string); ok {
this.sessHandler.Set(key, nil)
delete(this.Session, key)
}
}
}
}
//保存SESSION(请求结束时自动调用)
func (this *App) sessionSave() {
if !this.sessionOn {
return
}
this.sessHandler.Save()
}
SessionHandler接口
由上面的session操作可以看出,session操作只是实现了sid的获取或生成,然后就是在需要的时候调用SessionHandler的相应方法来完成实际的处理。
为了支持自定义的SessionHandler,把SessionHandler定义成一个接口,所有实现了该接口的对象都可以作为session处理器
框架中,使用 SetSessionHandler(sh *SessionHandler){} 方法来指定所使用的handler (不使用该方法设置handler时,使用配置文件中指定的内置handler)
- SessionHandler接口定义
type SessionHandler interface {
Open(sessId string, config *Conf) //开启session时调用
Set(key string, val interface{}) //写入session,给Session赋值时调用,val设为nil时,表示删除
Read() map[string]interface{} //读取session,返回反序列后的格式数据,用来设置到Session的map
Destroy() //销毁一个session(SessionDestory时调用)
Save() //将session的值序列化后持久化保存(请求完成时)
Gc(maxLife int64) //过期数据清理,系统按特定机率触发
}
SessionHandler的实现
框架内置了两种常用的SessionHandler,分别是基于文件和基于memcache的,下面简单介绍一下基于文件的实现思路,基于memcache或者其它方式类似,有兴趣的可以自行试验。
type fileSession struct {
file string
path string
change bool
data map[string]interface{}
}
func (this *fileSession) Open(sessId string, config *Conf) {
this.path, _ = os.TempDir()+"/sess"
this.file = fmt.Sprintf("%s/%s/%s/%s", this.path, sessId[:2], sessId[2:4], sessId[4:]) //session文件, hash两层路径
}
func (this *fileSession) Set(key string, val interface{}) {
this.change = true
if val == nil {
delete(this.data, key)
} else {
this.data[key] = val
}
}
func (this *fileSession) Read() map[string]interface{} {
this.data = make(map[string]interface{})
_, err := os.Stat(this.file)
if err == nil { //存在,读取
os.Chtimes(this.file, time.Now(), time.Now()) //设置一下最后更新时间
fi, _ := os.Open(this.file)
defer fi.Close()
content, _ := ioutil.ReadAll(fi)
if err := json.Unmarshal(content, &this.data); err != nil {
//check err
}
}
return this.data
}
func (this *fileSession) Destroy() {
os.Remove(this.file)
for k, _ := range this.data {
delete(this.data, k)
}
}
func (this *fileSession) Save() {
if this.change {
data, _ := json.Marshal(this.data) //将数据序列化后保存
//若目录不存在先创建
path := filepath.Dir(this.file)
_, err := os.Stat(path)
if err != nil && os.IsNotExist(err) {
os.MkdirAll(path, os.ModePerm)
}
//写入文件
fd, _ := os.OpenFile(this.file, os.O_TRUNC|os.O_CREATE, os.ModePerm)
fd.Write(data)
}
}
func (this *fileSession) Gc(maxLife int64) {
//遍历this.path,获取每一文件的更新时间,如果距离现在超过maxLife,则删除
}