简析 Cookie,Session 和 Token(JWT)

前言

在互联网的世界中,传递信息使用的协议是 HTTP,其是一种无状态的协议机制,也即前一次的请求与下一次的请求均为独立的请求,两者不存在任何联系。但随着带宽的发展与交互式 Web 应用的兴起,越来越多的系统需要记录用户的相关活动状态,于是 cookie,session 以及 token 等技术便应运而生。

Cookie

早期的 Web 应用面临的最大问题之一便是如何维持状态。当时,通常的解决办法就是在请求的页面中插入一个 token(比如在 form 表单中插入一个携带 token 的隐藏字段),或者将 token 放置于 URL 的 query 字符串中传递给请求页面。这两种方法都需要进行手动操作,而且极易出错。于是,当时网景公司的一名员工 Lou Montulli 就发明了 cookie 这种方法来实现 Web 通讯中的状态维持,此后网景公司在它的第一个浏览器版本中就开始支持 cookie,直到现在,所有的主流 Web 浏览器都支持 cookie。

简单来说,cookie 就是客户端存储的一小段文本文件,其格式为:

// response
Set-Cookie: name=value[; expires=date][; domain=domain][; path=path][; secure]
// request
Cookie: name=value [; name2=value2] ...

每次服务器下发 cookie 时,都会将 cookie 的相关内容存放在响应头部的 Set-Cookie 中。客户端接收到该 cookie 时,需要搜集并记录下来,然后再后续的请求中,将相关的 cookie 作为请求头 Cookie 的内容,发送给服务器(浏览器会自动帮我们做这些事)。

cookie

:cookie 有两种不同的版本:cookies版本0(使用 Set-Cookie 和 Cookie 首部)和 cookies版本1(使用 Set-Cookie2 和 Cookie2 首部),版本1 是对 版本0 的扩展,但还未得到完全的支持。因此本文讲述的均为 cookies版本0。

cookie 中几个比较重要的字段讲解:

  • value:通常使用 key=value 键值对的格式设置 cookie 键值:Set-Cookie: name=Whyn

  • domain:设置 cookie 的域名,当访问相关符合该域名的页面时,浏览器会自动发送这些 cookie:Set-Cookie: name=Nicholas; domain=whyn.com
    :默认情况下,domain 会被设置为创建该 cookie 的页面所在的域名。

  • path:当请求资源的 URL 路径满足 path 指定的路径时,会将该 cookie 进行发送:Set-Cookie:name=Whyn;path=/blog,当访问的页面为 /blog/blogxxx/blog/article1 等时(也即路径以 /blog 开头),都满足 path 的匹配,故会将该 cookie 进行发送。

  • expires:设置 cookie 过期时间,当 cookie 过期后,浏览器会自动删除该 cookie:Set-Cookie: name=Whyn; expires=Sat, 02 May 2019 23:38:25 GMT
    :通过对 expires 的设置,可以将 cookie 分为 会话 cookie持久 cookie:如果未设置过期时间,则 cookie 默认的生命周期仅存于当前会话中,当关闭浏览器后,cookie 就会失效(会话 cookie)。如果设置了 cookie 过期时间,则该 cookie 会被存储到硬盘上,我们称之为 持久 cookie
    我们经常在某些网页的登录页面中,都能看到登录表单携带一个类似 remember_me 这样的复选框,其实这个选项的作用就是通知服务器是否设置 expires 选项,当勾选上时,服务器就会下发一个设置了 expires 的 cookie,浏览器接收到该 cookie 后,查看到其设置了过期时间,就会将该 cookie 持久化到硬盘中,这样,即使浏览器关闭,也不会影响到我们的登录状态。

  • secure:只有当一个请求通过 SSL 或 HTTPS 创建时,包含 secure 选项的 cookie 才能被发送至服务器:Set-Cookie: name=Whyn; secure
    :默认情况下,在 HTTPS 连接上传输的 cookie 都会被自动添加上 secure 选项。

  • HttpOnly:设置以禁用 js 的 Document.cookie API, 防止 XSS:Set-Cookie: name=Whyn; HttpOnly

