一、Cookie的基本概念
1.什么是Cookie
Cookie
(也叫Web Cookie
或浏览器Cookie
)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie
使基于无状态的HTTP协议记录稳定的状态信息成为了可能。
Cookie
主要用于以下三个方面:
- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
- 个性化设置(如用户自定义设置、主题等)
- 浏览器行为跟踪(如跟踪分析用户行为等)
2.Go语言如何表示Cookie
在Go
的net/http
库中使用http.Cookie
结构体表示一个Cookie
数据,调用http.SetCookie
函数则会告诉终端用户的浏览器把给定的http.Cookie
值设置到浏览器Cookie
里,http.Cookie
结构体类型的定义如下:
1 type Cookie struct { 2 Name string 3 Value string 4 5 Path string // optional 6 Domain string // optional 7 Expires time.Time // optional 8 RawExpires string // for reading cookies only 9 10 // MaxAge=0 means no ‘Max-Age‘ attribute specified. 11 // MaxAge<0 means delete cookie now, equivalently ‘Max-Age: 0‘ 12 // MaxAge>0 means Max-Age attribute present and given in seconds 13 MaxAge int 14 Secure bool 15 HttpOnly bool 16 SameSite SameSite 17 Raw string 18 Unparsed []string // Raw text of unparsed attribute-value pairs 19 }
大概用法如下:
1 func someHandler(w http.ResponseWriter, r *http.Request) { 2 c := http.Cookie{ 3 Name: "UserName", 4 Value: "Casey", 5 } 6 http.SetCookie(w, &c) 7 }
Cookie的各个字段意义如下:
Domain
默认值是当前正在访问的Host
的域名,假设我们现在正在访问的是www.example.com
,如果需要其他子域名也能够访问到正在设置的Cookie
值的话,将它设置为example.com
。注意,只有正在被设置的Cookie
需要被其他子域名的服务访问到时才这么设置。用法如下:
1 c := Cookie{ 2 ...... 3 Domain: "example.com", 4 }
Path
设置当前的 Cookie 值只有在访问指定路径时才能被服务器程序读取。默认为服务端应用程序上的任何路径,但是您可以使用它限制为特定的子目录。例如:
1 c := Cookie{ 2 Path: "/app/", 3 }
Secure
标记为Secure
的Cookie只应通过被HTTPS
协议加密过的请求发送给服务端。但即便设置了 Secure
标记,敏感信息也不应该通过Cookie
传输,因为Cookie
有其固有的不安全性,Secure
标记也无法提供确实的安全保障。从 Chrome 52 和 Firefox 52 开始,不安全的站点(http:
)无法使用Cookie
的 Secure
标记。
HttpOnly
为避免跨域脚本 (XSS) 攻击,通过JavaScript
的API无法访问带有 HttpOnly
标记的Cookie,它们只应该发送给服务端。如果包含服务端Session
信息的Cookie
不想被客户端JavaScript
脚本调用,那么就应该为其设置 HttpOnly
标记。
二、Cookie的使用
1.设置cookie:
1 cookie := http.Cookie{Name: "cookiename", Value: "testcookievalue", Path: "/", MaxAge: 86400} 2 http.SetCookie(w, &cookie)
2.读取cookie:
1 cookie, err := req.Cookie("cookiename")
3.删除cookie
1 cookie := http.Cookie{Name: "cookiename", Path: "/", MaxAge: -1} 2 http.SetCookie(w, &cookie)
4.具体使用例子:
1 package main 2 3 import "net/http" 4 5 func main() { 6 http.HandleFunc("/readcookie", ReadCookie) 7 http.HandleFunc("/writecookie", WriteCookie) 8 http.HandleFunc("/deletecookie", DeleteCookie) 9 http.ListenAndServe(":9090", nil) 10 } 11 12 func WriteCookie(w http.ResponseWriter,r *http.Request) { 13 //创建新的本地cookie 14 cookie := http.Cookie{Name:"localCookie",Value:"GoLang",Path:"/",MaxAge:86400} 15 http.SetCookie(w,&cookie) 16 w.Write([]byte("设置cookie成功")) 17 } 18 19 func ReadCookie(w http.ResponseWriter,r *http.Request) { 20 //读取cookie 21 cookie,err := r.Cookie("localCookie") 22 if err == nil { 23 cookieValue := cookie.Value 24 //将数据写入http连接中 25 w.Write([]byte("cookie的值为:"+cookieValue)) 26 }else { 27 w.Write([]byte("读取cookie出错:"+err.Error())) 28 } 29 } 30 31 func DeleteCookie(w http.ResponseWriter,r *http.Request) { 32 cookie := http.Cookie{Name:"localCookie",Path:"/",MaxAge:-1} 33 http.SetCookie(w,&cookie) 34 w.Write([]byte("<删除cookie成功")) 35 }
三、Cookie的数据安全传输
接下来我们探讨两种安全传输Cookie
的方法
1.对Cookie数据进行数字签名
数字签名是对数据添加一个签名,以便验证其真实性。终端用户无需对数据进行加密或做掩码,但是我们需要向 cookie 添加足够的数据,这样如果用户更改了数据的话,我们能够检测出来。
通过哈希来实现这个方案——会对数据进行 hash,然后将数据和数据的哈希值都存到 cookie 中。之后当用户发送 cookie 给我们,我们会对数据再次做哈希,验证是否和之前的哈希值匹配。
我们也不希望用户创建新的哈希值,所以你通常会看到使用 HMAC 这类哈希算法,通过一个密钥对数据做哈希。防止用户同时修改数据以及数字签名(哈希值)。对数据进行数字签名是在数据上添加“签名”的行为,以便可以验证其真实性。不需要对数据进行加密或屏蔽。
签名的工作方式是通过散列-我们对数据进行散列,然后将数据与数据散列一起存储在Cookie
中。然后,当用户将Cookie
发送给我们时,我们再次对数据进行哈希处理,并验证其是否与我们创建的原始哈希匹配。
我们不希望用户也用篡改后的数据创建新的哈希,因此经常会看到使用HMAC
之类的哈希算法,以便可以使用密钥对数据进行哈希。这样可以防止最终用户同时编辑数据和数字签名(哈希)。
JWT
也是使用的这种数字签名的方式进行传输的。
上面的数据签名过程并不需要我们自己去实现,我们可以用github.com/gorilla/securecookie程序包来完成此操作,在该程序包中,你可以在创建SecureCookie
时为其提供哈希密钥,然后使用该对象来保护你的Cookie
。
对Cookie
数据进行签名:
1 //var s = securecookie.New(hashKey, blockKey) 2 var hashKey = securecookie.GenerateRandomKey(64) 3 var s = securecookie.New(hashKey, nil) 4 5 func SetCookieHandler(w http.ResponseWriter, r *http.Request) { 6 encoded, err := s.Encode("cookie-name", "cookie-value") 7 if err == nil { 8 cookie := &http.Cookie{ 9 Name: "cookie-name", 10 Value: encoded, 11 Path: "/", 12 } 13 http.SetCookie(w, cookie) 14 fmt.Fprintln(w, encoded) 15 }
解析被签名的 Cookie:
1 func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { 2 if cookie, err := r.Cookie("cookie-name"); err == nil { 3 var value string 4 if err = s.Decode("cookie-name", cookie.Value, &value); err == nil { 5 fmt.Fprintln(w, value) 6 } 7 } 8 }
注意这里的Cookie
数据未加密,仅仅是被编码了,任何人都可以把Cookie
数据解码回来。
这里有个非常重要的警告:对于同时往数字签名的数据中添加用户信息和过期时间的情况,如果用上述方法保证可靠性,你必须非常小心,严格遵守 JWT 的使用模式。不能单单依赖 cookie 的过期时间,因为该日期未被加密,用户可以创建一个新的没有过期时间的 cookie,然后把 cookie 签名的部分拷贝过去,基本上就是创建了一个保证他们永久在线的 cookie。
2.加密Cookie 数据
每当将数据存储在Cookie
中时,请始终尽量减少存储在Cookie
中的敏感数据量。不要存储用户密码之类的东西,并确保任何编码数据也没有此信息。在某些情况下,开发人员在不知不觉中将敏感数据存储在Cookie
或JWT
中,因为它们是base64
编码的,但实际上任何人都可以解码该数据。它已编码,未加密。
这是一个很大的错误,因此,如果你担心意外存储敏感内容,建议 你使用gorilla/securecookie
之类的软件包。
之前我们讨论了如何将其用于对Cookie
进行数字签名,但是securecookie
也可以用于加密和解密Cookie
数据,以使其无法轻松解码和读取。
要使用该软件包加密Cookie
,只需在创建SecureCookie
实例时传入一个blockKey
即可。
将上面签名Cookie
的代码片段进行一些小改动,其他地方完全不用动,securecookie
包会帮助我们进行Cookie
的加密和解密:
1 var hashKey = securecookie.GenerateRandomKey(64) 2 var blockKey = securecookie.GenerateRandomKey(32) 3 var s = securecookie.New(hashKey, blockKey)
3. 混淆数据
另外一种方式是对数据做掩码,确保用户无法伪造数据。例如,不要像如下方式一样保存 cookie:
1 // Don‘t do this 2 http.Cookie{ 3 Name: "user_id", 4 Value: "123", 5 }
我们可以保存一些数据值,通过这些数据值能够映射到数据库真实的数据。一般通过 session ID 或者记录 token 实现,有一个叫做 remember_tokens
的表来记录数据:
1 remember_token: LAKJFD098afj0jasdf08jad08AJFs9aj2ASfd1 2 user_id: 123
然后就可以只在 cookie 中保存记录 token,这样即便用户想要伪造,也不知道要改什么。它看起来就像乱码。
后面当用户访问我们的应用,我们会在数据库中查找其记录 token,然后判断是哪个用户登录了。
为了使该方案能够运行,你需要确保混淆数据是 :
- 映射到了一个用户(或者其他资源)
- 随机的
- 熵值较高
- 可以设为失效状态(例如,删除 / 改变 DB 中保存的 token)
这个方法的一个缺点是,对于每个需要验证用户身份的页面请求,都需要进行数据库查询,不过这个缺点一般不会被注意到,可以通过缓存或其他类似技术解决掉。该方法相对 JWT 的优点是你可以快速废弃 session。
这是我知道的最常见的验证策略,尽管 JWT 最近在所有的 JS 框架得到流行。
4.数据泄露(Data leaks)
像 cookie 盗用一样,在成为真正的威胁之前,数据泄露通常需要有其他的攻击途径,不过谨慎一些总是好的。也是因为,cookie 被盗并不意味着我们想要故意告诉黑客用户密码。
无论何时往 cookie 中保存数据,都要尽可能减少存储敏感数据的量。不要存储用户的密码,确保编码过得数据中也没有密码。类似这篇 文章指出的几个例子,开发者不知不觉地在 cookie 或者 JWT 中保存了敏感数据,采用 base64 编码,但实际上任何人都可以解码该数据。数据是被编码了,而不是加密了。
早前,我们讨论了如何对 cookie 数字签名,但是 securecookie
也可以用于加 / 解密你的 cookie 数据,故而其不太会被轻易解码、访问到。
启用该库的加密功能,你只需要在创建 SecureCookie
实例时候,简单地传入 block key。
1 var hashKey = []byte("very-secret") 2 // Add this part for encryption. 3 var blockKey = []byte("a-lot-secret") 4 var s = securecookie.New(hashKey, blockKey)
其他和文章数字签名部分例子类似。
还是要着重强调下,不要在 cookie 中保存任何敏感的数据;尤其不要存密码。加密简单来说就是让内容更加安全一点的方法,防止有半敏感数据出现在 cookie 中。
5.跨站脚本(Cross-site scripting(XSS))
跨站脚本,通常写做 XSS,黑客尝试向你的网站注入你没有写的 Javascript,但是由于攻击起作用的方式浏览器不知道,所以会像运行你服务器提供的代码一样运行。
通常,你需要尽最大的能力阻止 XSS 攻击,这里不会讨论 XSS 的过多细节,但以防蒙混过关,我建议不需要访问 cookie 的 JavaScript 代码就禁止其权限。之后如果有需要的话,可以在启用,所以这不是写低可靠性代码的理由。
Go 里面实现这点比较简单。创建 cookie 时候,只需要简单地设置 HttpOnly
字段为 true。
1 cookie := http.Cookie{ 2 // true means no scripts, http requests only. This has 3 // nothing to do with https vs http 4 HttpOnly: true, 5 }
6.跨站请求伪造(CSRF(Cross Site Request Forgery))
当用户访问一个不是你的网站,但是那个网站有一个表单提交到你的 Web 应用时候,可能会发生 CSRF。由于终端用户提交了表单且不是通过脚本提交的,浏览器会视为用户触发行为,在提交表单的同时传输 cookie 过去。
开始,这看起来并不坏,但如果外部网站发送非用户想要的数据呢?例如,badsite.com 可能有一个表单提交一个要求转 ¥ 100 到他们银行账户的请求,该请求会被发到 chase.com,你可能会在那里登录银行账户,这可能导致钱在未经用户允许情况下被转移。
cookie 本身并没有什么错,但是如果你用 cookie 做一些验证工作,你就需要 Gorilla 的csrf 包。
该包提供了一个 CSRF token,你可以将它插入到每个 Web 表单中,不论什么时候,只要提交的表单没有 token,那么 csrf
包的中间件就会拒绝这个表单,外部网站就不可能欺骗用户提交表单。
有关 CSRF 的更多内容,请看如下内容:
- https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)
- https://en.wikipedia.org/wiki/Cross-site_request_forgery
7.限制访问 cookie
最后要讨论的和特定的攻击没有关系,更多的是一种使用 cookie 的指导性原则:尽量限制对 cookie 的访问,只在给需要的地方提供访问权限。
前面只在讨论 XSS 的地方简单的提及到了这一点,但是其实你应该在任何地方限制对 cookie 的访问。例如,如果你的 Web 应用不使用子域名,那么就没有理由提供所有子域名访问 cookie 的权限。cookie 默认就是限制子域名的,所以实际上,你不需要做任何事情去限制特定的域名。
另一方面,如果你确实要和子域名共享 cookie,那么可以这么做:
1 c := Cookie{ 2 // Defaults to host-only, which means exact subdomain 3 // matching. Only change this to enable subdomains if you 4 // need to! The below code would work on any subdomain for 5 // yoursite.com 6 Domain: "yoursite.com", 7 }
更多有关 domain 解析的信息,请看 https://tools.ietf.org/html/rfc6265#section-5.1.3。也可以查看源代码里https://golang.org/src/net/http/cookie.go#L157 了解 cookie 如何获取其默认值。 你也可以阅读这个stack overflow 问题 获取更多信息,了解为什么现在无需像之前一样为子域名 cookie 设置句号前缀,Go 代码也显示如果你提供了句号前缀,它也会被裁剪掉。
除了指定域名外,你也可以对指定路径限制 cookie。
1 c := Cookie{ 2 // Defaults to any path on your app, but you can use this 3 // to limit to a specific subdirectory. Eg: 4 Path: "/app/", 5 }
简单说就是,你可以设置路径前缀为 /blah
类似的东西,如果想了解更多这块如何实现的内容,你可以看https://tools.ietf.org/html/rfc6265#section-5.1.4。
为什么不直接用 JWT ?
这个问题没法逃避,这里简单的解释一下。
尽管有很多人可能会告诉你,cookie 可以像 JWT 一样安全。实际上,JWT 和 cookie 解决的都不是一类问题,因为 JWT 可以存在 cookie 中,实质上和提供 header 的使用方式一样。
不管怎样,cookie 可以用于非验证数据,即便在这些案例中,了解适当的安全措施也是有用的。
总结
今天的文章除了阐述如何使用Go
语言安全地传输Cookie
数据外,再次格外强调一遍,编码和加密的不同,从数据可读性上看,两者差不多,但本质上是完全不一样的:
- 编码使用公开可用的方案将数据转换为另一种格式,以便可以轻松地将其反转。
- 加密将数据转换为另一种格式,使得只有特定的个人才能逆转转换。
我们在做数据传输时一定要记住两者的区别,某种意义上,我觉得记住这两点的区别比你学会今天文章里怎么安全传输Cookie
更重要。