随笔 - 21  文章 - 0  评论 - 0  阅读 - 6131

go会话控制(session)

session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(map)来保存信息。

当程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否包含了一个session标识-称为session id,如果已经包含一个session id则说明以前已经为此客户创建过session,服务器就按照session id把这个session检索出来使用(如果检索不到,可能会新建一个,这种情况可能出现在服务端已经删除了该用户对应的session对象,但用户人为地在请求的URL后面附加上一个JSESSION的参数)。如果客户请求不包含session id,则为此客户创建一个session并且同时生成一个与此session相关联的session id,这个session id将在本次响应中返回给客户端保存。

目前Go标准包没有为session提供任何支持,我们将会自己动手来实现go版本的session管理和创建。

session的创建过程

先想一下,首先我们要自己定义一个结构来存储用户信息,且要有一个对应的id来标识用户对应的这个结构。

这个结构可以先定义为

type SessionStore struct {
	sid          string                      //session id唯一标示
     timeAccessed time.Time           //最后访问的时间 value map[interface{}]interface{} //session里面存储的值 }

既然已经定义了结构体,肯定要有对应的方法或接口来供外部使用,无非就是设置值,读取值,获取值,以及获取id。

type Session interface {
    Set(key, value interface{}) error
    Get(key interface{}) interface{}
    Delete(key interface{}) error
    SessionID() string
}

session部分大致定义好了;服务器要存储许多用户的session,可以直接再内存中存储,但这种情况下,系统一旦掉电,所有的会话数据就会丢失。如果是电子商务类网站,这将造成严重的后果。所以为了解决这类问题,你可以将会话数据写到文件里或存储在数据库中,当然这样会增加I/O开销,但是它可以实现某种程度的session持久化,也更有利于session的共享。这里我们使用内存存储的形式。

服务端如何在内存中存储这些session,可以采用链表的形式(采用go包  "container/list",文章末介绍)。

type MProvider struct {    
  lock sync.Mutex //用来锁 sessions map[string]*list.Element //用来存储在内存 list *list.List //用来做gc,即管理这些session,如检查这些session最后一次访问时间到现在是否
                       //超过我们会话所设置的时间
}

内存存储只是一种存储形式,每种存储方式都有它们自己的实现(MProvider是内存存储),因此我们要提供一种统一的接口供外部使用。接口功能可以抽象为创建session,返回id对应的session,销毁id对应的session,以及删除过期的session。

type Provider interface {
    SessionInit(sid string) (Session, error)
    SessionRead(sid string) (Session, error)
    SessionDestroy(sid string) error
    SessionGC(maxLifeTime int64)
}

通过Provider接口提供的方法可以操作Session,在Cookie那一章提到生产的session通过使用Cookie机制来返回对应的id。回忆一下Cookie的结构体,Cookie.Value就是id,还需要Name。之前使用的Cookie是当时间到时浏览器自动清除,在这里我们不通过Cookie.Expires来设置过期时间,我们自己设置一个时间长度在服务端来检查并清除过期的session。

继续封装Provider,定义一个Manager结构体,包括cookieName,maxLifeTime(session的有效时长)。

type Manager struct {
    cookieName  string     // private cookiename
    lock        sync.Mutex // protects session
    provider    Provider
    maxLifeTime int64
}

分析一下Mangaer,cookieName是Cookie.Name;provider对应着session存储的实现(如内存存储,文件存储,数据库存储等),因此可以定义一个map[string]Provider来存储实现Provider接口的provider,这里我们只实现内存;maxLifeTime对应着session的有效时长,可以定义一个后台线程,里面定义一个定时器,maxLifeTime为周期来检查并清楚session。

复制代码
var provides = make(map[string]Provider)
func Register(name string, provider Provider) {  //注册Provider实现
	if provider == nil {
		panic("session: Register provider is nil")
	}
	if _, dup := provides[name]; dup {
		panic("session: Register called twice for provider " + name)
	}
	provides[name] = provider
}
//然后通过NewManager生成一个全局session管理器
func NewManager(provideName, cookieName string, maxLifeTime int64) (*Manager, error) {
	provider, ok := provides[provideName]
	if !ok {
		return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName)
	}
	return &Manager{provider: provider, cookieName: cookieName, maxLifeTime: maxLifeTime}, nil
}
复制代码

Manager对应的方法:

复制代码
//Session ID是用来识别访问Web应用的每一个用户,因此必须保证它是全局唯一的(GUID),下面代码展示了如何满足这一需求:
func (manager *Manager) sessionId() string { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "" } return base64.URLEncoding.EncodeToString(b) //对生成的id进行编码
}

//例:Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9Hixkk=
//--------------------------------------------------------------------------------------------------
创建session
复制代码
func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) {
    manager.lock.Lock()
    defer manager.lock.Unlock()
    cookie, err := r.Cookie(manager.cookieName)
    if err != nil || cookie.Value == "" {//先检查请求中是否已经存在cookie,若没有就先创建,然后使用cookie
        sid := manager.sessionId()    //发送到客户端
        session, _ = manager.provider.SessionInit(sid)
        cookie := http.Cookie{Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(manager.maxLifeTime)}
        http.SetCookie(w, &cookie)
    } else {
        sid, _ := url.QueryUnescape(cookie.Value) //若存在就通过id提取出请求对应的session返回       
     session, _
= manager.provider.SessionRead(sid) } return }
复制代码

上述流程可概述为:

通过sessionId()生成一个id,这个id是经过base64.URLEncoding.EncodeToString(b)编码的。

然后使用SessionStart()返回一个请求对应的session,若不存在就新建一个session,通过Cookie机制返回给客户端id,

即Cookie.Value: url.QueryEscape(id)    

   func QueryEscape(s string) string;函数对s进行转码使之可以安全的用在URL查询里。
若请求中存在cookie,就提取出来id:url.QueryUnescape(cookie.Value)对应的session
复制代码
复制代码
package main 
import(
    "fmt"
    "encoding/base64"
    "net/url"
    "crypto/rand"
    "io"
    "log"
)

//sessionId函数用来生成一个session ID,即session的唯一标识符
func sessionId() string {
    b := make([]byte, 32)
    //ReadFull从rand.Reader精确地读取len(b)字节数据填充进b
    //rand.Reader是一个全局、共享的密码用强随机数生成器
    if _, err := io.ReadFull(rand.Reader, b); err != nil { 
        return ""
    }
    fmt.Println(b) //[238 246 235 166 48 196 157 143 123 140 241 200 213 113 247 168 219 132 208 163 223 24 72 162 114 30 175 205 176 117 139 118]
    return base64.URLEncoding.EncodeToString(b)//将生成的随机数b编码后返回字符串,该值则作为session ID
}
func main() { 
    sessionId := sessionId() 
    fmt.Println(sessionId) //7vbrpjDEnY97jPHI1XH3qNuE0KPfGEiich6vzbB1i3Y=
    encodedSessionId := url.QueryEscape(sessionId) //对sessionId进行转码使之可以安全的用在URL查询里
    fmt.Println(encodedSessionId) //7vbrpjDEnY97jPHI1XH3qNuE0KPfGEiich6vzbB1i3Y%3D
    decodedSessionId, err := url.QueryUnescape(encodedSessionId) //将QueryEscape转码的字符串还原
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(decodedSessionId) //7vbrpjDEnY97jPHI1XH3qNuE0KPfGEiich6vzbB1i3Y=
}
复制代码

举个例演示session的使用:定义一个登录窗口,先检查请求中是否有cookie,若没有返回登录界面,并生成一个session;用户输入用户名密码,然后POST发送,此时检测出有cookie,若是POST则返回登录成功;若为GET表明用户重新访问,显示欢迎回来。

复制代码
var manager *session.Manager

var helloback = `<html lang="en">  //欢迎回来的界面
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<form action = "/login" method="post">
    <p>{{.}}欢迎回来</p>
</form>
</body>
</html>`

var successlogin = `<html lang="en">  //登录成功的界面
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<form action = "/login" method="post">
    <p>{{.}}登录成功</p>
</form>
</body>
</html>`


func main() {
    manager, _ = session.NewManager("memeory_Session", "Dadishi", 3600)
        http.HandleFunc("/", login)
    http.HandleFunc("/login", login)
    http.ListenAndServe(":8080", nil)    
}

func login(w http.ResponseWriter, r *http.Request) {
    cookie, _ := r.Cookie("Dadishi")
    w.Header().Set("Content-Type", "text/html")
    r.ParseForm()
    if cookie == nil {
        manager.SessionStart(w, r)
        t := template.Must(template.ParseFiles("./views/login.html"))
        t.Execute(w, nil)
    } else {
        sess := manager.SessionStart(w, r)
        if r.Method == "POST" {
            sess.Set("username", r.Form["username"])
            sess.Set("password", r.Form["password"])
            t, _ := template.New("success login").Parse(successlogin)
            t.Execute(w, sess.Get("username"))
        } else if r.Method == "GET" {
            t, _ := template.New("hello back").Parse(helloback)
            t.Execute(w, sess.Get("username"))
        }
    }

}

// ./views/login.html登录界面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form action = "/login" method="post">
<lable for="username">UserName</lable>
<input type="text" name="username" size="30">
<br>
<lable for="password">PassWord</lable>
<input type="password" name="password" size="16">
<br>
<input type="submit" value="登录">
</form>
</body>
</html>

复制代码

新用户访问,没有cookie,则服务端返回登录界面然后创建session。

 

 输入完登录,显示:

 

 

 

 关闭该界面,然后重新访问访问(可以看到cookie是一样的):

 

 

 

 

 定义Mangaer删除session的方法:

复制代码
func (manager *Manager) SessionDestory(w http.ResponseWriter, r *http.Request) {
    cookie, err := r.Cookie(manager.cookieName)
    if err != nil || cookie.Value == "" {
        return
    } else {
        manager.lock.Lock()
        defer manager.lock.Unlock()
        sid, _ := url.QueryUnescape(cookie.Value)
        manager.provider.SessionDestory(sid)
        delete(manager.sids, sid)
        expiration := time.Now()
        cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1}
        http.SetCookie(w, &cookie)
    }
}
复制代码

 定义Mangaer检查session是否过期并删除session的方法:

复制代码
func (manager *Manager) SessionGC() {
    manager.lock.Lock()
    defer manager.lock.Unlock()
    time.AfterFunc(time.Duration(manager.maxLifeTime), func() { manager.provider.SessionGC(manager.maxLifeTime) })
}


func init() {
    go globalSessions.GC()
}
复制代码

 

 

 

posted on   博览天下with天涯海角  阅读(607)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示
< 2025年3月 >
23 24 25 26 27 28 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 1 2 3 4 5

点击右上角即可分享
微信分享提示