cookie 只存储于客户端中,简单来讲,cookie 记录了用户的相关状态(比如用户标识等等),举个简单粗暴的例子:我们完全可以在 cookie 中记录当前登录的用户名和密码(已加密),如下所示:

Set-Cookie: name=Whyn;
Set-Cookie: passwd=diabqpaNaisdfqekf;

:更多Set-Cookie设置信息,可查看:MDN - Set-Cookie

当后续请求服务器时,将这些 cookie 发送给服务器,服务器接收到该 cookie 时,直接根据 name 字段查找数据库,找到用户后,取出其密码与该 cookie 的 passwd 字段对比,相同则验证通过,这样就做到了登录状态的维持(当然,实际中一般不会这样进行登录状态维持,安全性不过关,且每次请求都进行数据库查询也不现实)。

Session

session:中文译为 会话。主要用于服务端对客户端进行身份识别。客户端首次请求服务器时,服务器会在本地存储相关该客户端信息,并将一个标识该客户端身份的 sessionId 下发给客户端(通常使用 cookie 进行下发),后续客户端的其他请求只要携带该 sessionId,服务器就能获取得到该客户端的相关信息,这样就实现了状态维持。

session

cookie 与 session 的区别在于:cookie 是客户端用于进行状态存储,session 是服务器端用于状态存储。对于 cookie 来说,其存储在客户端,且是用户可见并且可以随意修改,因此其安全性相对较低;而 session 存放位置在服务器上,用户无法直接接触,因此其安全性会相对更高。

session 的实现通常都会借助于 cookie 机制,因为 session 需要把身份标识 sessionId 下发给客户端,通常这个下发动作就是使用 cookie 进行实现。

基于 session 验证方式的存在的一些弊端

  • 占用服务器内存资源:由于 session 存储于服务器端,随着越来越多用户的请求登录,服务器端的 session 记录表会越来越大,对服务器内存资源的开销逐渐增大;

  • 扩展性弱:session 机制在负载均衡下,无法很好地进行工作。当服务器资源不足时,无法很好地进行水平扩展;

  • CORS(跨域资源共享):当我们需要让数据跨多台移动设备上使用时,跨域资源共享会是一个让人头疼的问题。在使用 Ajax 抓取另一个域的资源时,就会出现禁止请求的情况;

  • CSRF(跨站请求伪造):用户在访问银行网站时,他们很容易受到跨站请求伪造的攻击,并且能够被利用其访问其他网站。

Token

通过使用 session 和 cookie,其实我们就可以实现了对客户端的请求的状态维持,但 session 存在一个比较重大的缺陷:如果 Web 服务器使用了负载均衡,那么就可能会出现 session 丢失问题,导致状态维持失效。

session 失效问题最主要的原因就是状态维持列表信息保存在服务器上,那么如果能让服务器不保持相关状态列表(即服务器无状态),那该问题就不复存在,且由于服务器是无状态的,因此,水平扩展便能很轻松地进行实现。由此,token 机制应运而生。

token 机制实现服务器无状态的原理就是将客户端状态列表信息存储到客户端中

token 的实现原理大致为:

  1. 比如,现在某个客户端进行登录请求,则其会将用户名和密码发送给服务器,服务器接收到并且验证通过后,就会对数据进行签名(比如使用 HMAC-SHA256 算法,加上一个服务器的密钥对用户数据(比如用户名、用户角色、过期时间...)进行签名),然后把这个签名和数据一起作为 token 下发给到客户端(放置在响应头 Authorization)。

  2. 客户端接收到 token 后,进行存储(通常存储在 localStorage 中,不推荐存放在 cookie)。

  3. 客户端进行请求时,会将该 token 同时发送给服务器(将 token 放置于请求头 Authorization 中)。服务器接收到该 token 后,使用相同的密钥对 token 中的数据进行签名,将签名后的结果与 token 中的签名进行比对,相同则验证通过。
    :由于 token 主要存在两部分内容:用户数据 + 签名。由于签名使用的密钥存储在服务器中,所以密钥无法被篡改,因此签名是安全的。客户端唯一能篡改的就是用户数据内容,但是当数据被篡改后,服务器验证签名就会失败,从而保证用户数据的安全性。

简单理解如下:

生成 token:
加密函数A(数据A + 密钥A) => 签名A
token = 数据A + 签名A

验证:
token => 数据A, 签名A
加密函数A(数据A + 密钥A) => 签名B
签名B == 签名A ? 验证成功 : 验证失败

JSON Web Token

上面我们讲了 token 的格式与实现原理,但是在实际使用中,我们无需自己实现上述 token 格式内容,因为已经有现成的标准的 token 实现格式:JSON Web Token

JSON Web Token:简称 JWT,是一个具备紧凑和自描述安全传输的 JSON 对象。JWT 携带的信息由于进行了数字签名,因此是可以被验证与信任的。

一个 JWT 由三部分组成:头部载荷签名,这三部分都是 JSON 格式,且由 . 进行隔离,因此,一个 JWT 字符串格式应当看起来为:xxxx.yyyyy.zzzzz

  • 头部(Header):头部用于描述关于该 JWT 的最基本信息,例如其类型以及签名所使用的算法等:

    {
        "typ": "JWT",
        "alg" : "HS256"
    }
    

    然后,将该头部 JSON 对象进行 Base64 编码作为 JWT 的第一部分。

  • 载荷(Payload):载荷部分主要包含各种声明(即各种实体状态和一些额外元数据)。主要有三种类型声明:保留声明(reserved),公有声明(public)和 私有声明(private):

    • 保留声明(reserved):是一些列预先定义的声明,不强制但是推荐使用,其主要提供了 7 默认声明字段:

      • iss(issuer):JWT token 签发者
      • exp(expiration time):过期时间
      • sub(subject):主体,通常指代用户名
      • aud(audience):接收 JWT token 的一方
      • nbf(Not Before):设置不可用时间段,在该指定时间之前,该 JWT token 都是不可用的
      • iat(Issued At):JWT token 签发时间
      • jti(JWT ID):JWT token 唯一身份标识,主要用来作为一次性 token,从而回避重放攻击

      可以看到,预定义的声明都只是使用三个字符,这其实就体现了 JWT 具备紧凑的格式。

    • 公有声明(public):公有声明可随意定义,一般用于添加用户相关信息。
      :不要添加密码等敏感信息,因为该部分相当于明文存储,客户端可以进行解密。

    • 私有声明(private):由签发者和消费者所共同定义的私有声明。
      :不要添加密码等敏感信息,因为该部分相当于明文存储,客户端可以进行解密。

    {
        "iss": "Whyn",
        "iat": 1441593502,
        "exp": 1441594722,
        "aud": "www.example.com",
        "sub": "jrocket@example.com",
        "from_user": "B",
        "target_user": "A"
    }
    

    然后将该载荷部分进行 Base64 编码作为 JWT 的第二部分内容。

  • 签名(Signature):即对 JWT 的头部和载荷部分进行签名,需要的数据为:头部内容(已 Base64 编码),载荷(已 Base64 编码),密钥(secret)和 加密算法(algorithm)。
    比如,使用 HMAC SHA256 加密算法进行加密,则生成签名的格式如下:

    signature = HMACSHA256(
                  base64UrlEncode(header) + "." +
                  base64UrlEncode(payload),
                  secret) // or base64UrlEncode(secret)
    

    最后,将头部,载荷和签名结合到一起生成最终的 jwt(即 token):

    base64UrlEncode(header) + "." +
    base64UrlEncode(payload) + "." +
    base64UrlEncode(signature) 
    

:由于 Header 和 Payload 只是简单进行了 Base64 编码,因此客户端可以解码为明文,故不要将私密信息放置到 Header 和 Payload 中。

总结

Cookie,Session 和 Token 都是为了能够实现客户端与服务器间的状态维持。其中:

  • Cookie 存储于客户端,主要用于一些简短信息的存储;
  • Session 存储于服务器端,维护了客户端的相关信息列表,并且通过一个唯一的身份标识 sessionId 识别各个客户端。
  • Token 存储于客户端,其结构紧凑且自带验证功能,很好地解决了 Session 无法水平扩展服务器的局限,实现了服务器无状态但通信具备却状态维持功能。通常使用 JWT 作为 Token 的具体实现方式。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容