详解Web应用会话管理:3种常见方式实现HTTP状态管理
HTTP 具有无状态的特性。一次请求完成后,连接会断开。当下次服务器接收到请求时,它无法知晓该请求是由哪个用户发送而来。不过它能知道是从哪个客户端地址发送过来的。但对于我们的应用而言,我们是通过用户来进行管理的,而非依靠客户端。

对于我们的应用来说,它需要有状态管理。这样,服务端就能准确知晓是哪个用户发起了 http 请求,进而判断其是否有权限继续该请求。这个过程就是通常所说的会话管理。也可以简单地将其理解为,从用户登录到退出应用的这一段期间。本文总结了 3 种常见的实现 web 应用会话管理的方式:
1)基于server端session的管理方式
2)cookie-base的管理方式
3)token-base的管理方式
这些内容有助于加深对 web 里用户登录机制的理解,并且对实际项目开发具有参考价值,欢迎大家进行阅读和指正。
1. 基于server端session的管理
早期 web 应用常借助服务端 session 来管理用户会话。以下是对服务端 session 的快速了解:
服务器在用户第一次访问应用时会创建对象,这个对象就是服务端 session,它代表着用户的一次会话过程,能够用来存放数据。并且服务器会为每一个 session 分配一个唯一的 sessionid,这样就能保证每个用户都有一个不同的 session 对象。
服务器创建完 session 后,会把 sessionid 通过 cookie 返还给用户所在的浏览器。当用户第二次及之后向服务器发送请求时,会通过 cookie 将 sessionid 传回给服务器,以便服务器能依据 sessionid 找到与该用户对应的 session 对象。
5)通常服务器都会根据当前的请求时间,将其对应的 session 的失效时间再延长 2 个小时。
session 一开始不具备会话管理的作用。只有在用户登录认证成功后,并且将用户登录成功的凭证放入 session 对象中,它才可以用来管理会话。管理会话的逻辑较为简单,只要拿到用户的 session 对象,查看其里面是否有登录成功的凭证,就能判断该用户是否已经登录。用户主动退出时,会将其 session 对象里的登录凭证清除掉。因此,在用户登录前,以及用户退出后或者 session 对象失效的时候,肯定都无法获取到所需的登录凭证。
以上过程可简单使用流程图描述如下:
主流的 web 开发平台包括 java、.net 和 php,它们都原生支持这种会话管理的方式。这种方式开发起来很简单,相信大部分后端开发人员在入门时都了解并使用过它。它的一个较大优点是安全性良好。在浏览器端与服务器端保持会话状态的媒介一直只是一个 sessionid 串。只要这个串足够随机,攻击者就难以轻易冒充他人的 sessionid 来进行操作。只有通过 CSRF 或 http 劫持的方式,才有可能冒充别人进行操作。即便冒充成功,也必须被冒充的用户的 session 中包含有效的登录凭证才行。在决定用它管理会话之前,需要根据自身的应用情况考虑以下几个问题:
这种方式会将话信息存储于 web 服务器内。当用户同时在线的数量较多时,这些会话信息会占用较多的内存。
当应用采用集群部署的方式时,会面临多台 web 服务器之间如何进行 session 共享的问题。因为 session 是由单个服务器创建的,然而处理用户请求的服务器未必是创建 session 的服务器,所以它就无法获取到之前已经放入到 session 中的登录凭证等信息。
多个应用共享 session 时,除了前面提到的问题,还会面临跨域问题。这是因为不同的应用可能部署在不同的主机上,所以需要在各个应用中做好 cookie 跨域的处理。
针对问题 1 以及问题 2,我所见过的解决方案为:运用 redis 这种中间服务器去对 session 进行增删改查的管理。这样做一方面能够减轻 web 服务器的负担,另一方面也可以解决不同 web 服务器共享 session 的问题。针对问题 3,因为服务端的 session 依靠 cookie 来传递 sessionid。所以在实际的项目当中,只需解决各个项目中如何实现 sessionid 的 cookie 跨域访问的问题就可以了。这种情况是能够实现的,只是过程比较麻烦,前后端都有可能需要进行处理。
如果不把以上三个问题考虑在内,这种管理方式是比较值得被使用的,尤其对于一些小型的 web 应用而言。然而,一旦应用在将来有了扩展的必要,就必须要对前面的三个问题持谨慎态度。要是真的要在项目中使用这种方式,建议将其与单点登录框架如 CAS 结合起来使用,如此一来会让应用的扩展性变得更强。
2. cookie-based的管理方式
前一种方式会给服务器带来负担且使架构变得复杂,于是后来有人想出一个方案,即直接将用户的登录凭证存到客户端。用户登录成功后,把登录凭证写入 cookie 中,并为 cookie 设置有效期。后续请求只需验证存有登录凭证的 cookie 是否存在以及凭证是否有效,就能判断用户的登录状态。使用它来实现会话管理的整体流程如下:
用户发起登录请求。服务端依据传入的用户密码等身份信息,对用户是否满足登录条件进行验证。若满足,就依据用户信息创建一个登录凭证。这个登录凭证简单来讲是一个对象,其最简单的形式可仅包含用户 id、凭证创建时间以及过期时间这三个值。
服务端创建好登录凭证后,首先对其进行数字签名,接着用对称加密算法进行加密处理,之后将签名以及加密后的字串写入 cookie 中。并且 cookie 的名字需要固定,比如 ticket,这是因为后续获取时需要依据这个名字来获取 cookie 的值。这一步添加数字签名的目的在于防止登录凭证中的信息被篡改。因为一旦信息被篡改,那么在下一步进行签名验证时肯定会失败。做加密的目的是,当防止 cookie 被他人截取时,能够使得他人无法轻易读取其中的用户信息。
用户登录之后发起后续请求。服务端依据上一步存储登录凭证的 cookie 名字,能够获取到相关的 cookie 值。接着先对其进行解密处理,之后再进行数字签名的认证。倘若这两步都失败了,那就表明这个登录凭证是非法的;而如果这两步都成功了,随后就可以拿到最初存入的登录凭证了。首先用这个凭证的过期时间与当前时间进行对比,接着判断凭证是否过期,若过期,就要求用户重新登录;若未过期,就允许请求继续进行。
这种方式的最大优点在于实现了服务端的无状态化。它彻底将服务端对会话的管理逻辑移除了。服务端只需负责创建和验证登录 cookie ,而无需保持用户的状态信息。对于第一种方式的第二个问题,即用户会话信息共享的问题,它能够很好地解决。如果只是同一个应用进行集群部署,由于验证登录凭证的代码相同,所以无论哪个服务器处理用户请求,都能获取到 cookie 中的登录凭证进行验证。如果是不同的应用,只要每个应用都包含相同的登录逻辑,那么它们就能够轻易实现会话共享。不过在这种情况下,登录逻辑中数字签名以及加密解密所用到的密钥文件或密钥串,需要在不同的应用中共享,也就是说需要算法完全保持一致。
这种方式将登录凭证直接存放在客户端,并且需要通过 cookie 进行传递,因此它的缺点较为明显:
cookie 有大小限制,无法存储过多数据。若登录凭证存储的消息过多,加密签名后的串就会变长,从而引发其他问题,例如在其他业务场景需要使用 cookie 时,可能会没有足够的空间可用。因此,在使用 cookie 时需谨慎,要观察实际登录 cookie 的大小。如果过长,需考虑是否是数字签名的算法过于严格导致签名后的串过长,若如此,可适当调整签名逻辑。比如,若一开始使用 4096 位的 RSA 算法做数字签名,可考虑换成 1024 位或 2048 位。
每次传送 cookie 时,会使请求的数量增加,并且也会对访问性能产生影响。
3)也有跨域问题,毕竟还是要用cookie。
第一种方式与之相比,cookie-based 方案明显更优。目前许多 web 开发平台或框架都将其作为会话管理的默认方式,例如在 php 中,yii 框架就是如此,这是我们团队后端目前正在使用的,它采用的就是这个方案,上述提到的那些登录逻辑,框架也都已进行了封装,实际使用起来较为简单;在 asp.net 中,forms 身份认证也是同样的思路,有一篇很好的文章对其实现细节进行了清晰的阐述。
前面两种会话管理方式因都使用 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 并进行验证。
这种方式不借助 cookie 来传递 token 。每次请求时,会主动将 token 添加到 http header 中或者 url 后面。因此,即便在 native app 中,也能够使用它来调用通过 web 发布的 api 接口。在 app 中还需要做两件事:
要有效存储 token,就必须保证在每次调用接口时,都能从相同的位置获取到同一个 token。
每次调接口的代码都需要将 token 加到 header 中。每次调接口的代码也都需要将 token 加到接口地址里面。
看起来麻烦,实则并不麻烦。这两件事情对于 app 而言,是比较容易做到的。只要对接口调用的模块进行一些简单的封装就可以了。
这种方式对网页应用也适用。token 能够存放在 localStorage 中,或者存放在 sessionStorage 里。接着,在每次发送 ajax 请求时,都把 token 取出并放到 ajax 请求的 header 里。然而,如果是进行非接口的请求,例如仅仅通过点击链接来请求一个页面这种情况,就无法自动携带 token 了。所以这种方式也仅限于走纯接口的web应用。
这种方式用于 web 应用时存在跨域问题。例如,若应用部署在 a.com,而 api 服务部署在 b.com,从 a.com 内部向 b.com 发出 ajax 请求,在默认情况下会出现跨域错误。这种问题可通过 CORS(跨域资源共享)的方式快速解决,相关细节可阅读前面给出的 CORS 文章以详细了解。
这种方式与 cookie-based 的方式都存在一个问题,即 ticket 或者 token 刷新的问题。在某些产品中,你肯定不希望用户登录后,经过半个小时的操作,却导致 ticket 或者 token 到达过期时间,进而让用户又得重新登录。这个时候需要考虑 ticket 或 token 自动刷新的事宜。简单而言,在验证 ticket 或 token 有效之后,能够自动将 ticket 或 token 的失效时间予以延长,接着把延长后的 ticket 或 token 返回给客户端。倘若客户端检测到服务器返回了新的 ticket 或 token,就将原来的 ticket 或 token 进行替换。
4. 安全问题
在 web 应用当中,会话管理的安全性一直以来都是最为重要的安全问题。它对用户产生的影响是非常大的。
从会话管理凭证方面来看,第一种方式的会话凭证只是一个 session id。只要这个 session id 足够随机,而非自增的数字 id 值,那么其他人就难以轻易冒充别人的 session id 来进行操作。第二种方式的凭证(ticket)以及第三种方式的凭证(token),它们都是在服务端经过数字签名和加密处理的串。只要密钥不被泄露,其他人就无法轻易获取这个串中的有效信息并对其进行篡改。总之,这三种会话管理方式的凭证本身是比较安全的。
从客户端和服务端的 http 过程来看,若别人截获到客户端请求中的会话凭证,便可利用此凭证冒充原用户,实施一些非法操作,并且服务器也无法识别。对于这种安全问题,可通过简单采用 https 来加以解决,尽管可能仍存在 http 劫持这种更高级别的威胁,但就我们从代码层面能够采取的防范措施而言,确实仅能达到这个层次。
最后的安全问题为 CSRF(跨站请求伪造)。此问题与代码关系密切,本质上是代码的漏洞。通常情况下,这些漏洞作为开发人员不易察觉,只有那些一心想搞事的人才会专门去寻找这些漏洞。因此,这种问题的防范更多地依赖于开发人员对这种攻击方式的了解,包括常见的攻击形式和应对方法。凭证信息本身即便很安全,然而别人若利用 CSRF 这种手段,就能够获取到他人的凭证,接着便可凭借此凭证冒充他人去实施非法操作,正因如此,确实应该多花些时间去了解一下它的相关资料。举例而言,如果将凭证直接放置在 url 后面进行传递,那么就有成为 CSRF 漏洞的可能。当恶意用户在我们的应用内上传了一张引用其自身网站的图片,并且正常用户登录后访问的页面包含此图片时,由于图片加载会向恶意网站发送 get 请求。当恶意网站收到请求,就能从该请求的 Reffer header 中看到包含此图片的页面地址,而此地址恰好包含了正常用户的会话凭证。这样,恶意用户就获取到了正常用户的凭证。只要该凭证未失效,他就能利用它冒充用户进行非法操作。
5. 总结
前面这三种方式各自具有优点和使用场景。我认为不存在哪种方式是最好的。在做项目时,需要根据项目将来的扩展情况和架构情况,来决定使用哪种方式是最合适的。本文的目的就是想介绍这几种方式的原理,从而掌握 web 应用中登录验证的关键因素。
前端开发人员而言,本文介绍了 3 种会话管理方式。其中,与前端关系最为紧密的是第三种方式。如今,前端开发 SPA 应用和 hybrid 应用已十分流行。因此,掌握好这种方式的认证过程和使用方式,对前端来说是很有帮助的。这种方式的技术很早就已经有许多实现了,同时还有现成的标准可供使用,这个标准就是 JWT(json-web-token)。
JWT 本身并未进行任何技术层面的实现,只是明确了 token-based 的管理方式该以何种方式实现。它规定了 token 应当包含的标准内容,同时也规定了 token 的生成过程和方法。目前,已经有非常多的技术实现了这个标准。
更多可参阅:#libraries-io
为了更全面地认识第三种会话管理方式的实现,我选择以 express 以及上面众多 JWT 实现中的 jsonwebtoken 来进行研究,相关内容将在下一篇博客中详细介绍。本文内容至此结束,感谢阅读,欢迎关注下一篇博客的内容。