Cookie,Session,Token

Cookie,Session,Token

第一节 Web认证技术的演变

  服务器接收请求时需要标识用户身份,当然首先可以想到IP地址可以作为唯一标识符,即某个时间段内来自同一IP地址的所有请求一定属于相同客户端。但是网络地址转换(NAT)并不可靠,比如大学校园或大型企业,可能有数千人使用相同的IP地址,其真实IP则隐藏在NAT路由之后。我们需要一个维护用户状态的机制,而HTTP协议本身是无状态的,所以所有的HTTP服务器都普遍采用了HTTP会话的概念。

  早期的网站功能简单,访问量小,只要通过HTTP请求访问一个个资源就满足了需求。但随着交互式网站的发展,此时Web需要记录用户的状态等信息来维护会话,而HTTP是无状态的,所以就开始使用sessionid来标记用户请求,用来当作密匙区分用户。

  sessionid的作用使其需要双边保存,随着访问用户的迅速增加,服务器端很难去维护动辄数十万百万的sessionid,所以就极大的限制了服务器的性能。一个小小的sessionid就引出了集群,负载均衡,主从备份等问题。所以如果可以从根源上替代sessionid,用好的设计来减少服务器的维护工作就变得尤为重要。

  既然sessionid是为了验证用户身份,那么可不可以只让用户保存,也能来验证其合法性呢?token即令牌便是由此而来,就像古时君王赐予手下主将的虎符,当用户首次登陆验证后,系统就赐予用户token,以后用户就可以根据token来表明其身份,而不用每次都向系统进行验证。

  因为Token的无状态设计,所以解决了Session机制导致的问题,但其也受限于无状态的设计,对于一些用户信息的管理必须要依赖于有状态时就显得不是那么适用了。


2.1 什么是Cookie?

  Cookie就是指浏览器中能持久存储的某种数据,由服务器生成发送给浏览器,浏览器以KV映射的形式保存cookie到指定目录下,在以后的请求中会一起把这部分数据发送给服务器。因为cookie保留在客户端,所以如今流行的浏览器对cookie加了诸多安全限制。

2.2 作用

Cookie主要用于以下三个方面:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等)

  HTTP Cookie 是服务器发送到用户浏览器并保存在用户本地的一小块数据,它会在浏览器下次向同一服务器再发送请求时被携带。通常 HTTP Cookie 用于告知服务端两个请求是否来自同一浏览器,如保存用户的登录状态。Cookie使基于无状态的HTTP协议记录稳定的状态信息成为可能。

  Cookie是 HTTP request header ,包含由服务器通过 Set-Cookie header 发送并存储的HTTP cookies。如果浏览器设置禁用Cookie,此header会被移除。

  过去Cookie曾一度用来存储客户端数据,但随着浏览器开始支持各种存储方式,Cookie渐渐的在被淘汰,主要是每次请求携带Cookie数据带来性能开销(尤其是移动环境下)

1
2
3
4
5
Cookie: <cookie-list>
Cookie: name=value
Cookie: name=value; name2=value2; name3=value3
如:
Cookie: PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;
  • Name: Cookie名,一旦创建便不可修改。
  • Value: Cookie值,Unicode字符为字符编码,二进制数据则为Base64编码。
  • Domain: 可访问Cookie的域名,默认当前主机。
  • Path: Cookie使用路径,/表示本域名下所有页面都可以访问Cookie,/path/则表示只有对应路径才能访问Cookie。
  • Max Age: Cookie失效时间,单位为秒,通常和Expires一起使用,计算有效时间。值为正表示在指定秒后失效,为负表示浏览器关闭时失效,且浏览器不会以任何形式保存此Cookie。
  • Expires: 定义了Cookie的绝对过期日期,和Max Age互斥。若Cookie不含这两个特性,则会在浏览器关闭时删除。
  • Size: Cookie大小。
  • HttpOnly: 当此属性为True,只会发送给服务器端,避免跨域脚本攻击(XSS),无法通过document.cookie访问。限制非HTTP协议程序接口对客户端COOKIE进行访问的,所以客户端脚本,如JS是无法取得这种COOKIE的,同时,JQuery中的 $.cookie('xxx') 方法也无法正常工作,所以想要在客户端取到httponly的COOKIE的唯一方法就是使用AJAX,将取COOKIE的操作放到服务端,接收客户端发送的ajax请求后将取值结果通过HTTP返回客户端。
  • Secure: 表示该Cookie是否仅被使用安全协议传输,在传输数据前先将数据加密,默认为false。用来防止用户发送未加密的会话ID时被窃取。但即使设置了Secure,敏感信息也不建议通过Cookie传输,Chrome 52之后不允许不安全的http使用Secure标记,所以意味着设置后只会在HTTPS请求携带此Cookie。
  • SameSite: 使Cookie在跨站请求时不会被发送,从而阻止跨站请求伪造攻击(CSRF),但并不是所有浏览器都支持。

  由上可知Cookie会分为会话期Cookie持久性Cookie,会话期Cookie在浏览器关闭后即删除,也就是仅在会话期有效,有些浏览器提供会话恢复功能。

  当Cookie的域和页面的域相同,可以称为第一方Cookie,如果不同则称为第三方Cookie。大部分浏览器默认允许第三方Cookie,也可以通过一些组件来阻止第三方Cookie。

  服务器收到HTTP请求后,可以在响应头中添加Set-Cookie。浏览器收到响应后会保存下Cookie,并在之后对服务器的请求中通过请求头Cookie将保存信息发送给服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Set-Cookie: <cookie-name>=<cookie-value> 
Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>
Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<non-zero-digit>
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>
Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value>
Set-Cookie: <cookie-name>=<cookie-value>; Secure
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly

Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Strict
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Lax
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=None

