一、Session 、Cookie 简介
cookie
的出现是因为 HTTP
是无状态的一种协议(cookie
是 HTTP
协议的一部分),换句话说,服务器记不住你,可能你每刷新一次网页,就要重新输入一次账号密码进行登录。这显然是让人无法接受的,cookie
的作用就好比服务器给你贴个标签,然后你每次向服务器再发请求时,服务器就能够 cookie
认出你。
抽象地概括一下:一个 cookie 可以认为是一个「变量」,形如 name=value
,存储在浏览器;一个 session 可以理解为一种数据结构,多数情况是「映射」(键值对),存储在服务器上。
注意:cookie
可以是一个变量,也可以是一组键值对,一般来说服务器上可能会设置多个cookie
,这时候我们就说这是一组键值对。
cookie
可以在服务器端通过 HTTP
的 SetCookie
字段设置 cookie
,比如我用 Go 语言写的一个简单服务:
func cookie(w http.ResponseWriter, r *http.Request) {
// 设置了两个 cookie
http.SetCookie(w, &http.Cookie{
Name: "name1",
Value: "value1",
})
http.SetCookie(w, &http.Cookie{
Name: "name2",
Value: "value2",
})
// 将字符串写入网页
fmt.Fprintln(w, "页面内容")
}
当浏览器访问对应网址时,通过浏览器的开发者工具查看此次 HTTP
通信的细节,可以看见服务器的回应发出了两次 SetCookie
命令:
在这之后,浏览器的请求中的 Cookie
字段就带上了这两个 cookie
:
Cookie
的作用其实就是这么简单,无非就是服务器给每个客户端(浏览器)打的标签,方便服务器辨认而已。当然,HTTP
还有很多参数可以设置 Cookie
,比如过期时间,或者让某个 Cookie
只有某个特定路径才能使用等等。
一般来说Cookie不会直接被拿去使用,服务器光靠Cookie进行验证过于简单、不安全。所以基本都需要服务器通过数据签名的方式将数据与服务器密令拼接在一起来验证。密令一般是伪造不了的。
但问题是,我们也知道现在的很多网站功能很复杂,而且涉及很多的数据交互,比如说电商网站的购物车功能,信息量大,而且结构也比较复杂,无法通过简单的 cookie
机制传递这么多信息,而且要知道 Cookie
字段是存储在 HTTP header
中的,就算能够承载这些信息,也会消耗很多的带宽,比较消耗网络资源。
Session
就可以配合 Cookie
解决这一问题,Session
将数据保存在服务端,通过Cookie
传过来的sessionID
来去在服务器中查询对应的数据。sessionID
一般是服务端随机生成的,那来与数据进行关联。具体流程是:服务端生成sessionID
后传给客户端的Cookie
保存起来,下次请求的时候客户端通过Cookie
传对应的sessionID
去服务端查找相关联的数据。
比如说一个 Cookie
存储这样一个变量 sessionID=xxxx
,仅仅把这一个 Cookie
传给服务器,然后服务器通过这个 ID 找到对应的 Session
,这个 Session
是一个数据结构,里面存储着该用户的购物车等详细信息,服务器可以通过这些信息返回该用户的定制化网页,有效解决了追踪用户的问题。
注:这一个过程是客户端先进行登录后,传给服务端登录成功的信息,服务端再给一个唯一能便是这个客户端的Cookie
给客户端,这个Cookie
里就包含了sessionID
,这时候如果客户端要什么别的信息,就可以通过Cookie
内的这个sessionID
传给服务端去判断是否存在这个sessionID
,判断这个sessionID
是否合法,如果存在,服务端再返回客户端所需要的相应数据到前端。
session
是一个数据结构,由网站的开发者设计,所以可以承载各种数据,只要客户端的 cookie
传来一个唯一的 session ID
,服务器就可以找到对应的 session
,认出这个客户。进而通过 session
去获取对应的结构信息。
注:Session
其实本质上还是Cookie
,与Cookie
不同的是Session
只给一个sessionID
到服务端作为唯一身份的辨识,会话数据基本都存储在服务端,由服务端自行管理。
当然,由于 Session
的会话数据全部存储在服务器中,肯定会消耗服务器的资源,所以 Session
一般都会有一个过期时间,服务器一般会定期检查并删除过期的 session
,如果后来该用户再次访问服务器,可能就会面临重新登录等等措施,然后服务器新建一个 Session
,将 session ID
通过 cookie
的形式传送给客户端。
那么,我们知道 Cookie 和 Session 的原理,有什么切实的好处呢?除了应对面试,我给你说一个鸡贼的用处,就是可以白嫖某些服务。
有些网站,你第一次使用它的服务,它直接免费让你试用,但是用一次之后,就让你登录然后付费继续使用该服务。而且你发现网站似乎通过某些手段记住了你的电脑,除非你换个电脑或者换个浏览器才能再白嫖一次。
那么问题来了,你试用的时候没有登录,网站服务器是怎么记住你的呢?这就很显然了,服务器一定是给你的浏览器打了 Cookie,后台建立了对应的 Session 记录你的状态。你的浏览器在每次访问该网站的时候都会听话地带着 Cookie,服务器一查 session 就知道这个浏览器已经免费使用过了,得让它登录付费,不能让它继续白嫖了。
那如果我不让浏览器发送 Cookie,每次都伪装成一个第一次来试用的小萌新,不就可以不断白嫖了么?浏览器会把网站的 Cookie 以文件的形式存在某些地方(不同的浏览器配置不同),你把他们找到然后删除就行了。但是对于 Firefox 和 Chrome 浏览器,有很多插件可以直接编辑 Cookie,比如我的 Chrome 浏览器就用的一款叫做
EditThisCookie
的插件。这类插件可以读取浏览器在当前网页的 Cookie,点开插件可以任意编辑和删除 Cookie。当然,偶尔白嫖一两次还行,不鼓励高频率白嫖,想常用还是掏钱吧,否则网站赚不到钱,就只能取消免费试用这个机制了。
(简单来说,就是将访问网站的 Cookie 给修改掉,每次都当一个第一次进入网站的新用户,这样就能实现白嫖的效果。)
二、Session 的实现
Session
的原理不难,但是具体实现它可是很有技巧的,一般需要三个组件配合完成,它们分别是 Manager
、Provider
和 Session
三个类(接口)。
1、浏览器通过 HTTP 协议向服务器请求路径 /content
的网页资源,对应路径上有一个 Handler
函数接收请求,解析 HTTP header
中的 cookie
,得到其中存储的 sessionID
,然后把这个 ID 发给 Manager
。
2、Manager
充当一个 session
管理器的角色,主要存储一些配置信息,比如 session
的存活时间,cookie
的名字等等。而所有的 session
存在 Manager
内部的一个 Provider
中。所以 Manager
会把 sid
(sessionID
)传递给 Provider
,让它去找这个 ID 对应的具体是哪个 session
。
3、Provider
就是一个容器,最常见的应该就是一个散列表,将每个 sid
和对应的 session 一一映射起来。收到 Manager
传递的 sid
之后,它就找到 sid
对应的 session
结构,也就是前面开发中所写过的Session
结构,然后返回它。
4、Session
中存储着用户的具体信息,由 Handler
函数中的逻辑拿出这些信息,生成该用户的 HTML 网页,返回给客户端。
三、Session原理
那么你也许会问,为什么搞这么麻烦,直接在 Handler 函数中搞一个哈希表,然后存储 sid
和 Session
结构的映射不就完事儿了?
这就是设计层面的技巧了,下面就来说说,为什么分成 Manager
、Provider
和 Session
。
Session
先从最底层的 Session
说。既然 Session
就是键值对,为啥不直接用哈希表,而是要抽象出这么一个数据结构呢?
第一,因为 Session
结构可能不止存储了一个哈希表,还可以存储一些辅助数据,比如 sid
,访问次数,过期时间或者最后一次的访问时间,这样便于实现想 LRU
、LFU
这样的算法。
第二,因为 Session
可以有不同的存储方式。如果用编程语言内置的哈希表,那么 Session
数据就是存储在内存中,如果数据量大,很容易造成程序崩溃,而且一旦程序结束,所有 Session
数据都会丢失。所以可以有很多种 Session
的存储方式,比如存入缓存数据库 Redis
,或者存入 MySQL
等等。
因此,Session
结构提供一层抽象,屏蔽不同存储方式的差异,只要提供一组通用接口操纵键值对:
type Session interface {
// 设置键值对
Set(key, val interface{})
// 获取 key 对应的值
Get(key interface{}) interface{}
// 删除键 key
Delete(key interface{})
}
Provider
再说 Provider
为啥要抽象出来。我们上面那个图的 Provider
就是一个散列表,保存 sid
到 Session
的映射,但是实际中肯定会更加复杂。我们不是要时不时删除一些 session
吗,除了设置存活时间之外,还可以采用一些其他策略,比如 LRU 缓存淘汰算法
,这样就需要 Provider
内部使用哈希链表这种数据结构来存储 session
。
因此,Provider
作为一个容器,就是要屏蔽算法细节,以合理的数据结构和算法组织 sid
和 Session
的映射关系,只需要实现下面这几个方法实现对 Session
的增删查改:
type Provider interface {
// 新增并返回一个 session
SessionCreate(sid string) (Session, error)
// 删除一个 session
SessionDestroy(sid string)
// 查找一个 session
SessionRead(sid string) (Session, error)
// 修改一个session
SessionUpdate(sid string)
// 通过类似 LRU 的算法回收过期的 session
SessionGC(maxLifeTime int64)
}
Manager
最后说 Manager
,大部分具体工作都委托给 Session
和 Provider
承担了,Manager
主要就是一个参数集合,比如 Session
的存活时间,清理过期 Session
的策略,以及 Session
的可用存储方式。Manager
屏蔽了操作的具体细节,我们可以通过 Manager
灵活地配置 Session
机制。
综上,session
机制分成几部分的最主要原因就是解耦,实现定制化。我在 Github
上看过几个 Go 语言实现的 Session
服务,源码都很简单,有兴趣的朋友可以学习学习:
https://github.com/alexedwards/scsopen in new window
https://github.com/astaxie/build-web-application-with-golang
后日谈:
上面的说法都是基于网页端的。但是在互联网里,除了网页还有app和小程序等,这些客户端的网络请求接口默认都是没有Cookie
的,那该咋进行客户端验证呢?
我们将客户端程序的sessionID
在存储系统里保存起来,然后换个名字叫Token
。Token
和Session
一样,这说明Token
的会话数据也是储存在服务端中,只不过少了Cookie
机制,Token
需要自己维护,而不是服务端维护。这时候客户端请求不再使用Cookie
字段,而是使用Authorization
字段。
另外还有问题,我们通过本文的说明了解到Session
和Token
的会话数据都在服务端中。如果服务端是单机存储这些数据就还好。但如果是分布式服务器存储会话数据,那么就会出现有的服务器中没有客户端对应的会话数据导致客户端辨识失败。
要解决这个问题就需要架设一个中心化存储服务(例如Redis
),将会话数据存储到里面保证服务器之间的数据一致性。但这又会引发中心化依赖问题,假使Redis
宕机了,那么所有的会话数据都会变得不可用。另外大量查询查中心化服务器也会导致性能瓶颈。在边缘化服务器中,也没办法使用中心化服务。所以还是希望会话数据由客户端进行存储保管。
那这样可以直接把cookie
的数据搬过来作为Token
,Cookie
的加密签名算法由服务端而定,但这样的加密算法又容易有漏洞,不安全。于是就有了JWT
标准帮忙规范加密,JWT
中有三部分,第三部分签名的原理就和Cookie
的签名原理很相似。里面的三个部分都组合在一起构成了一定程度的加密,但加密始终是由服务端人员进行设计的,容易被解密,所以用JWT
辨识客户端还是尽量少传输敏感信息。
这里关于Cookie
、Session
、JWT
的问题在《Cookie和Session与JWT的区别》一文中讲的比较清楚,可以去看看。
评论(0)