Cookie是指网站为了辨别用户身份而储存于客户端的数据,由网景公司的前雇员卢·蒙特利在1993年3月发明。最初定义于RFC 2109, 以及后续的规范 RFC 2965RFC 6265

服务器可以设置或读取Cookies中包含信息,借此维护用户跟服务器会话中的状态,并且可以基于Cookie实现Session,用来在服务器端存储用户的数据。

现在,几乎所有的商业网站都会使用Cookie技术用来标示浏览的用户,比如电子商务中的购物车、广告追踪系统等,并且涉及到一系列的安全问题和隐私问题。

Go的标准库中提供了Cookie的操作,并且第三方的库提供了Session的实现,所以在使用Go开发web应用中,我们可以很方便的实现session的管理,但是也有一些安全方面的设置需要注意。

本文介绍了使用Go语言开发web应用的时候,服务器端Cookie和Session的使用。

千言不如一例, 首先我们看一下下面这个例子。这个例子提供了设置Cookie,读取Cookie和删除Cookie的示例。

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
package main
import (
"encoding/json"
"flag"
"net/http"
)
var (
addr = flag.String("addr", ":8080", "server address")
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", index)
mux.HandleFunc("/get", getCookie)
mux.HandleFunc("/delete", deleteCookie)
mux.HandleFunc("/set", setCookie)
http.ListenAndServe(*addr, mux)
}
func index(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`<a href="#" onclick="alert(document.cookie)">Click here!</a>`))
}
func getCookie(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("this_is_a_test_cookie")
if err != nil {
w.Write([]byte("读取cookie失败: " + err.Error()))
} else {
data, _ := json.MarshalIndent(c, "", "\t")
w.Write([]byte("读取的cookie值: \n" + string(data)))
}
}
func deleteCookie(w http.ResponseWriter, r *http.Request) {
c := http.Cookie{
Name: "this_is_a_test_cookie",
MaxAge: -1}
http.SetCookie(w, &c)
w.Write([]byte("cookie已被删除"))
}
func setCookie(w http.ResponseWriter, r *http.Request) {
c := http.Cookie{
Name: "this_is_a_test_cookie",
Value: "true",
HttpOnly: true,
//Secure: true,
MaxAge: 300}
http.SetCookie(w, &c)
w.Write([]byte("cookie已创建\n"))
}

主要是围绕http.Cookie这个struct的一些方法。

  • 读取: http.RequestCookie(name string) (*Cookie, error)Cookies() []*Cookie方法 , http.ResponseCookies() []*Cookie方法。
  • 写入: http.RequestAddCookie(c *Cookie), SetCookie(w ResponseWriter, cookie *Cookie)函数

从服务器端来看, 我们从http.Request中读取客户端传入的Cookie, 或者把Cookie写入到http.Response中。
从客户端来看,我们需要把Cookie设置到http.Request传给服务器,或者从http.Response中读取Cookie。
所以你会看到RequestResponse中都有读取和设置Cookie的方法,只是针对不同的场景而已。

查看下面的Cookie的定义,你也会看到有些字段在读取的时候是空值,这是因为在这个场景下,是没有这个值的,比如服务器端读取Cookie值时,浏览器并不会把Max-Age的传给服务器,所以在服务器端读取这个Cookie指的时候MaxAge值是0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Cookie struct {
Name string
Value string
Path string
Domain string
Expires time.Time
RawExpires string
MaxAge int
Secure bool
HttpOnly bool
SameSite SameSite
Raw string
Unparsed []string
}

这几个字段要熟悉:

  • Name/Value: Cookie的名称和指,cookie中最基本的数据
  • Path: 可以将Cookie限定在某个路径下,只有这个路径和它的子路径才可以访问这个Cookie, 比如Path = examle.com/abc/的Cookie,只有examle.com/abc/和子路径比如examle.com/abc/def才能访问。默认为当前路径。
  • Domain: 只关联的web服务器的域名, 比如example.com。如果你设置的Cookie的domain为a.example.com,那么访问b.example.com的时候是不能访问这个Cookie的,如果想访问,那么请将domain设置它们共同的域example.com。 你能在访问example.com的时候设置domain为baidu.com吗?不行,这是出于安全的限制。 默认是当前的域名。

  • Expires: 为过期时间,Cookie超过这个时间点就会被删除了。

  • RawExpires: Expires字符串表示, 格式为Wdy, DD Mon YYYY HH:MM:SS GMT或者Wdy, DD Mon YY HH:MM:SS GMT,在读取Cookie时候会被设置

  • MaxAge: 最大存活时间,单位是秒,-1为删除这个Cookie, 0是不设置Max-Age, 正数为存活的秒数。你可以查看这个测试页了解不同的设置的效果。

如果同时设置了ExpiresMaxAge,以Max-Age为准。

  • Secure: 设置 Cookie 只在确保安全的请求中才会发送。当请求是 HTTPS 或者其他安全协议时,包含 secure 选项的 Cookie 才能被保存到浏览器或者发送至服务器。

  • HttpOnly: 这个选项用来设置 Cookie 是否能通过 js 去访问。强烈建议设置这个值为true,否则容易被XSS等攻击。你可以把上面的例子这个字段注释掉,访问首页的时候点击连接为显示当前页面的Cookie的值,这只是用来测试,要是有一段javascript把你的cookie传到第三方网站危险就大了。

  • SameSite: 2016年Chrome中加入的一个新属性,避免在跨域(XSRF)访问的时候把Cookie传给第三方网站。

Cookie的缺陷

Cookie也有一些天生的缺陷。

  • Cookie会被附加在每个HTTP请求中,所以无形中增加了流量。
  • 由于在HTTP请求中的Cookie是明文传递的,很容易遭受到中间人的攻击,所以安全性成问题,除非用HTTPS。
  • Cookie的大小限制在4KB左右,对于复杂的存储需求来说是不够用的。
  • 不同的浏览器Cookie不是共享的,你在Chrome中登录了一个网站,使用Firefox还需要再次登录,因为这两个浏览器的Cookie不共享。
  • 同一台机器同一个浏览器会共享同一个Cookie池,A用完浏览器后,如果不清理Cookie, B使用的时候会得到A的Cookie。
  • Cookie存在本地是明文访问的,其他用户如果能访问的这个Cookie,就能看到这个Cookie的内容
  • 容易遭受XSS跨站访问
  • 第三方脚本追踪。网站中嵌入第三方的代码,就容易被第三方公司利用,在一些互联网巨头和广告公司中经常会使用。你在A网站浏览一些商品,在浏览B网站的时候,B网站的广告会给你推送这些类似商品的信息,这是因为A和B都嵌入了同一个第三方公司的代码,通过Cookie能追踪到你的浏览记录。
  • Cookie投毒攻击,例如在一个购物网站的Cookie中包含了顾客应付的款项,攻击者将该值改小,达到少付款的目的。

所以在使用Cookie的时候,需要一些额外的措施,避免收到攻击,下面是一些推荐设置:

  • 设置合理的domain和path
  • 设置合适的MaxAge, 不使用时或者推出时设置为-1
  • 设置HttpOnly为true
  • 设置SameSite
  • 采用https, 设置Secure为true
  • cookie不存储私密的东西,名称不设置直观易读的名称
  • cookie进行加盐和加密
  • 不设置太大的Cookie
  • 设置到安全较高的操作时,服务器端对cookie和客户端ID(浏览器属性、操作系统、客户端IP)进行验证,避免被人窃取cookie

加密Cookie

涉及到私密的数据的时候,可以采取服务器加密的方式,在服务器先进行加密,再设置Cookie。你可以使用securecookie实现这个功能。

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
// Hash keys should be at least 32 bytes long
var hashKey = []byte("very-secret")
// Block keys should be 16 bytes (AES-128) or 32 bytes (AES-256) long.
// Shorter keys may weaken the encryption used.
var blockKey = []byte("a-lot-secret")
var s = securecookie.New(hashKey, blockKey)
func SetCookieHandler(w http.ResponseWriter, r *http.Request) {
value := map[string]string{
"foo": "bar",
}
if encoded, err := s.Encode("cookie-name", value); err == nil {
cookie := &http.Cookie{
Name: "cookie-name",
Value: encoded,
Path: "/",
Secure: true,
HttpOnly: true,
}
http.SetCookie(w, cookie)
}
}
func ReadCookieHandler(w http.ResponseWriter, r *http.Request) {
if cookie, err := r.Cookie("cookie-name"); err == nil {
value := make(map[string]string)
if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil {
fmt.Fprintf(w, "The value of foo is %q", value["foo"])
}
}
}

hashKey用来生成Cookie值的摘要(hmac),可以验证数据是否被篡改,blockKey是可选的,可以用来加密cookie的值。

cookie值可以是任意的对象,默认使用gob进行序列化,当然也可以配置使用json格式。

使用起来很方便,只是多一步Encode/Decode的过程。

它还提供了另一个好处,就是可以在服务器端验证Cookie的设置日期,在服务器端也进行MaxAge的校验。

Session

Cookie将数据存放在客户端,并且还有4k的大小的限制,为了更好的和用户进行交互,很多编程语言的开发框架提供了session的功能。

Session还是基于cookie实现的(当然在禁用cookie的情况下可以在url后加后缀的方式曲折的实现)。一个session对应一个sessionid, 可以将这个sessionid作为cookie设置到客户端,服务器端建立一个 sessionid <---> session的对应结构。 浏览器将sessionid发送给客户端的时候,服务器根据这个id得到session对象,就可以存取这个sessoin的内容。

可以将session放在内容中,也可以放在中心服务器如memcached、redis、mysql中,设置可以在web服务器中同步,这样可以实现有状态的session负载均衡。

sessionid要随机化,否则如果被人猜中的话,可以通过伪造session id冒充用户。

sessionid在cookie中的名称,不同的编程语言/web框架各不相同。

比如php使用PHPSESSID, java使用JSESSIONID, ColdFusion使用CFID & CFTOKEN, asp.net使用ASP.NET_SessionId, 通过cookie中的sessionid的名称,我们大致能推断出服务器所使用的编程语言。如果你不想暴露服务器的技术栈,你可以使用通用的名称,比如id

Go中有多个第三方的session实现,最常用的是gorilla/sessions, 它可以用在其他的go的web框架中,并且有一二十个不同存储的实现,可以实现分布式的session。

使用起来也很方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import (
"net/http"
"github.com/gorilla/sessions"
)
var store = sessions.NewCookieStore(os.Getenv("SESSION_KEY"))
func MyHandler(w http.ResponseWriter, r *http.Request) {
// 得到一个session
session, _ := store.Get(r, "session-name")
// 设置session的一些值
session.Values["foo"] = "bar"
session.Values[42] = 43
// 在返回之前保存它
session.Save(r, w)
}

如果不使用gorilla/mux框架,你需要context.ClearHandler包装你的handler,否则会出现内存泄漏。2018年12月9日 gorilla/sessions 的提交12bd476使用标准库context替换了它自己的标准库,不再有内存泄漏的问题了。

beego中也实现了一个独立的session模块, 最近几天, fasthttp作者也实现了一个session库,当然秉承他的理念,性能是第一位的, 有兴趣的同学也可以关注下fasthttp/session

JWT

对于访问认证来说,为每个客户端提供一个session对象,这对于用户访问巨大的网站来说,这是相当奢侈的。那么能不能提供一种机制,把用户的访问权限放在客户端,但是又能保证客户端的数据不被篡改?

类似securecookie机制,目前正在流行一种JWT的认证方式。

你可以搜索一些相关的介绍,比如什么是 JWT -- JSON WEB TOKENJSON Web Token 入门教程

Go也有jwt的库可以使用, 比较有名的是jwt-go

参考文档

  1. https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
  2. https://www.sohamkamani.com/blog/2017/01/08/web-security-session-cookies/
  3. https://stackoverflow.com/questions/17834144/how-tomcat-handles-session-internally
  4. https://docs.spring.io/spring-session/docs/1.3.3.RELEASE/reference/html5/guides/custom-cookie.html#custom-cookie-spring-configuration
  5. https://stackoverflow.com/questions/595872/under-what-conditions-is-a-jsessionid-created
  6. https://www.jianshu.com/p/e8736aa3be2b
  7. https://www.cnblogs.com/doit8791/p/5926575.html
  8. https://harttle.land/2015/08/10/cookie-session.html
  9. https://mrcoles.com/blog/cookies-max-age-vs-expires/
  10. https://github.com/boj/redistore
  11. https://github.com/gorilla/sessions
  12. https://github.com/gorilla/securecookie
  13. https://github.com/fasthttp/session
  14. https://www.sohamkamani.com/blog/2018/03/25/golang-session-authentication/
  15. https://www.calhoun.io/securing-cookies-in-go/
  16. https://medium.com/@sherryhsu/session-vs-token-based-authentication-11a6c5ac45e4
  17. https://www.owasp.org/index.php/Session_Management_Cheat_Sheet
  18. https://en.wikipedia.org/wiki/HTTP_cookie#Cookie_theft_and_session_hijacking