Cookie和Session剖析

[TOC]

为什么要有Cookie?

HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来

保存状态信息。

Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带上

用途:

· 会话状态管理(如用户登录状态、购物车)

· 浏览器行为跟踪(如跟踪分析用户行为等)

Cookie创建过程?

服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中

1
2
3
4
5
HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry
[page content]

客户端之后对同一个服务器发送请求时,会从浏览器中取出 Cookie 信息并通过 Cookie 请求首部字段发送给服务
器。

1
2
3
GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry

Cookie分类

会话期 Cookie:浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。

持久性 Cookie:指定过期时间(Expires)或有效期(max-age)之后就成为了持久性的 Cookie

Session?

除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。

session 认证流程:

  • 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session
  • 请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器
  • 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名
  • 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。

SessionID 是连接 Cookie 和 Session 的一道桥梁

在对安全性要求极高的场景下,例如转账等操作,除了使用 Session管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。

Session和Cookie区别?

  • Cookie 只能存储 ASCII 码字符串,而 Session 则可以存储任何类型的数据,因此在考虑数据复杂性时首选
    Session;
  • Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加
    密,然后在服务器进行解密;
  • 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信
    息都存储到 Session 中。

Cookie和Session的选择?

  1. Cookie 只能存储 ASCII 码字符串,而 Session 则可以存储任何类型的数据,因此在考虑数据复杂性时首选Session;

  2. Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密;

  3. 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。

Cookie的不可跨域名性?

一个cookie只能用于一个域名(有效的二级域名,比如 shop.com),不能够发给其它的域名。Cookie跨域指的是允许不同的二级域名间共享

Token?

  1. 把用户的用户名和密码发到后端
  2. 后端进行校验,校验成功会生成token, 把token发送给客户端
  3. 客户端自己保存token, 再次请求就要在Http协议的请求头中带着token去访问服务端,和在服务端保存的token信息进行比对校验。
  • 每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里
  • 基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库
  • token 完全由应用管理,所以它可以避开同源策略

img

Token 和 Session 的区别

  • Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而 Token 是令牌访问资源接口(API)时所需要的资源凭证。Token 使服务端无状态化,不会存储会话信息。

  • Session 和 Token 并不矛盾,作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击,而 Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。

  • 所谓 Session 认证只是简单的把 User 信息存储到 Session 里,因为 SessionID 的不可预测性,暂且认为是安全的。而 Token ,如果指的是 OAuth Token 或类似的机制的话,提供的是 认证 和 授权 ,认证是针对用户,授权是针对 App 。

JWT

JWT 的 Header 通常包含两个字段,分别是:typ(type) 和 alg(algorithm)。

  • typ:token的类型,这里固定为 JWT
  • alg:使用的 hash 算法,例如:HMAC SHA256 或者 RSA

一个简单的例子:

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

我们对他进行编码后是:

1
2
>>> base64.b64encode(json.dumps({"alg":"HS256","typ":"JWT"}))
'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9'
Payload

JWT 中的 Payload 其实就是真实存储我们需要传递的信息的部分,例如正常我们会存储些用户 ID、用户名之类的。此外,还包含一些例如发布人、过期日期等的元数据。

但是,这部分和 Header 部分不一样的地方在于这个地方可以加密,而不是简单得直接进行 BASE64 编码。但是这里我为了解释方便就直接使用 BASE64 编码,需要注意的是,这里的 BASE64 编码稍微有点不一样,切确得说应该是 Base64UrlEncoder,和 Base64 编码的区别在于会忽略最后的 padding(=号),然后 ‘-‘ 会被替换成’_’。

举个例子,例如我们的 Payload 是:

1
{"user_id":"zhangsan"}

那么直接 Base64 的话应该是:

1
2
>>> base64.urlsafe_b64encode('{"user_id":"zhangsan"}')
'eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ=='

然后去掉 = 号,最后应该是:

1
'eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ'
Signature

Signature 部分其实就是对我们前面的 Header 和 Payload 部分进行签名,保证 Token 在传输的过程中没有被篡改或者损坏,签名的算法也很简单,但是,为了加密,所以除了 Header 和 Payload 之外,还多了一个密钥字段,完整算法为:

1
2
3
4
5
    Signature = HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
复制代码

还是以前面的例子为例,

1
2
3
    base64UrlEncode(header)  =》 eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9
base64UrlEncode(payload) =》 eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ
复制代码

secret 就设为:”secret”, 那最后出来的签名应该是:

1
2
3
4
5
6
7
8
    >>> import hmac
>>> import hashlib
>>> import base64
>>> dig = hmac.new('secret', >>> msg="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ",
digestmod=
>>> base64.b64encode(dig.digest())
'ec7IVPU-ePtbdkb85IRnK4t4nUVvF2bBf8fGhJmEwSs='
复制代码

将上面三个部分组装起来就组成了我们的 JWT token了,所以我们的

1
2
    {'user_id': 'zhangsan'}
复制代码

的 token 就是:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ.ec7IVPU-ePtbdkb85IRnK4t4nUVvF2bBf8fGhJmEwSs
  1. 在头部信息中声明加密算法和常量, 然后把header使用json转化为字符串

  2. 在载荷中声明用户信息,同时还有一些其他的内容;再次使用json 把载荷部分进行转化,转化为字符串

  3. 使用在header中声明的加密算法和每个项目随机生成的secret来进行加密, 把第一步分字符串和第二部分的字符串进行加密, 生成新的字符串。词字符串是独一无二的。

  4. 解密的时候,只要客户端带着JWT来发起请求,服务端就直接使用secret进行解密。

JWT与token的联系

Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。

JWT: 将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。