Files
build-web-application-with-…/6.2.md
xiemengjun c612a95519 完成了第二小节session实现部分
接下来讲解session劫持
2012-09-23 23:42:16 +08:00

223 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#6.2 Go如何使用session
通过上一小节的介绍我们知道session是服务器端的一种实现用户和服务器之间认证的解决方案那么session到底怎么样实现整个流程的呢Go目前内部没有任何pkg支持session我们这小节将会使用Go来实现session管理和创建
##session创建过程
session的基本原理是服务端为每一个session维护一份会话信息数据而客户端和服务端依靠一个全局唯一的标识来访问会话信息数据。用户访问web应用时服务端程序决定何时创建session创建session可以概括为三个步骤
- 生成全局唯一标识符sessionid
- 开辟数据存储空间。一般会在内存中创建相应的数据结构但这种情况下系统一旦掉电所有的会话数据就会丢失如果是电子商务网站这种事故会造成严重的后果。不过也可以写到文件里甚至存储在数据库中这样虽然会增加I/O开销但session可以实现某种程度的持久化而且更有利于session的共享
- 将session的全局唯一标示符发送给客户端。
问题的关键就在服务端如何发送这个session的唯一标识上。考虑到HTTP协议的定义数据无非可以放到请求行、头域或Body里所以一般来说会有两种常用的方式cookie和URL重写。
1. Cookie
服务端只要设置Set-cookie头就可以将session的标识符传送到客户端而客户端此后的每一次请求都会带上这个标识符由于cookie可以设置失效时间所以一般包含session信息的cookie会设置失效时间为0(会话cookie)即浏览器进程有效时间。至于浏览器怎么处理这个0每个浏览器都有自己的方案但差别都不会太大(一般体现在新建浏览器窗口的时候)
2. URL重写
所谓URL重写顾名思义就是重写URL。试想在返回用户请求的页面之前将页面内所有的URL后面全部以get参数的方式加上session标识符或者加在path info部分等等这样用户在收到响应之后无论点击哪个链接或提交表单都会在再带上session的标识符从而就实现了会话的保持。读者可能会觉得这种做法比较麻烦确实是这样但是如果客户端禁用了cookie的话URL重写将会是首选。
##Go实现session管理
通过上面session创建过程的讲解读者应该对session有了一个大体的认识但是具体到动态页面技术里面又是怎么实现session的呢下面我们将结合session的生命周期lifecycle用Go语言来实现session管理。
###session管理设计
我们知道session管理需要有如下几个元素
- 全局session管理器
- sessionid 全局唯一性
- 每个客户生成一个session
- session 的存储,可以存储到内存、文件、数据库等
- session 过期处理
接下来让我们一一来讲解一下Go实现session管理的整个设计思路以及相应的代码示例
###Session管理器
我们就来定义一个全局的session管理器
type SessionManager struct {
cookieName string //private cookiename
lock sync.Mutex // protects session
provide Provide
maxlifetime int64
}
func NewSessionManager(provideName, cookieName string, maxlifetime int64) (*SessionManager, error) {
provide, ok := provides[provideName]
if !ok {
return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName)
}
return &SessionManager{provide: provide, cookieName: cookieName, maxlifetime: maxlifetime}, nil
}
Go实现整个的流程应该也是这样的在main包中创建一个全部的session管理器
var globalSessions *SessionManager
//然后在init函数中初始化
func init() {
globalSessions = NewSessionManager("memory","gosessionid",3600)
}
我们知道session是保存在服务器段的数据那么他可以以任何的方式存在起来可以存储在内存、数据库或者文件中。那么我们就可以抽象出来Provide接口这个接口实现session管理器底层存储的问题。
type Provide interface {
SessionInit(sid string) (Session, error)
SessionRead(sid string) (Session, error)
SessionDestroy(sid string) bool
SessionGC(maxlifetime int64)
}
- SessionInit函数实现初始化一个Session接口然后返回一个新的Session接口
- SessionRead函数根据sid返回一个已经存在的Session接口如果不存在那么内部调用SessionInit函数返回一个新的Session接口
- SessionDestroy函数用来销毁对应sid的Session接口
- SessionGC根据maxlifetime来删除过期的数据
那么Session接口需要实现怎么样的功能呢有过PHP等Web开发经验的用户来说Session操作一般也就几个功能设置值、读取值、删除值那么Session接口也就这三个功能
type Session interface {
Set(key interface{}, value interface{}) //set session value
Get(key interface{}) interface{} //get session value
Del(key interface{}) bool //delete session value
}
>以上设计思路来源于database/sql/driver定义好接口然后session的存储实现相应的接口这样就可以使用了因此NewSessionManager也实现了如下功能函数Register,用来注册不同的session存储实现。
var provides = make(map[string]Provide)
// Register makes a session provide available by the provided name.
// If Register is called twice with the same name or if driver is nil,
// it panics.
func Register(name string, provide Provide) {
if driver == nil {
panic("session: Register provide is nil")
}
if _, dup := provides[name]; dup {
panic("session: Register called twice for provide " + name)
}
provides[name] = provide
}
###全局唯一的Session ID
我们知道Session ID是用来识别每个访问Web应用的用户因此必须保证sessionID都能有一个唯一的值下面代码展示了如何实现这个唯一值
func (this *SessionManager) sessionId() string {
b := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return ""
}
return string(b)
}
###session创建
每个来访问的用户我们需要为他分配Session然后根据Session里面的数据判断用户是否已经进行了相应的操作那么我们如何来创建Session呢SessionStart这个函数就是用来检测用户是否已经创建了Session如果没有就创建之。
func (this *SessionManager) SessionStart(w ResponseWriter, r *http.Request) (session Session) {
this.lock.Lock()
defer this.lock.Unlock()
cookie, err := r.Cookie(this.cookieName)
if err != nil || cookie.Value == "" {
sid := this.sessionId()
session = this.provide.SessionInit(sid)
expiration := time.Now()
expiration.Add(time.Duration(this.maxlifetime))
cookie := http.Cookie{Name: this.cookieName, Value: sid, Expires: expiration}
http.SetCookie(w, &cookie)
} else {
session = this.provide.SessionRead(cookie.Value)
}
return
}
我们根据前面用户登陆的操作我们来看看如何应用:
func login(w http.ResponseWriter, r *http.Request) {
session:=globalSessions.SessionStart(w,r)
fmt.Println("method:", r.Method) //获取请求的方法
if r.Method == "GET" {
if session.Get("uid").(string) != "" {
t, _ := template.ParseFiles("main.gtpl")
}else{
t, _ := template.ParseFiles("login.gtpl")
}
t.Execute(w, nil)
} else {
//请求的是登陆数据,那么执行登陆的逻辑判断
fmt.Println("username:", r.Form["username"])
fmt.Println("password:", r.Form["password"])
}
}
###操作值:设置、读取和删除
SessionStart函数返回的是一个实现了设置、读取、删除接口的Session接口那么我们如何来对数据进行操作呢
上面的例子已经大概的展示了读取数据的操作`session.Get("uid")`
那么我们再来看看详细的操作
func login(w http.ResponseWriter, r *http.Request) {
session:=globalSessions.SessionStart(w,r)
fmt.Println("method:", r.Method) //获取请求的方法
if r.Method == "GET" {
if session.Get("uid").(string) != "" {
session.Del("password")
t, _ := template.ParseFiles("main.gtpl")
}else{
t, _ := template.ParseFiles("login.gtpl")
}
t.Execute(w, nil)
} else {
//请求的是登陆数据,那么执行登陆的逻辑判断
fmt.Println("username:", r.Form["username"])
fmt.Println("password:", r.Form["password"])
session.Set("username",r.Form["username"])
session.Set("password",r.Form["password"])
}
}
通过上面的例子我们可以看到这个Session的操作和操作key/value数据库类似Set、Get、Del操作
我们知道Session里面有GC功能那么只要当我们进行了相应的上面的任意一个操作我们都需要去修改相应的Session实体的最后访问时间这样当调用GC的时候就不会误删除还在使用的Session实体。
###session重置
我们知道Web应用中有用户退出这个操作那么当用户退出应用的时候我们是否需要进行session数据的重置下面这个函数就是实现了这个功能
//Destroy sessionid
func (this *SessionManager) SessionDestroy(w ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(this.cookieName)
if err != nil || cookie.Value == "" {
return
} else {
this.lock.Lock()
defer this.lock.Unlock()
this.provide.SessionDestroy(cookie.Value)
expiration := time.Now()
cookie := http.Cookie{Name: this.cookieName, Expires: expiration}
http.SetCookie(w, &cookie)
}
}
###session销毁
我们来看一下Session管理器如何来管理销毁,只要我们在Main启动的时候启动
func init() {
globalSessions.GC()
}
func (this *SessionManager) gc() {
this.lock.Lock()
defer this.lock.Unlock()
this.provide.GC(this.maxlifetime)
time.AfterFunc(this.maxlifetime, func() { this.GC() })
}
我们可以看到GC充分利用了time包中的定时器功能当大于`maxlifetime`之后调用GC函数这样就可以保证`maxlifetime`时间内的session都是可用的这个数字其实也可以用于统计在线用户数之类的。
##总结
通过上面的介绍我们实现了一个Session管理器定义了Provide接口Provide接口用来提供Session存储实现。Session管理器用来在Web应用中实现全局的Session管理接下来我们会实现内存的一个实现方式供大家参考学习。
## links
* [目录](<preface.md>)
* 上一节: [session和cookie](<6.1.md>)
* 下一节: [预防session劫持](<6.3.md>)
## LastModified
* $Id$