cookie、session、token详解
tbghg

介绍

Http协议是一个无状态的协议,因此需要一个标识用来让服务器区分不同的浏览器。cookie就是这个管理服务器与客户端之间状态的标识。

cookie原理:浏览器第一次向服务器发送请求时,服务器可以通过在response头部设置Set-Cookie字段,浏览器收到响应就会设置cookie并存储,在下一次该浏览器向服务器发送请求时,就会在request头部自动带上Cookie字段,服务器端收到该cookie用以区分不同的浏览器。当然,这个cookie与某个用户的对应关系应该在第一次访问时就存在服务器端,这时就需要session了。

基础知识

  • cookie是有大小限制的。每个cookie所存放的数据不能超过4k,如果cookie字符串长度超过4k,则该属性将返回空字符串。
  • 由于cookie最终都是以文件形式存放在客户端计算机中,所以查看和修改cookie都是很方便的,这就是为什么常说cookie不能存放重要信息的原因。
  • 每个cookie是存在有效期的。在默认情况下,一个cookie的生命周期就是在关闭浏览器的时候结束。如果想要cookie能在浏览器关掉之后还可以使用,就必须要为cookie设置有效期,也就是cookie的失效日期。
  • alert(typeof document.cookie) 结果是string,而不是array。
  • cookie有域和路径这个概念。域就是domain的概念,因为浏览器是个注意安全的环境,所以不同的域之间是不能互相访问cookie的(当然可以通过特殊设置达到cookie跨域访问)。路径就是routing的概念,一个网页所创建的cookie只能被这个网页在同一目录或者子目录下的所有页面访问,而不能被其他目录下的网页访问。
  • 创建cookie的方式和定义变量的方式有些相似,都需要使用cookie名称和cookie值。同个网站可以创建多个cookie,而多个cookie可以存放在同一个cookie文件中。当然不同浏览器都有着cookie数量、大小的上限

常见问题

  • cookie存在两种类型
    • 你浏览的当前网站本身设置的cookie
    • 来自在网页上嵌入广告或图片等其他域来源的,第三方cookie(网页可通过使用这些cookie跟踪你的使用信息)
  • cookie的生命周期,大致也两种状态:
    • 临时性质的cookie。当前使用的过程中网站会存储一些你的个人信息,当浏览器关闭后这些信息也会从计算机中删除
    • 设置失效时间的cookie。就算浏览器关闭了,这些信息依然会在计算机中,如登录名和密码,这样无须每次都到特定站点时都进行登录,这种cookie可以在计算机中保留几天,几个月,甚至几年
  • cookie的两种清除方式
    • 通过浏览器工具清除cookie(有第三方工具,浏览器自身也有这种功能)
    • 通过设置cookie的有效期来清除cookie
    • 删除cookie有时候可能导致某些网页无法正常运行
  • 浏览器可以通过设置来接收和拒绝访问cookie
  • 出于功能和性能的原因考虑,建议尽量减少cookie的使用数量,并且尽量使用小cookie

路径概念

cookie有域和路径的概念,现在介绍路径在cookie中作用。 cookie一般都是由于用户访问页面而被创建的,可是并不是只有在创建cookie的页面才可以访问这个cookie。 默认情况下只有与创建cookie的页面在同一个目录或子目录的网页才可以访问,这个是因为安全方面的考虑,造成不适所有页面都可以随意访问其他页面创建的cookie。

http://www.cnblog.com/Darren\_code/ 这里种的cookie
http://www.cnblog.com/Darren\_code/archive/cookie.html 可以拿到上面的cookie
http://www.cnblog.com/xxx/ 就拿不到cookie,http://www.cnblog.com也拿不到

那么如何让这个cookie能被其他目录或者父级目录访问,通过设置cookie的路径就可以实现,如:

document.cookie = “name=value;path=path”;
document.cookie = “name=value;expires=date;path=path”

path就是cookie的路径,最常用的就是让cookie在根目录下,这样不管是哪个子页面创建的cookie,所有的页面都可以访问了;

document.cookie = “name=yulong;path=/“

域概念

路径能解决在同一个域下访问cookie的问题,咱们接着说cookie实现同域访问的问题

document.cookie = “name=yulong;path=path;domain=domain”

如果我们有a:www.qq.com 与 b:sports.qq.com 公共一个关联域名qq.com 想让b下的cookie被a访问,我们就需要用到cookie的domain属性,并且把path设置成更目录”/“

document.cookie = “name=yulong;path=/;domain=qq.com”

一定是同域之间的访问,不能把domain的值设置成非主域的域名。

安全性

