Dawn's Blogs

分享技术 记录成长

0%

Go Web编程学习笔记 (3) Session和数据存储

Session 和 Cookie

Cookie 是由浏览器维持的,保存在客户端的一段文本信息,伴随着用户请求和页面在 Web 服务器和浏览器之间传递。通过 Cookie,服务器就可以验证 Cookie 信息、记录用户状态。

img

Cookie 是有时间限制的,根据生命周期不同分为会话 Cookie 和持久 Cookie:

  • 会话 Cookie:不设置生命周期的默认值,表示这个 Cookie 的生命周期到浏览器关闭为止,会话 Cookie 一般保存在内存中。
  • 持久 Cookie:设置了过期时间时,浏览器就会将 Cookie 保存在硬盘上,有效期直到超过了过期时间。

Go 语言中,通过 net/http 包中的 SetCookie 来设置:

1
func SetCookie(w ResponseWriter, cookie *Cookie)

Cookie 结构体结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Cookie struct {
Name string
Value string
Path string
Domain string
Expires time.Time
RawExpires string
// MaxAge=0表示未设置Max-Age属性
// MaxAge<0表示立刻删除该cookie,等价于"Max-Age: 0"
// MaxAge>0表示存在Max-Age属性,单位是秒
MaxAge int
Secure bool
HttpOnly bool
Raw string
Unparsed []string // 未解析的“属性-值”对的原始文本
}

设置 Cookie 例子:

1
2
3
4
5
6
7
8
9
10
11
// 设置过期时间
expiration := time.Now()
expiration = expiration.AddDate(1, 0, 0)
// 实例化 Cookie对象
cookie := &http.Cookie{
Name: "username",
Value: "dawn",
Expires: expiration,
}
// 写入 HTTP 响应
http.SetCookie(w, cookie)

Go 语言中,使用 r.Cookie 根据 name 读取 Cookie:

1
func (r *Request) Cookie(name string) (*Cookie, error)

还可以通过 r.Cookies 一次性得到所有的 Cookie 信息:

1
func (r *Request) Cookies() []*Cookie

Session

Session 是一种服务器端的机制,服务器使用一种类似于散列表的结构来保存信息。服务器使用 Session id 来标识 Session 信息,它由服务器端产生,相当于一个密钥。可以借助 Cookie 或者 GET 请求方式传输 Session id。

img


总结

  • Session 和 Cookie 机制都是为了克服 HTTP 协议的无状态缺陷,用于记录用户状态。
  • Session 依托 Cookie 实现,将 Session id 保存在 Cookie 中。
  • Cookie 因为将所有信息都保存在客户端中,发起请求时会携带这些信息,所以有一定的安全隐患。

Go 实现 Session

Session 创建过程

服务器端在创建 Session 时会分为三个步骤:

  • 生成一个全局唯一的标识符 Session id
  • 开辟数据存储空间来存储 Session 信息:
    • 内存:速度快,系统一旦掉电,所有的会话数据就会丢失。
    • 文件或者数据库:增加 I/O 开销,但是可以实现某种程度上的 Session 持久化,以及 Session 的共享。
  • 将 Session id 发送给客户端
    • Cookie:服务端通过设置 Set-cookie 头就可以将 Session id 发送给客户端,而客户端此后的每一次请求都会带上这个 Session id。一般情况下,会将包含 Session id 的 Cookie 的过期时间设置为 0。
    • URL 重写:返回给用户的页面里的所有的 URL 后面追加 Session id,这样用户在收到响应之后,无论点击响应页面里的哪个链接或提交表单,都会自动带上 Session id。如果客户端禁用了 Cookie,这种方案就是首选。

Go 实现 Session 管理

目前 Go 标准包没有为 Session 提供任何支持,下面手动实现 Session 的管理和创建。

Session 管理器

定义一个全局的 Session 管理器,用于 Session 的管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Manager struct {
cookieName string
lock sync.Mutex
provider Provider
maxLifeTime int64
}

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{
cookieName: cookieName,
provider: provider,
maxLifeTime: maxLifeTime,
}, nil
}

Session 是保存在服务器端的数据,它可以以任何的方式存储,比如存储在内存、数据库或者文件中。因此我们抽象出一个 Provider 接口,用以表征 Session 管理器底层存储结构

  • SessionInit 实现 Session 的初始化。
  • SessionRead 返回 sid 所代表的 Session 变量,如果不存在,那么将以 sid 为参数调用 SessionInit 函数创建并返回一个新的 Session 变量。
  • SessionDestroy 销毁 sid 对应的 Session 变量。
  • SessionGC 根据 maxLifeTime 来删除过期的数据。
1
2
3
4
5
6
7
8
9
10
type Provider interface {
// SessionInit 实现 Session 的初始化
SessionInit(sid string) (Session, error)
// SessionRead 返回 sid 所代表的 Session 变量,如果不存在,那么将以 sid 为参数调用 SessionInit 函数创建并返回一个新的 Session 变量
SessionRead(sid string) (Session, error)
// SessionDestroy 销毁 sid 对应的 Session 变量
SessionDestroy(sid string) error
// SessionGC 根据 maxLifeTime 来删除过期的数据
SessionGC(maxLifeTime int64)
}

对 Session 的处理基本上就是设置值、读取值、删除值、获取当前 Session id 四种操作,所以 Session 接口定义如下:

1
2
3
4
5
6
type Session interface {
Set(key, value interface{}) error // set session value
Get(key interface{}) interface{} // get session value
Delete(key interface{}) error // delete session value
SessionID() string // back current sessionID
}

全局唯一的 Session id

Session id 用来标识每一个用户以及对应的 Session,所以必须是全局唯一的:

1
2
3
4
5
6
7
8
9
// 创建全局唯一的 session id
func (manager *Manager) sessionId() string {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return ""
}

return base64.URLEncoding.EncodeToString(b)
}

Session 的创建

SessionStart 用来检测是否已经有某个 Session 与当前来访用户发生了关联,如果没有则创建之

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// SessionStart 用来检测是否已经有某个 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 == "" {
// 没有 Session,创建
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)
session, _ = manager.provider.SessionRead(sid)
}
return
}

Session 销毁

在销毁 Session 时,除了从存储结构中删除 Session 信息之外,还需要设置 Cookie过期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(manager.cookieName)
if err != nil || cookie.Value == "" {
return
}
// 上锁
manager.lock.Lock()
defer manager.lock.Unlock()
// 从存储结构中删除
manager.provider.SessionDestroy(cookie.Value)
// 设置 Cookie
expiration := time.Now()
cookie = &http.Cookie{
Name: manager.cookieName,
Path: "/",
HttpOnly: true,
Expires: expiration,
MaxAge: -1, // 立即删除session
}
http.SetCookie(w, cookie)
}

过期 Session 清理

GC 利用了 time 包的定时器功能,当超时之后调用 GC 清理过期数据。

1
2
3
4
5
6
7
8
9
// GC 删除过期数据
func (manager *Manager) GC() {
manager.lock.Lock()
defer manager.lock.Unlock()
manager.provider.SessionGC(manager.maxLifeTime)
time.AfterFunc(time.Duration(manager.maxLifeTime), func() {
manager.GC()
})
}

Go 实现 Session 存储

在上一节中,定义了 Session 管理器 session.Manager,以及两个接口 session.Providersession.Session,这两个接口用于存储 Session 数据。本节将实现一个基于内存的 Session 存储方式。

Provider 接口实现

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package memory

import (
"container/list"
"go-web-demo/ch6/session"
"sync"
"time"
)

type Provider struct {
lock sync.Mutex // 用来锁
sessions map[string]*list.Element // 用来存储在内存
list *list.List // 用来做 gc,按照 SessionStore.timeAccessed 最近访问时间排序
}

// Provider 实现 session.Session 接口

// SessionInit 实现 Session 的初始化
func (p *Provider) SessionInit(sid string) (session.Session, error) {
p.lock.Lock()
defer p.lock.Unlock()
v := make(map[interface{}]interface{}, 0)
newSess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v}
element := p.list.PushFront(newSess)
p.sessions[sid] = element
return newSess, nil
}

// SessionRead 返回 sid 所代表的 Session 变量,如果不存在,那么将以 sid 为参数调用 SessionInit 函数创建并返回一个新的 Session 变量
func (p *Provider) SessionRead(sid string) (session.Session, error) {
if element, ok := p.sessions[sid]; ok {
// 已存在 Session
return element.Value.(*SessionStore), nil
}
// 未存在,新建Session
sess, err := p.SessionInit(sid)
return sess, err
}

// SessionDestroy 销毁 sid 对应的 Session 变量
func (p *Provider) SessionDestroy(sid string) error {
if element, ok := p.sessions[sid]; ok {
// Session 已存在
// 从字典中删除
delete(p.sessions, sid)
// 从链表中删除
p.list.Remove(element)
return nil
}

return nil
}

// SessionGC 根据 maxLifeTime 来删除过期的数据
func (p *Provider) SessionGC(maxLifeTime int64) {
p.lock.Lock()
defer p.lock.Unlock()

for {
element := p.list.Back()
if element.Value.(*SessionStore).timeAccessed.Unix()+maxLifeTime < time.Now().Unix() {
// 超时,清理 Session
// 从字典中删除
delete(p.sessions, element.Value.(*SessionStore).sid)
// 从链表中删除
p.list.Remove(element)
}
}
}

// SessionUpdate 用于更新最近访问时间
func (p *Provider) SessionUpdate(sid string) error {
p.lock.Lock()
defer p.lock.Unlock()

if element, ok := p.sessions[sid]; ok {
element.Value.(*SessionStore).timeAccessed = time.Now()
p.list.MoveToFront(element)
return nil
}
return nil
}

Session 接口实现

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
32
33
34
35
36
37
38
39
40
41
42
43
package memory

import (
"container/list"
"time"
)

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

var (
p = &Provider{list: list.New()}
)

// Set 设置值
func (st *SessionStore) Set(key, value interface{}) error {
st.value[key] = value
return p.SessionUpdate(st.sid)
}

// Get 获取值
func (st *SessionStore) Get(key interface{}) interface{} {
p.SessionUpdate(st.sid)
if v, ok := st.value[key]; ok {
return v
}
return nil
}

// Delete 删除值
func (st *SessionStore) Delete(key interface{}) error {
delete(st.value, key)
return p.SessionUpdate(st.sid)
}

// SessionID 获取 sid
func (st *SessionStore) SessionID() string {
return st.sid
}

初始化以及注册 Provider

初始化以及在 session.Manager注册该基于内存的 Provider。

1
2
3
4
5
6
7
8
9
10
11
package memory

import (
"container/list"
"go-web-demo/ch6/session"
)

func init() {
p.sessions = make(map[string]*list.Element, 0)
session.Register("memory", p)
}

调用

当 import 的时候已经执行了 memory 函数里面的 init 函数,这样就已经注册到 session 管理器中,我们就可以使用了:

1
2
3
4
5
6
7
8
9
10
11
12
import (
"go-web-demo/ch6/session"
_ "go-web-demo/ch6/session/providers/memory"
)

var globalSessions *session.Manager

func init() {
globalSessions, _ = session.NewManager("memory", "gosessionid", 3600)
// 开一个线程启动 GC
go globalSessions.GC()
}

防范 Session 劫持

httponly 和 token

第一种方案通过 httponly + token 来预防 Session 劫持:

  • 可以设置 Session id 的值只能由 Cookie 进行设置,同时设置 httponly 为 true,这个属性可以防止通过客户端脚本访问到 Cookie,进而防止攻击者读取到 Cookie 中的 Session id。
  • 在每个请求里面加上 token,然后每一次验证 token,从而保证用户的请求都是唯一性。

间隔生成新的 Sesseion id

第二种方案是给 Session 额外设置一个创建时间的值,一旦过了一定的时间,就销毁这个 Session id,重新生成 Session。

1
2
3
4
5
6
7
8
9
10
11
// 设置过期时间 60s
expires := 60

createTime := sess.Get("createtime")
if createTime == nil {
sess.Set("createtime", time.Now().Unix())
} else if createTime.(int64) + expires < time.Now().Unix() {
// 当前 Session 过期,重新分配 Session
globalSessions.SessionDestroy(w, r)
sess = globalSessions.SessionStart(w, r)
}

这里为 Session 设置了一个值,用于记录生成 Session id 的时间。每次请求都判断是否过期,如果过期,则分配新的 Session id。

总结

上面两个手段的组合可以在实践中消除 session 劫持的风险:

  • Session id 的频繁改变,使得攻击者很难获取到有效的 Session id。

  • Session 只在 Cookie 中传递,并且设置了 httponly 选项,可以有效阻止通过客户端脚本访问到 Cookie,也可以预防 XSS 攻击获取 Cookie。

  • 还可以设置 MaxAge=0,这样 Cookie 就不会存储在浏览器的记录中,随着浏览器关闭 Cookie 也随机消失。