81

Go web 开发中的cookie和session

 5 years ago
source link: https://colobu.com/2018/09/28/cookie-and-session-in-go/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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和删除Cookie的示例。

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。

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 实现这个功能。

// 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。

使用起来也很方便:

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,否则会出现内存泄漏。

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK