以下要说的,虽然不是开发过程中必须会遇到的,但却是进阶之路上必须要掌握的,一些涉及到状态管理与安全的应用当中尤为重要。

  我之前虽略有学习,但也是东拼西凑临时看的一点皮毛,所以在这个假期利用一点时间,整合一篇博文出来,方便以后自己温故,当然能对新入行的朋友有些许帮助,那是最好的了。

  好,废话结束,下面咱们开始。

  ----------------------------

  我们首先分别说一下cookie、session和token各是什么,以及使用方法,然后再说一下他们的关系和区别。

   一、Cookie

  如果是从来没接触过cookie的童鞋,可以先去http://www.runoob.com/js/js-cookies.html文档看一下,很通俗易懂的,里面介绍了cookie的基础属性以及使用cookie时的三种封装,十分钟搞定了再回来。

  由于HTTP是一种无状态的协议,服务器单从网络连接上无从知道客户身份。怎么办呢?就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理。

  Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。

  除了name(名)和value(值),cookie还有以下一些可选属性,用来控制cookie的有效期,作用域,安全性等:

  expires属性

  指定了cookie的生存期,默认情况下cookie是暂时存在的,他们存储的值只在浏览器会话期间存在,当用户退出浏览器后这些值也会丢失,如果想让cookie存在一段时间,就要为expires属性设置为未来的一个用毫秒数表示的过期日期或时间点,expires默认为设置的expires的当前时间。现在已经被max-age属性所取代,max-age用秒来设置cookie的生存期。

  如果max-age属性为正数,则表示该cookie会在max-age秒之后自动失效。浏览器会将max-age为正数的cookie持久化,即写到对应的cookie文件中。无论客户关闭了浏览器还是电脑,只要还在max-age秒之前,登录网站时该cookie仍然有效。

  如果max-age为负数,则表示该cookie仅在本浏览器窗口以及本窗口打开的子窗口内有效,关闭窗口后该cookie即失效。max-age为负数的Cookie,为临时性cookie,不会被持久化,不会被写到cookie文件中。cookie信息保存在浏览器内存中,因此关闭浏览器该cookie就消失了。cookie默认的max-age值为-1。

‍  如果max-age为0,则表示删除该cookie。cookie机制没有提供删除cookie的方法,因此通过设置该cookie即时失效实现删除cookie的效果。失效的Cookie会被浏览器从cookie文件或者内存中删除。

  如果不设置expires或者max-age这个cookie默认是Session的,也就是关闭浏览器该cookie就消失了。

  这里要说明一下:Session的cookie在ie6下,如果用户实在网页上跳转打开页面或新开窗口(包括target=”_blank”,鼠标右键新开窗口),都是在同一个Session内。如果用户新开浏览器程序或者说是进程再打开当前的页面就不是同一个Session。其他浏览器只要你Session存在,还是同一个Session,cookie还能共享。

  domain属性

  domain属性可以使多个web服务器共享cookie。domain属性的默认值是创建cookie的网页所在服务器的主机名。不能将一个cookie的域设置成服务器所在的域之外的域。
  例如让位于a.sodao.com的服务器能够读取b.sodao.com设置的cookie值。如果b.sodao.com的页面创建的cookie把它的path属性设置为“/”,把domain属性设置成“.sodao.com”,那么所有位于b.sodao.com的网页和所有位于a.sodao.com的网页,以及位于sodao.com域的其他服务器上的网页都可以访问这个cookie。

  path属性

  它指定与cookie关联在一起的网页。在默认的情况下cookie会与创建它的网页,该网页处于同一目录下的网页以及与这个网页所在目录下的子目录下的网页关联。

  secure属性

  它是一个布尔值,指定在网络上如何传输cookie,默认是不安全的,通过一个普通的http连接传输;

  HttpOnly属性

  HttpOnly 属性限制了 cookie 对 HTTP 请求的作用范围。特别的,该属性指示用户代理忽略那些通过“非 HTTP” 方式对 cookie 的访问(比如浏览器暴露给js的接口)。注意 HttpOnly 属性和 Secure 属性相互独立:一个 cookie 既可以是 HttpOnly 的也可以有 Secure 属性。
  在前段时间的项目中我就用js去读取一个cookie,结果怎么都取不到这个值,最后查证这个cookie是httpOnly的,花了近2个小时,悲剧了。

  cookie的传输

  浏览器将cookie信息以name-value对的形式存储于本地,每当请求新文档时,浏览器将发送Cookie,目的是让Server可以通过HTTP请求追踪客户。所以从WEB性能的角度来说我们要尽量的减小cookie,以达到传输性能的最大化。

  cookie的编码和解码

  由于cookie的名/值中的值不允许包含分号,逗号和空格符,为了最大化用户代理和服务器的兼容性,任何被存储为 cookie 值的数据都应该被编码,例如用我们前端熟知的js全局函数encodeURIComponent编码和decodeURIComponent解码。 

  cookie作为客户端存储

  前面说了每当请求新文档时,浏览器将发送Cookie到服务器,导致WEB性能下降。所以不建议将cookie作为客户端存储一种实现方案,替代方案参见:JavaScript本地存储实践(html5的localStorage和ie的userData)等。

  cookie的同名问题

  同名的 cookie,不同的 domain 或不同的 path,属不同的 cookie;同名的 cookie,相同的 domain 且相同的 path,不同的 expires,属同一个 cookie。

  cookie的web安全方面

  Cookie 的HttpOnly 属性是Cookie 的扩展功能,它使JavaScript 脚本无法获得Cookie。其主要目的为防止跨站脚本攻击(Cross-sitescripting,XSS)对Cookie 的信息窃取。发送指定HttpOnly 属性的Cookie 的方法

   Set-Cookie: name=value; HttpOnly

  Cookie 的secure 属性用于限制Web 页面仅在HTTPS 安全连接时,才可以发送Cookie。

  基本上可以用到的cookie相关的知识就是以上这些,接下来我们说一下session。

  二、Session

  Session是服务器端使用的一种记录客户端状态的机制,使用上比Cookie简单一些,服务器使用一种类似于散列表的结构来保存信息。

  由于Session这个词汇包含的语义很多,因此需要在这里明确一下 Session的含义。

  首先,我们通常都会把Session翻译成会话,因此我们可以把客户端浏览器与服务器之间一系列交互的动作称为一个 Session。

  从这个语义出发,我们会提到Session持续的时间,会提到在Session过程中进行了什么操作等等;

  其次,Session指的是服务器端为客户端所开辟的存储空间,在其中保存的信息就是用于保持状态。

  从这个语义出发,我们则会提到往Session中存放什么内容,如何根据键值从 Session中获取匹配的内容等。

  要使用Session,第一步当然是创建Session了。那么Session在何时创建呢?当然还是在服务器端程序运行的过程中创建的,在创建了Session的同时,服务器会为该Session生成唯一的Session id,而这个Session id在随后的请求中会被用来重新获得已经创建的Session;

  在Session被创建之后,就可以调用Session相关的方法往Session中增加内容了,而这些内容只会保存在服务器中,发到客户端的只有Session id;当客户端再次发送请求的时候,会将这个Session id带上,服务器接受到请求之后就会依据Session id找到相应的Session,从而再次使用之。

  如果说Cookie机制是通过检查客户身上的“通行证”来确定客户身份的话,那么Session机制就是通过检查服务器上的“客户明细表”来确认客户身份。Session相当于程序在服务器上建立的一份客户档案,客户来访的时候只需要查询客户档案表就可以了。

  Session的生命周期

  Session保存在服务器端。为了获得更高的存取速度,服务器一般把Session放在内存里。每个用户都会有一个独立的Session。如果Session内容过于复杂,当大量客户访问服务器时可能会导致内存溢出。因此,Session里的信息应该尽量精简。

  Session在用户第一次访问服务器的时候自动创建。需要注意只有访问JSP、Servlet等程序时才会创建Session,只访问HTML、IMAGE等静态资源并不会创建Session。如果尚未生成Session,也可以使用request.getSession(true)强制生成Session。

  Session生成后,只要用户继续访问,服务器就会更新Session的最后访问时间,并维护该Session。用户每访问服务器一次,无论是否读写Session,服务器都认为该用户的Session“活跃(active)”了一次。

  关于新手必须要理解的几个名词,cookie、session和token-LMLPHP

  也就是说,session状态管理,是依赖cookie的,如果一旦在浏览器中cookie被禁止了,session机制就会瘫痪(不绝对),之前我们有两种方法来在cookie被禁时使用:

  经常被使用的一种技术叫做URL重写,就是把session id直接附加在URL路径的后面。

  还有一种技术叫做表单隐藏字段。

  不过现在两者存在一定安全问题,一般的大型网站都已经不再使用,像web淘宝,一旦禁止了浏览器的cookie机制,便会被禁止登录,而网易音乐则一种半登录状态,既只有登录信息,但是像通过session id取到的列表则不会显示,有兴趣的童鞋可以禁了cookie之后,去登录试试。

  因为session是服务端机制,前端并不会对其进行操作,所以一些方法我们此处略过,感兴趣的童鞋,可以研究一下Session的其他方法

  session基本上就介绍到这里,下面我们说一下token。

  三、Token

  token的意思是“令牌”,是用户身份的验证方式,最简单的token组成:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,由token的前几位+盐以哈希算法压缩成一定长的十六进制字符串,可以防止恶意第三方拼接token请求服务器)。

  关于token,比较流行的一种方式是使用web token,所谓的token可以看作是一个标识身份的令牌。

  客户端在登录成功后可以获得服务端加密后的token,然后在后续需要身份认证的接口请求中在header中带上这个token,服务端就可以通过判断token的有效性来验证该请求是否合法。

  是不是觉得这个token和session_id有点像,似乎两者是干的同一件事,我来说一个场景:

  用户主动退出登录状态。

  容易想到的方案是,客户端登录成功后, 服务器为其分配sessionId, 客户端随后每次请求资源时都带上sessionId。  

  服务器判断用户是否登录, 完全依赖于sessionId, 一旦其被截获, 黑客就能够模拟出用户的请求。

  于是我们需要引入token的概念: 用户登录成功后, 服务器不但为其分配了sessionId, 还分配了token, token是维持登录状态的关键秘密数据。

  在服务器向客户端发送的token数据,也需要加密。

  客户端向服务器第一次发起登录请求(不传输用户名和密码)。

  服务器利用RSA算法产生一对公钥和私钥。并保留私钥, 将公钥发送给客户端。

  客户端收到公钥后, 加密用户密码,向服务器发送用户名和加密后的用户密码; 同时另外产生一对公钥和私钥,自己保留私钥, 向服务器发送公钥; 于是第二次登录请求传输了用户名和加密后的密码以及客户端生成的公钥。

  服务器利用保留的私钥对密文进行解密,得到真正的密码。 经过判断, 确定用户可以登录后,生成sessionId和token, 同时利用客户端发送的公钥,对token进行加密。最后将sessionId和加密后的token返还给客户端。

  客户端利用自己生成的私钥对token密文解密, 得到真正的token。

  这可以看做是token和session管理状态的区别,可以看出一个是放在服务端,一个是放在客户端,这也就是下面我们要说的,三者之间的区别以及联系。

  四、session、cookie以及token的联系

  不打算按着网上的模式,陈列一堆诸如cookie和session的区别之类的话题,三者最终都会落实在会话管理上。

  So,我们今天就说说三者在会话管理上的区别,以及应用。

  

  1、session管理方式

  2、cookie管理方式

  3、token管理方式

  1.session管理方式

  在早期web应用中,通常使用服务端session来管理用户的会话。快速了解服务端session:

  1) 服务端session是用户第一次访问应用时,服务器就会创建的对象,代表用户的一次会话过程,可以用来存放数据。服务器为每一个session都分配一个唯一的sessionid,以保证每个用户都有一个不同的session对象。

  2)服务器在创建完session后,会把sessionid通过cookie返回给用户所在的浏览器,这样当用户第二次及以后向服务器发送请求的时候,就会通过cookie把sessionid传回给服务器,以便服务器能够根据sessionid找到与该用户对应的session对象。

  3)session通常有失效时间的设定,比如2个小时。当失效时间到,服务器会销毁之前的session,并创建新的session返回给用户。但是只要用户在失效时间内,有发送新的请求给服务器,通常服务器都会把他对应的session的失效时间根据当前的请求时间再延长2个小时。

  4)session在一开始并不具备会话管理的作用。它只有在用户登录认证成功之后,并且往sesssion对象里面放入了用户登录成功的凭证,才能用来管理会话。管理会话的逻辑也很简单,只要拿到用户的session对象,看它里面有没有登录成功的凭证,就能判断这个用户是否已经登录。当用户主动退出的时候,会把它的session对象里的登录凭证清掉。所以在用户登录前或退出后或者session对象失效时,肯定都是拿不到需要的登录凭证的。

  以上过程可简单使用流程图描述如下: 
关于新手必须要理解的几个名词,cookie、session和token-LMLPHP

  主流的web开发平台(java,.net,php)都原生支持这种会话管理的方式,而且开发起来很简单,相信大部分后端开发人员在入门的时候都了解并使用过它。它还有一个比较大的优点就是安全性好,因为在浏览器端与服务器端保持会话状态的媒介始终只是一个sessionid串,只要这个串够随机,攻击者就不能轻易冒充他人的sessionid进行操作;除非通过CSRF或http劫持的方式,才有可能冒充别人进行操作;即使冒充成功,也必须被冒充的用户session里面包含有效的登录凭证才行。但是在真正决定用它管理会话之前,也得根据自己的应用情况考虑以下几个问题:

  1)这种方式将会话信息存储在web服务器里面,所以在用户同时在线量比较多时,这些会话信息会占据比较多的内存;

  2)当应用采用集群部署的时候,会遇到多台web服务器之间如何做session共享的问题。因为session是由单个服务器创建的,但是处理用户请求的服务器不一定是那个创建session的服务器,这样他就拿不到之前已经放入到session中的登录凭证之类的信息了;

  3)多个应用要共享session时,除了以上问题,还会遇到跨域问题,因为不同的应用可能部署的主机不一样,需要在各个应用做好cookie跨域的处理。

  针对问题1和问题2,我见过的解决方案是采用redis这种中间服务器来管理session的增删改查,一来减轻web服务器的负担,二来解决不同web服务器共享session的问题。针对问题3,由于服务端的session依赖cookie来传递sessionid,所以在实际项目中,只要解决各个项目里面如何实现sessionid的cookie跨域访问即可,这个是可以实现的,就是比较麻烦,前后端有可能都要做处理。

  如果不考虑以上三个问题,这种管理方式比较值得使用,尤其是一些小型的web应用。但是一旦应用将来有扩展的必要,那就得谨慎对待前面的三个问题。如果真要在项目中使用这种方式,推荐结合单点登录框架如CAS一起用,这样会使应用的扩展性更强。

  2.cookie管理方式

  由于前一种方式会增加服务器的负担和架构的复杂性,所以后来就有人想出直接把用户的登录凭证直接存到客户端的方案,当用户登录成功之后,把登录凭证写到cookie里面,并给cookie设置有效期,后续请求直接验证存有登录凭证的cookie是否存在以及凭证是否有效,即可判断用户的登录状态。使用它来实现会话管理的整体流程如下:

  1)用户发起登录请求,服务端根据传入的用户密码之类的身份信息,验证用户是否满足登录条件,如果满足,就根据用户信息创建一个登录凭证,这个登录凭证简单来说就是一个对象,最简单的形式可以只包含用户id,凭证创建时间和过期时间三个值。

  2)服务端把上一步创建好的登录凭证,先对它做数字签名,然后再用对称加密算法做加密处理,将签名、加密后的字串,写入cookie。cookie的名字必须固定(如ticket),因为后面再获取的时候,还得根据这个名字来获取cookie值。这一步添加数字签名的目的是防止登录凭证里的信息被篡改,因为一旦信息被篡改,那么下一步做签名验证的时候肯定会失败。做加密的目的,是防止cookie被别人截取的时候,无法轻易读到其中的用户信息。

  3)用户登录后发起后续请求,服务端根据上一步存登录凭证的cookie名字,获取到相关的cookie值。然后先做解密处理,再做数字签名的认证,如果这两步都失败,说明这个登录凭证非法;如果这两步成功,接着就可以拿到原始存入的登录凭证了。然后用这个凭证的过期时间和当前时间做对比,判断凭证是否过期,如果过期,就需要用户再重新登录;如果未过期,则允许请求继续。

关于新手必须要理解的几个名词,cookie、session和token-LMLPHP

  这种方式最大的优点就是实现了服务端的无状态化,彻底移除了服务端对会话的管理的逻辑,服务端只需要负责创建和验证登录cookie即可,无需保持用户的状态信息。对于第一种方式的第二个问题,用户会话信息共享的问题,它也能很好解决:因为如果只是同一个应用做集群部署,由于验证登录凭证的代码都是一样的,所以不管是哪个服务器处理用户请求,总能拿到cookie中的登录凭证来进行验证;如果是不同的应用,只要每个应用都包含相同的登录逻辑,那么他们也是能轻易实现会话共享的,不过这种情况下,登录逻辑里面数字签名以及加密解密要用到的密钥文件或者密钥串,需要在不同的应用里面共享,总而言之,就是需要算法完全保持一致。

  这种方式由于把登录凭证直接存放客户端,并且需要cookie传来传去,所以它的缺点也比较明显:

  1)cookie有大小限制,存储不了太多数据,所以要是登录凭证存的消息过多,导致加密签名后的串太长,就会引发别的问题,比如其它业务场景需要cookie的时候,就有可能没那么多空间可用了;所以用的时候得谨慎,得观察实际的登录cookie的大小;比如太长,就要考虑是非是数字签名的算法太严格,导致签名后的串太长,那就适当调整签名逻辑;比如如果一开始用4096位的RSA算法做数字签名,可以考虑换成1024、2048位;

  2)每次传送cookie,增加了请求的数量,对访问性能也有影响;

  3)也有跨域问题,毕竟还是要用cookie。

  相比起第一种方式,cookie-based方案明显还是要好一些,目前好多web开发平台或框架都默认使用这种方式来做会话管理,比如php里面yii框架,这是我们团队后端目前用的,它用的就是这个方案,以上提到的那些登录逻辑,框架也都已经封装好了,实际用起来也很简单;asp.net里面forms身份认证,也是这个思路,这里有一篇好文章把它的实现细节都说的很清楚:

  文章分享

  前面两种会话管理方式因为都用到cookie,不适合用在native app里面:native app不好管理cookie,毕竟它不是浏览器。这两种方案都不适合用来做纯api服务的登录认证。要实现api服务的登录认证,就要考虑下面要介绍的第三种会话管理方式。

  3.token管理方式

  这种方式从流程和实现上来说,跟cookie-based的方式没有太多区别,只不过cookie-based里面写到cookie里面的ticket在这种方式下称为token,这个token在返回给客户端之后,后续请求都必须通过url参数或者是http header的形式,主动带上token,这样服务端接收到请求之后就能直接从http header或者url里面取到token进行验证:

