首先,HTTP协议是无状态(stateless)的。Cookie和Session都是在无状态的HTTP协议上来维护会话状态。
因为HTTP协议是无状态的,每次用户请求到达服务器时,HTTP服务器不知道用户是谁,是否登录过,服务器之所以知道我们登录过,是因为服务器在我们登录时设置了浏览器的Cookie。Session则是借由Cookie实现更高层的服务器与浏览器间的会话。
Cookie实现机制
Cookie 是客户端保存的小型文本文件,内容为一系列键值对。
Cookie是HTTP服务器设置的,并且保存在浏览器中。
用户再访问其他页面时,会在HTTP请求中保存之前的Cookie。
Cookie的具体传递流程:
- 浏览器向URL发起HTTP请求,这个请求可以是GET一个页面,POST一个登录表单等。
- 对应的服务器接受到HTTP请求,并计算应当返回给浏览器的HTTP响应,HTTP响应包括响应头和响应体两个部分。
- 响应头中加入
Set-Cookie
字段,他的值是要设置的Cookie。
在 RFC2109 6.3 Implementation Limits 中提到: UserAgent(浏览器就是一种用户代理)至少应支持300项Cookie, 每项至少应支持到4096字节,每个域名至少支持20项Cookie。 - 浏览器接收到服务器发来的HTTP响应。
- 浏览器收到HTTP响应,发现Set-Cookie字段,并将字段的值保存在硬盘或者内存中。
Set-Cookie字段的值可以是很多项Cookie,每一项都可以指定过期时间Expires。 - 浏览器下次给服务器发送HTTP请求时,会将服务器设置的Cookie附加在HTTP请求的头字段Cookie中。
浏览器可以存储多个域名下的Cookie,但是只发送当前请求的域名曾指定过的Cookie。 - 服务器接收到HTTP请求,发现请求头中有Cookie字段,就知道之前已和这个用户打过招呼了。
总之,服务器通过Set-Cookie响应头字段来指示浏览器保存Cookie, 浏览器通过Cookie请求头字段来告诉服务器之前的状态。 Cookie中包含若干个键值对,每个键值对可以设置过期时间。
Cookie的隐患
Cookie是有隐患的。
第一次服务器验证用户和密码,设置Set-Cookie为authed=true,后返回 200(OK)。
于是浏览器再次发带有Cookie信息的HTTP请求(设置authed=true),服务器会得知用户已登录,于是按照登录的权限去处理这次请求。
但是,假如用HTTP客户端软件(Node.js)来模拟浏览器发请求,明文设置相同的Cookie字段,并设置authed=true,服务器会不会被骗?这种攻击十分容易,因为服务器是会被篡改的。
Cookie防篡改
服务器可以为每个Cookie项生成签名,由于用户篡改Cookie后无法生成对应的签名, 服务器便可以得知用户对Cookie进行了篡改。
- 在服务器中配置一个不为人知的字符串(我们叫它Secret),比如:x$sfz32。
- 当服务器需要设置Cookie时(比如authed=false),不仅设置authed的值为false, 在值的后面进一步设置一个签名,最终设置的Cookie是authed=false|6hTiBl7lVpd1P。
- 签名6hTiBl7lVpd1P是这样生成的:Hash('x$sfz32'+'false')。 要设置的值与Secret相加再取哈希。
- 用户收到HTTP响应并发现头字段Set-Cookie: authed=false|6hTiBl7lVpd1P。
- 用户在发送HTTP请求时,篡改了authed值,设置头字段Cookie: authed=true|???。 因为用户不知道Secret,无法生成签名,只能随便填一个。
- 服务器收到HTTP请求,发现Cookie: authed=true|???。服务器开始进行校验: Hash('true'+'x$sfz32'),便会发现用户提供的签名不正确。
通过给Cookie添加签名,使得服务器得以知道Cookie被篡改。
但是,Cookie是明文传输的, 只要服务器设置过一次authed=true|xxxx就知道true的签名是xxxx了, 以后就可以用这个签名来欺骗服务器了。因此Cookie中最好不要放敏感数据。 一般来讲Cookie中只会放一个Session Id,而Session存储在服务器端。
Session实现机制
Session一般用来存储重要数据,一般储存在HTTP服务器内存中,或者内存数据库中(redis),对于重量级应用甚至可以储存在数据库中。
下面以储存在redis中为例,考察如何验证用户登陆状态的问题。
- 用户提交账户和密码的表单,发送POST请求。
- 服务器验证账户和密码,若正确则把当前用户名储存到redis中,在redis中生成对应的id。
- 服务器设置Cookie为SessionID并发送HTTP响应,也同样设置签名。
- 用户收到HTTP响应后,便看不到敏感数据了,然后发送Session Cookie字段的请求给服务器。
- 服务器收到请求后,同样进行Session Cookie的防篡改验证,若通过验证,则根据ID从redis中取出用户对象,查看该对象的状态并继续执行业务逻辑。