// Multiple directives are also possible, for example:
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly

如:
HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry
请求时:
GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry

2.5 安全问题

  • XSS
  • CSRF

  更多内容参考常见的安全漏洞和攻击方式


第三节 Session

  session即会话,服务器要确认当前对话对象身份,所以要给各个客户端分配不同的身份标识,客户端将身份标识存储在cookie中,在请求时一同发送给服务器,服务器端则通过session把用户信息临时存储在服务器上。

  基于Session认证就是客户端要存储一份sessionid,请求时携带sessionid,服务器端维护session数据,并根据sessionid查询用户信息。


第四节 JWT

Token

  token即令牌,用来通过对密匙和用户ID进行加密,生成对应的签名,返回给用户。用户发起请求时需要携带签名,服务器根据相同加密算法和签名进行校对,来判断数据是否被修改。

JWT

  JWT即Json Web Token,其主要结构如下。

1
2
3
4
5
6
7
8
9
10
11
base64(header).base64(json payload).signature
{
"alg" : "HS256", //加密算法
"typ" : "JWT" //Token类型
}
{
username : 'xxx',
email : 'sa@xxx.com',
role : 'user',
exp : 123123123
}
  • header即头部,用来描述一些基本信息,比如这个token是用什么算法签名的,是什么版本的等等。
  • payload即负载,就是一个json object。你可以任意放置你想要的信息,只要符合json的格式即可。标准中已经规定好了有一些字段的意思,比如iat表示issue at,token签发的时间;exp表示token过期的时间等等。根据这些约定就可以实现一些小的代码库来检查比如token是不是过期了等等。但是请注意,很多人误解,认为JWT是加了密的,但其实payload是明文的。
  • signature是一个签名。服务器端可以自行选择一个算法和一个secret,与payload拼接上,得到一个签名。secret并不会在网络中传输,所以客户端无法伪造一个JWT。这样,一旦一个签名生成,再传回给服务器,服务器就可以知道这个token是不是它当初生成的。

  服务器端只要验证JWT签名正确,且未超时即可。服务器端直接从JWT中获取已验证的用户信息,而不用再去持久层取一次了。

两种Web认证比较

基于Session验证的方式存在的问题?

  1. 开销大:需要为用户维护session,用户增多时需要大量的内存开销。
  2. 扩展性差:无法直接在多台服务器间共享用户信息,而创建session的服务器和验证的服务器很肯不相同,需要共享session。
  3. CORS(跨域资源共享):数据跨越多台移动设备时就会有资源共享问题,要处理兼容性问题。
  4. CSRF(跨站请求伪造):容易受到跨站请求伪造攻击。

  Token则是无状态的身份验证,解决了Session验证的诸多问题.每一次请求都发送token,放在headers中保证HTTP无状态,设置服务器Access-Control-Allow-Origin:*,保证服务器能接收到来自所有域的请求。

Token验证的优点?

  1. 无状态和可扩展:token存储在客户端,身份验证过程基本都在高速内存中执行,可以从一台服务器传送用户信息到另外一台服务器,也可以和第三方程序共享权限。
  2. 安全性:token可以避免CSRF攻击,即使是保存token到客户端也不牵涉到认证,且token有时效性。
  3. 多平台跨域:token只要通过了身份验证,便可以在任何域上发送请求。
  4. 开发便捷:无需再编写用户的一些数据库和缓存查询,降低了接口反应延迟。

  但是JWT无法在服务器端对用户请求进行管理,也无法规范性的对payload数据进行控制。因此牵涉到管理用户登录信息的需求就很难避免去使用基于session的Web认证机制。所以在大部分场景需求下是不适合使用”纯净”的基于Token的Web认证,而是仅仅用JWT来代替sessionid。

系统无法统计如用户登录次数,登录平台,也无法断开用户的登录。一旦实现如撤回token认证,就要重新实现session机制,就算不得无状态了。

