Cookie / Session安全机制

原文:https://harttle.land/2015/08/10/cookie-session.html

Cookie 和 Session是为了在无状态的HTTP协议上维护会话状态,使得服务器知道当前是和哪个用户打交道。

Cookie和Session

因为HTTP协议是无状态的,即每次请求到达服务器端的时候,服务端是无法知道当前用户的身份、是否登录等。现在之所以服务器知道用户的身份是因为服务器在用户第一次请求的时候设置了Cookie。而Session是借助Cookie而实现的在服务器和浏览器直接的会话。

image.png

Cookie的实现机制

Cookie是由客户端保持的一个文本文件,其内容是一系列的键值对。Cookie是由服务器端设置,保存在浏览器端的。在用户访问服务器的时候,会在HTTP请求头中携带当前的Cookie。下面我们看一下Cookie的工作流程:

  • 用户访问某个页面,浏览器向服务器发起请求
  • 服务器端获取浏览器的请求,并基于浏览器响应
  • 服务器端在响应头中添加Cookie:set-cookie:,其值为要设置的cookie的键值对,如:path、expires、domain等。

RFC2109 6.3 Implementation Limits 中提到: UserAgent(浏览器就是一种用户代理)至少应支持300项Cookie, 每项至少应支持到4096字节,每个域名至少支持20项Cookie。

  • 浏览器接收到服务器的响应,并解析。
  • 当遇到set-cookie时,就会将其值保存在内存或者磁盘中。
  • 浏览器下次给该服务器发送HTTP请求时, 会将服务器设置的Cookie附加在HTTP请求的头字段Cookie中。

浏览器可以存储多个域名下的Cookie,但只发送当前请求的域名曾经指定的Cookie, 这个域名也可以在Set-Cookie字段中指定)。

  • 服务器接收到浏览器的请求,发现请求头中存在cookie字段的时候,就会确定和该用户已有过交互。
  • 浏览器会自动清除过期的cookie。

总之,服务器通过Set-Cookie响应头字段来指示浏览器保存Cookie, 浏览器通过Cookie请求头字段来告诉服务器之前的状态。 Cookie中包含若干个键值对,每个键值对可以设置过期时间。

Cookie的安全隐患

我们知道可以发送HTTP请求的不只是浏览器,很多HTTP客户端软件(包括curl、Node.js)都可以发送任意的HTTP请求,可以设置任何头字段。 假如我们直接设置Cookie字段并发送该HTTP请求, 服务器岂不是被欺骗了?这种攻击非常容易,Cookie是可以被篡改的!
**

Cookie的防篡改机制

服务器可以为每个Cookie项生成签名,由于用户篡改Cookie后无法生成对应的签名, 服务器便可以得知用户对Cookie进行了篡改。一个简单的校验过程可能是这样的:

  1. 在服务器中配置一个不为人知的字符串(我们叫它Secret),比如:x$sfz32
  2. 当服务器需要设置Cookie时(比如authed=false),不仅设置authed的值为false, 在值的后面进一步设置一个签名,最终设置的Cookie是authed=false|6hTiBl7lVpd1P
  3. 签名6hTiBl7lVpd1P是这样生成的:Hash('x$sfz32'+'false')。 要设置的值与Secret相加再取哈希。
  4. 用户收到HTTP响应并发现头字段Set-Cookie: authed=false|6hTiBl7lVpd1P
  5. 用户在发送HTTP请求时,篡改了authed值,设置头字段Cookie: authed=true|???。 因为用户不知道Secret,无法生成签名,只能随便填一个。
  6. 服务器收到HTTP请求,发现Cookie: authed=true|???。服务器开始进行校验: Hash('true'+'x$sfz32'),便会发现用户提供的签名不正确。

通过给Cookie添加签名,使得服务器得以知道Cookie被篡改。然而故事并未结束。
因为Cookie是明文传输的, 只要服务器设置过一次authed=true|xxxx我不就知道true的签名是xxxx了么, 以后就可以用这个签名来欺骗服务器了。因此Cookie中最好不要放敏感数据。 一般来讲Cookie中只会放一个Session Id,而Session存储在服务器端。

Session实现机制

Session 是存储在服务器端的,避免了在客户端Cookie中存储敏感数据。 Session 可以存储在HTTP服务器的内存中,也可以存在内存数据库(如redis)中, 对于重量级的应用甚至可以存储在数据库中。
我们以存储在redis中的Session为例,还是考察如何验证用户登录状态的问题。

  1. 用户提交包含用户名和密码的表单,发送HTTP请求。

  2. 服务器验证用户发来的用户名密码。

  3. 如果正确则把当前用户名(通常是用户对象)存储到redis中,并生成它在redis中的ID。
    这个ID称为Session ID,通过Session ID可以从Redis中取出对应的用户对象, 敏感数据(比如authed=true)都存储在这个用户对象中。

  4. 设置Cookie为sessionId=xxxxxx|checksum并发送HTTP响应, 仍然为每一项Cookie都设置签名。

  5. 用户收到HTTP响应后,便看不到任何敏感数据了。在此后的请求中发送该Cookie给服务器。

  6. 服务器收到此后的HTTP请求后,发现Cookie中有SessionID,进行放篡改验证。

  7. 如果通过了验证,根据该ID从Redis中取出对应的用户对象, 查看该对象的状态并继续执行业务逻辑。

Web应用框架都会实现上述过程,在Web应用中可以直接获得当前用户。 相当于在HTTP协议之上,通过Cookie实现了持久的会话。这个会话便称为Session。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!