通常cookie信息都是使用http连接传递数据,这种传递方式很容易被查看,所以cookie存储的信息容易被窃取。加入cookie中所传递的内容比较重要,那么就要求使用加密的数据传输。 所以cookie的这个属性的名称是secure,默认值为空,如果一个cookie的属性为secure,那么它与服务器之间的就通过https或者其他安全协议传递数据

document.cookie = ‘username=yulong;secure’

把cookie设置为secure,只是cookie与服务器之间的数据传输过程加密,而保存在本地的cookie文件并不加密。如想让本地cookie加密,要自己加密。

就算设置了secure属性也不代表他人不能看到你机器本地保存的cookie信息,所以说到底,别把重要信息放cookie就对了。

编码细节

在输入cookie信息时,不能包含空格,分号,逗号等特殊符号,而在一般情况下,cookie信息的存储都是采用未编码的方式。所以在设置cookie信息之前,要先使用escape()函数将cookie信息进行编码,在获取得到cookie值的时候,再使用unescape()函数把值进行转换回来,

document.cookie = name = “=” + escape(value)
return unescape(docment.cookie.substring(start,end))

这样就不用担心因为cookie值出现了特殊符号而导致cookie信息报错了。

session

session是会话的意思,浏览器第一次访问服务端,服务端就会创建一次会话,在会话中保存标识该浏览器的信息。它与cookie的区别就是session是缓存在服务端的,cookie 则是缓存在客户端,他们都由服务端生成,为了弥补Http协议无状态的缺陷。

session-cookie认证

  1. 服务器在接受客户端首次访问时在服务器端创建session,然后保存session(我们可以将session保存在内存中,也可以保存在redis中,推荐使用后者),然后给这个session生成一个唯一的标识字符串,然后在响应头中种下这个唯一标识字符串。
  2. 签名。这一步通过秘钥对sid进行签名处理,避免客户端修改sid。(非必需步骤)
  3. 浏览器中收到请求响应的时候会解析响应头,然后将sid保存在本地cookie中,浏览器在下次http请求的 请求头中会带上该域名下的cookie信息。
  4. 服务器在接受客户端请求时会去解析请求头cookie中的sid,然后根据这个sid去找服务器端保存的该客 户端的session,然后判断该请求是否合法。

用户登录认证

使用session-cookie做登录认证时,登录时存储session,退出登录时删除session,而其他的需要登录后才能操作的接口需要提前验证是否存在session,存在才能跳转页面,不存在则回到登录页面。

Token

基于Token的认证,是指将所有认证相关的信息在服务器端编码成一个Token,并由服务器签名,以确保不被篡改。Token本身是明文的。存在Token里的信息可以有比如user id、权限列表、用户昵称一类的。这样服务器只要拿着token和token的签名,就可以直接验证用户的身份是合法的。在现实当中,基于Token的认证的主要标准是Json Web Token (JWT)

JWT资料还是比较多的,不再赘述另外这个网站不错JWT

注意事项

  • 使用了JWT,无法实现在服务器端对用户请求进行管理——管理员没法统计多少个人登录了,一个人登录了多少次,登陆了什么设备;同时,也无法强行“踢”掉一个用户的登录——JWT一旦生成,在失效之前,总是有效的。如果实现了一个token黑名单之类的功能,就等价于实现了Session机制,无状态带来的好处就无从谈起。这个限制对于任何一个要认真做用户风险控制的网站来说都是不可能接受的。
  • 使用了JWT,无法很好的控制payload的数据量。尽管规范表示,应该只把认证的相关信息放到payload里。但实际上,开发人员往往会误用,把几乎所有和user相关的数据都放到payload里。而payload的尺寸过大,比如达到数KB,就会极大的损耗带宽和IO性能。要记得,为了达成“无状态”,每个请求都必须把全量的JWT都带着……

这两个严重的缺陷限定了JWT只能用到一些不太认真的场景。而对于真正的社交、金融、游戏等认真一点的服务,还是要选择基于Session的认证。

另外如果token中还有session id,就可以在服务器端实现基于Session的认证。因此,你可以将user id、session id、token过期时间等几个关键数据放到payload里——只放这几个,不放其他的数据,得到一个用来做Session认证的JWT。更进一步,如果你把JWT的规范稍微小改一下,比如payload不用JSON,而是更紧凑的格式;定死了签名算法,即可省略JWT的header了;最后再优化一下编码格式,就能得到一个你自己的token。

