3种web会话管理的方式

http 是无状态的,一次请求结束,连接断开,下次服务器再收到请求,它就不知道这个请求是哪个用户发过来的。当然它知道是哪个客户端地址发过来的,但是对于我们的应用来说,我们是靠用户来管理,而不是靠客户端。所以对我们的应用而言,它是需要有状态管理的,以便服务端能够准确的知道 http 请求是哪个用户发起的,从而判断他是否有权限继续这个请求。这个过程就是常说的会话管理。它也可以简单理解为一个用户从登录到退出应用的一段期间。本文总结了 3 种常见的实现 web 应用会话管理的方式:

1)基于 serversession 的管理方式

2)cookie-base 的管理方式

3)token-base 的管理方式

这些内容可以帮助加深对 web 中用户登录机制的理解,对实际项目开发也有参考价值,欢迎阅读与指正。

1. 基于 server 端 session 的管理

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

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

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

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

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

以上过程可简单使用流程图描述如下:

http://static.cyblogs.com/459873-20161115231400951-1095594983.png

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

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

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

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

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

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

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

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

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

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

http://static.cyblogs.com/459873-20161120210043123-760641758.png

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

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

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

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

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

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

http://www.cnblogs.com/fish-li/archive/2012/04/15/2450571.html

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

3. token-based 的管理方式

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

http://static.cyblogs.com/459873-20161120210044154-648255641.png

这种方式不通过 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.comapi 服务部署在 b.com,从 a.com 里面发出 ajax 请求到 b.com,默认情况下是会报跨域错误的,这种问题可以用 CORS跨域资源共享)的方式来快速解决,相关细节可去阅读前面给出的 CORS 文章详细了解。

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

4. 安全问题

web 应用里面,会话管理的安全性始终是最重要的安全问题,这个对用户的影响极大。

首先从会话管理凭证来说,第一种方式的会话凭证仅仅是一个 session id,所以只要这个 session id 足够随机,而不是一个自增的数字 id 值,那么其它人就不可能轻易地冒充别人的 session id 进行操作;第二种方式的凭证(ticket)以及第三种方式的凭证(token)都是一个在服务端做了数字签名,和加密处理的串,所以只要密钥不泄露,别人也无法轻易地拿到这个串中的有效信息并对它进行篡改。总之,这三种会话管理方式的凭证本身是比较安全的。

然后从客户端和服务端的 http 过程来说,当别人截获到客户端请求中的会话凭证,就能拿这个凭证冒充原用户,做一些非法操作,而服务器也认不出来。这种安全问题,可以简单采用 https 来解决,虽然可能还有 http 劫持这种更高程度的威胁存在,但是我们从代码能做的防范,确实也就是这个层次了。

最后的安全问题就是 CSRF(跨站请求伪造)。这个跟代码有很大关系,本质上它就是代码的漏洞,只不过一般情况下这些漏洞,作为开发人员都不容易发现,只有那些一门心思想搞些事情的人才会专门去找这些漏洞,所以这种问题的防范更多地还是依赖于开发人员对这种攻击方式的了解,包括常见的攻击形式和应对方法。不管凭证信息本身多么安全,别人利用 CSRF,就能拿到别人的凭证,然后用它冒充别人进行非法操作,所以有时间还真得多去了解下它的相关资料才行。举例来说,假如我们把凭证直接放到 url 后面进行传递,就有可能成为一个 CSRF 的漏洞:当恶意用户在我们的应用内上传了 1 张引用了他自己网站的图片,当正常的用户登录之后访问的页面里面包含这个图片的时候,由于这个图片加载的时候会向恶意网站发送 get 请求;当恶意网站收到请求的时候,就会从这个请求的 Reffer header 里面看到包含这个图片的页面地址,而这个地址正好包含了正常用户的会话凭证;于是恶意用户就拿到了正常用户的凭证;只要这个凭证还没失效,他就能用它冒充用户进行非法操作。

5. 总结

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

作为一个前端开发人员,本文虽然介绍了 3 种会话管理的方式,但是与前端关系最紧密的还是第三种方式,毕竟现在前端开发 SPA 应用以及 hybrid 应用已经非常流行了,所以掌握好这个方式的认证过程和使用方式,对前端来说,显然是很有帮助的。好在这个方式的技术其实早就有很多实现了,而且还有现成的标准可用,这个标准就是 JWT(json-web-token)

JWT 本身并没有做任何技术实现,它只是定义了 token-based 的管理方式该如何实现,它规定了 token 的应该包含的标准内容以及 token 的生成过程和方法。目前实现了这个标准的技术已经有非常多:

http://static.cyblogs.com/459873-20161120210045904-1163191341.png

参考地址

如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员微信号:chengcheng222e,他会拉你们进群。

简栈文化服务订阅号