虽然规范建议只将认证信息存入payload,但开发者往往会将所有用户信息放入,导致payload尺寸过大,又因为每次请求都要携带,造成了一定的性能损耗

存储位置

客户端浏览器的存储位置主要为:

  • Local Storage
  • Cookie

Local Storage

  常用Header+Local Storage的方式来避免CSRF,但更容易被XSS攻击

  只有设置为HttpOnly的Cookie是脚本无法访问的,所以很适合用来存放token/sessionid

1
Set-Cookie: access_token=xxxxxxxxxxxxxxxxxx; HttpOnly; Secure; Same-Site=strict; Path=/;

  使用此cookie可以完全隔离XSS攻击,而不用担心漏洞问题。

防范CSRF

  在传统页面Web网站中,一般会使用CSRF Token。这是个非常流行的做法。像Tomcat这类的容器都会自带CSRF Token的产生和检查Filter。

CSRF Token流程

  客户端要首先向服务器请求一个带有提交表单的页面,服务器返回的页面中会嵌入一个CSRF Token。当用户提交表单时,CSRF Token会被一起携带发给服务器做验证。所以当服务器看到CSRF Token,就可以放心大胆的确认用户的的确确是看看到了提交前的表单界面,从而避免了用户稀里糊涂提交一个被伪造的表单的可能性。

  CSRF Token只适合于传统的页面请求,在SPA的情况下会比较尴尬。因为在SPA中,客户端与服务器之间的交互主要是通过接口完成的,没有页面的概念。此时的确可以照猫画虎的做一个接口让用户拿到CSRF Token,但这样什么也确认不了。因为攻击者可以调用同样的接口,拿到合法的CSRF Token。

  这时有几种办法:

  1. 给所有接口都添加一个请求secret,来标记其来自于合法的客户端。这个secrect可以是固定的随机字符串,也可以通过某些动态算法产生。对于CSRF,浏览器只会做自动传Cookie而已,并不能帮助传入secret。这样一来,就可以确定消除CSRF的风险。但注意,这个机制仅能防范CSRF,而不能防范人为的攻击。黑客只要拿得到客户端,就一定能找到生成secret的办法。secret有一个顺带的功能是提高了第三方用户随意调用接口的门槛——他们必须得去查看客户端源代码,学会了怎么生成secret才能调用接口。

  2. 用Same-Site Cookie。回到上面CSRF步骤的第二步骤。当用户看到了B站点伪造的表单,点击了提交,向站点A发出请求时,被标记了Same-Site=strict的Cookie是不会被携带的,因为当时的主站点域名B和提交的站点域名A不一样。这是Same-Site=strict标记是个相对较新的标准。目前大部分浏览器都已经支持了。但如果大量的用户群还在类似于IE8这样的老系统上,这个招数便是无效的。

  3. 歪招,总是用json格式提交。CSRF可能发生的一个前提条件是必须用传统表单提交。这是因为传统表单提交可以跨域——你在站点B,可以提交表单给站点A。而Ajax的请求除非开启CORS,是不允许跨域的,所以天然的屏蔽掉了这个问题。传统表单的提交的格式必然是application/x-www-form-urlencoded。因此只要保证服务器能够拒绝处理所有application/x-www-form-urlencoded格式的POST请求,就能确保SPA不受CSRF的影响。那用啥呢?JSON - application/json。(我专门写这一条的原因是,jquery的ajax库的默认行为正是使用application/x-www-form-urlencoded格式。如果你还在用,可以考虑改一下。)

  4. 另一个歪招,双认证。将你的认证信息同时放在HttpOnly Cookie和Authorization Header。服务器要先比对这两个值是一样的,然后再去执行认证过程。这样可以同时防范XSS和CSRF。代价是,如果你的认证信息比较长,会浪费一些带宽。

  5. 使用HTTPS,将Cookie设置为Secure,浏览器就可以只在访问https网址时才会携带Cookie

一个靠谱的Web认证应该:

  • 可以使用Session也可以使用Token做认证,但是总是要保证服务器端可以管理Session,通过Session是否存在来最终确定认证的有效性;
  • 将认证信息存放在标记为HttpOnly,Secure,Same-Site=strict的Cookie中,从而避免XSS和CSRF;
  • 总是使用https,只要你的网络链路经过了公网;
  • 如果是传统的页面网站,请使用CSRF Token机制;
  • 如果可以,做一个简单的请求secret,可以辅助防止CSRF,也可以稍稍的提高接口被爬取的门槛;
  • 如果是SPA应用,放心大胆的禁用对application/x-www-form-urlencoded的支持
  • 保证token/session必须有一个有效期

参考博客和文章书籍等:

HTTP headers

实现一个靠谱的Web认证

因博客主等未标明不可引用,若部分内容涉及侵权请及时告知,我会尽快修改和删除相关内容