关于新手必须要理解的几个名词,cookie、session和token-LMLPHP

  这种方式不通过cookie进行token的传递,而是每次请求的时候,主动把token加到http header里面或者url后面,所以即使在native app里面也能使用它来调用我们通过web发布的api接口。app里面还要做两件事情:

  1)有效存储token,得保证每次调接口的时候都能从同一个位置拿到同一个token;

  2)每次调接口的的代码里都得把token加到header或者接口地址里面。

  看起来麻烦,其实也不麻烦,这两件事情,对于app来说,很容易做到,只要对接口调用的模块稍加封装即可。

  这种方式同样适用于网页应用,token可以存于localStorage或者sessionStorage里面,然后每发ajax请求的时候,都把token拿出来放到ajax请求的header里即可。不过如果是非接口的请求,比如直接通过点击链接请求一个页面这种,是无法自动带上token的。所以这种方式也仅限于走纯接口的web应用。

  这种方式用在web应用里也有跨域的问题,比如应用如果部署在a.com,api服务部署在b.com,从a.com里面发出ajax请求到b.com,默认情况下是会报跨域错误的,这种问题可以用CORS(跨域资源共享)的方式来快速解决,相关细节可去阅读前面给出的CORS文章详细了解。

  这种方式跟cookie-based的方式同样都还有的一个问题就是ticket或者token刷新的问题。有的产品里面,你肯定不希望用户登录后,操作了半个小时,结果ticket或者token到了过期时间,然后用户又得去重新登录的情况出现。这个时候就得考虑ticket或token的自动刷新的问题,简单来说,可以在验证ticket或token有效之后,自动把ticket或token的失效时间延长,然后把它再返回给客户端;客户端如果检测到服务器有返回新的ticket或token,就替换原来的ticket或token。

  总结:

  前面这三种方式,各自有各自的优点及使用场景,我觉得没有哪个是最好的,做项目的时候,根据项目将来的扩展情况和架构情况,才能决定用哪个是最合适的。本文的目的也就是想介绍这几种方式的原理,以便掌握web应用中登录验证的关键因素。

  作为一个前端开发人员,本文虽然介绍了3种会话管理的方式,但是与前端关系最紧密的还是第三种方式,毕竟现在前端开发SPA应用以及hybrid应用已经非常流行了,所以掌握好这个方式的认证过程和使用方式,对前端来说,显然是很有帮助的。

 

  文章部分参考:诸葛流云

  

  

05-15 21:21