Token和session的区别

  1. session-cookie的缺点:(1)认证方式局限于在浏览器中使用,cookie是浏览器端的机制,如果在app端就无法使用cookie。(2)为了满足全局一致性,我们最好把session存储在redis中做持久化,而在分布式环境下,我们可能需要在每个服务器上都备份,占用了大量的存储空间。(3)在不是Https协议下使用cookie,容易受到CSRF跨站点请求伪造攻击。
  2. token的缺点:(1)加密解密消耗使得token认证比session-cookie更消耗性能。(2)token比sessionId大,更占带宽。
  3. 两者对比,它们的区别显而易见:(1)token认证不局限于cookie,这样就使得这种认证方式可以支持多种客户端,而不仅是浏览器。且不受同源策略的影响。(2)不使用cookie就可以规避CSRF攻击。(3)token不需要存储,token中已包含了用户信息,服务器端变成无状态,服务器端只需要根据定义的规则校验这个token是否合法就行。这也使得token 的可扩展性更强。

存储认证信息

谈完了session和token,我们来说所说这个信息在客户端怎么存储。客户算也分两大类——浏览器和Native App。先说说浏览器。

浏览器

浏览器中的存储主要是Local Storage和Cookie。

其实浏览器用于存放认证信息的存储还有Session Storage,但是它和Local Storage差不多,只是失效的机制不太一样。这里只用Local Storage讨论。

使用基于Token认证的开发人员很喜欢使用Header + Local Storage。因为这样可以有效防止CSRF (下一小节专门讲)。

但是使用Local Storage,反而会增加中招XSS(Crossing Site Script)的机会。一旦中招XSS,攻击者可以轻易的拿到认证信息,并且传回自己的接受网址而不被用户察觉。这样一来攻击者能够轻易的代替用户登录了。

整个浏览器中,只有一种资源是脚本无法访问到的。这就是被设置为HttpOnly的cookie。这是非常理想的放置认证token/session id的地方。设置这种token只需要在Set Cookie时这么写:

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

(Secure和Same-Site是什么?下文会解释)

XSS攻击者没有任何办法从HttpOnly的Cookie中拿到你的认证信息,除非他能在你登录网站后,直接进入你的电脑,打开浏览器的开发者工具并人肉复制粘贴(叫你不锁屏,哼)。

有些人坚称自己的程序可以保证不受XSS的攻击,所以可以放心的用Local Storage。比如使用React框架开发的程序理论上所有的DOM节点都由React的虚拟DOM产生,所有的标签生成都进行了escape。espace掉的脚本就无法执行,也就不可能XSS了。

这样讲没有错误,但是XSS最令人头疼的地方在于你很难保证你的网站对所有用户的输入都进行了escape。

  • 你编写的是一个写文章的网站,需要支持用户手工输入HTML,并且HTML必须得直接展示;
  • 你编写的网站99%是React这样的框架生成的,但是可能会有一些边角,为了方便还是使用jquery等传统技术
  • 你的网站是一个团队开发,尽管开发规范要求大家都要对用户的输入进行escape处理,但是只要是人就会忘,而escape这件事情不一定能进入到测试的Case清单;
  • ……

只要有一个漏洞存在,那么整个防护体系就完全失效。这就是为什么HttpOnly Cookie这样的绝对隔离措施很关键的原因。

Native App

Native App比浏览器相对简单。一般Native App都是静态编译产生,不存在XSS的问题。同时移动操作系统都会有沙箱机制,避免其他App读取到自己的数据(除非你root了……)。因此,Native App可以比较放心的将数据存在App沙箱内某个存储即可。如果不放心,可以考虑如iOS KeyChain或者Android KeyStore这样的设施。

但Native App与服务器交互有一些区别。Native App一般是不直接支持Cookie机制的。所以如果一个服务器端使用Cookie来保存认证信息,就需要Natvie App手工解析Set-Cookie Header,同时,Native App因为不直接支持Cookie,所以倾向于在请求中使用AuthorizationHeader来传入认证信息。这也需要服务器适配。当然,最简单的办法是让Native App引入一个模拟Cookie行为的库。

防止CSRF

CSRF代表Crossing Site Recource Forge。大致的触发流程是:

  1. 用户登录了站点A,并且在Cookie中留下了A站点的认证信息
  2. 用户进入了站点B,而站点B用一些方式(比如一个提交行为是到A站点某关键接口的表单)引诱用户去点击。当用户点击时,会发出到A站点的请求。而浏览器会给这个请求附带上A站点的认证信息,从而让这个请求能够执行。这种行为可能是,但不限于,给某个A站点的某个其他用户提权/转账/发文辱骂等等。

上文中提到了,很多人用JWT+Local Storage的本心是为了防护CRSF。这样做的原因是——因为Cookie的发送是完全由浏览器控制的,不受网页本身的控制。所以最简单直接的办法,就是不用Cookie,不让自动发送认证信息成为可能。问题在于,这么干是有XSS风险的。从上文中可以看到,为了避免XSS,就必须用HttpOnlyCookie。

那么怎么在使用Cookie的同时,还能防范CSRF呢?

传统页面Web网站

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

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

SPA

CSRF Token只适合于传统的页面请求,在SPA的情况下会比较尴尬。

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

这时有几种办法:

  • 给所有接口都添加一个请求secret,来标记其来自于合法的客户端。这个secrect可以是固定的随机字符串,也可以通过某些动态算法产生。对于CSRF,浏览器只会做自动传Cookie而已,并不能帮助传入secret。这样一来,就可以确定消除CSRF的风险。但注意,这个机制仅能防范CSRF,而不能防范人为的攻击。黑客只要拿得到客户端,就一定能找到生成secret的办法。secret有一个顺带的功能是提高了第三方用户随意调用接口的门槛——他们必须得去查看客户端源代码,学会了怎么生成secret才能调用接口。
  • Same-Site Cookie。回到上面CSRF步骤的第二步骤。当用户看到了B站点伪造的表单,点击了提交,向站点A发出请求时,被标记了Same-Site=strict的Cookie是不会被携带的,因为当时的主站点域名B和提交的站点域名A不一样。这是Same-Site=strict标记是个相对较新的标准。目前大部分浏览器都已经支持了。但如果大量的用户群还在类似于IE8这样的老系统上,这个招数便是无效的。
  • 歪招,总是用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格式。如果你还在用,可以考虑改一下。)
  • 另一个歪招,双认证。将你的认证信息同时放在HttpOnly Cookie和Authorization Header。服务器要先比对这两个值是一样的,然后再去执行认证过程。这样可以同时防范XSS和CSRF。代价是,如果你的认证信息比较长,会浪费一些带宽。

总是使用https

http是明文传输的。在http下,用户输入的任何信息,从他的电脑到服务器之间的每个链路节点都是明文的。在这里个链路中的任何地方,都可以截取到完整的数据,包含你的密码,认证token……

这就是为什么https是必须的。https主要提供三个保证:

  • 端端加密。通过https交互的原始数据,只有用户的浏览器和最终的服务器可以看到。其他中间节点无法获)。
  • 客户端可以认定要访问的服务器就是那个服务器。这是被证书体系所支撑的。一旦浏览器的地址栏出现了网址的证书信息,并且是绿色的提示,那么用户的心里就可以稳了。(当然国内其实也不完全是这样,讲多了查水表,懂者自懂)。
  • 服务器可以认定访问的客户端就是合法的客户端。这种模式被称为“2-Way SSL”或者“Mutal SSL”。这种模式是可选的,需要多配置一个客户端证书,一般场景用不到,多见于企业Web服务。

早些时候,很多人对https有一些抵触,大致的原因是,支持https需要软件改造;服务器对证书进行密码计算要耗费很多CPU;同时也会带来跟多的网络请求和响应(多了ssl握手)。这无疑会带来一些成本和体验上的问题。但那已经是10多年前的事情了。现在的软硬件处理能力和网络基础设施比起10年前都有数倍的提高。如果今天,一个商业网站仍然坚持不用https,那么可以请他的老板去大街上裸奔。

使用了https后,为了进一步保证安全,将Cookie设置为Secure。这样,浏览器就可以只在访问https网址时才会携带Cookie。如果不做这样的设置,通过https站点设置的Cookie,仍然会向http站点发送。当这个站点的域名解析被劫持,就可能造成向一个伪造的服务器发出你的认证信息。

认证信息不应该永久有效

很多人为了“用户体验”,选择让一个登录永久有效。这样做是非常危险的。因为一旦用户的认证信息被别人获取了,就永久性的失去了防御的机会。

因此,总是要保证认证信息的有效期是有限的。一般根据业务场景的安全级别不同,可以设为若干分钟~若干天。就算是社交娱乐类的应用,有效期最好也不要超过两周。

但,为了让频繁使用的用户体验更好,可以考虑实现会话期续期。但需要注意,这里说的续期是指从用户角度看可以延续其不需要登录的时间长度,而不是直接让session/token有效期变长。必须实现为给用户替换一个新的session id/token。这样做,既能保证同一个认证信息不会永久有效,又能让正常的、频繁使用的用户免除登录之苦。

总结一下

总结下来,一个靠谱的Web认证应该:

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

如果你也觉得靠谱,就不妨照着做。

OAuth2.0

OAuth,把这个看完基本就没问题了,说的很形象(当然我也正在看)

参考文章

 评论
评论插件加载失败
正在加载评论插件