当前位置: 首页 > 科技观察

快醒醒,Cookie + Session 的时代已经过去了

时间:2023-03-20 23:03:18 科技观察

醒醒,Cookie+Session的时代结束了。转载本文请联系飞天小牛公众号。这篇文章主要是在做Echo社区项目的时候写的。在维护用户登录状态的需求下,为什么要用ThreadLocal来存储用户信息,而不是普通的Cookie+Session。Cookie+Session由于HTTP协议是无状态的,在操作完成关闭浏览器后,客户端和服务器的连接就会断开,所以我们必须要有一个机制来保证客户端和服务器之间会话的连续性服务器。常见的方式是使用Cookie+Session(会话)。具体来说,当客户端向服务器发起请求时,服务器会为此次请求开辟一块内存空间(Session对象),服务器可以在该内存空间中存放客户端在会话期间的一些操作记录(比如用户信息可以存放在Session),同时会生成一个sessionID,通过响应头的Set-Cookie:JSESSIONID=XXXXXXX命令将sessionID保存在客户端的Cookie中。可能有同学会问,为什么不直接把所有的数据都存到cookie里,返回整个session,把sessionID存到cookie里呢?Cookie长度限制:首先,最基本的,cookie是有长度限制的,限制了它可以存储数据的长度,影响了性能:Cookies确实可以让服务器程序像跟踪每个客户端的访问一样Session,但是客户端每次访问都要返回这些cookies,所以如果cookies中存放的数据较多,这无疑会增加客户端与服务器端的数据传输量,增加服务器端的压力。安全性:Session数据其实是属于服务端的,cookies是属于客户端的。将本应保存在session中的数据放入客户端cookie中,使得服务端数据延伸到外网和客户端,显然在安全问题上存在问题。当然,我们可以对这些数据进行加密,但从技术上讲,物理接触是最安全的。这样,根据Cookie+Session的机制,当服务端收到客户端请求时,只需要从cookie中获取sessionID,就可以得到相应的Session。当会话处于活动状态时,我们认为客户端始终处于活动状态(用户已登录)。一旦会话过期,就可以认为客户端已经停止与服务器交互(用户注销)。如果遇到cookie被禁用的情况,一般的做法是将这个sessionID放在URL参数中。这也是面试中经常被问到的问题。这种机制在单体应用时代被广泛使用,但是随着分布式时代的到来,Session的缺点逐渐暴露出来。比如我们有多个服务器,客户端1向服务器发送请求。由于负载均衡的存在,请求被转发到服务器A,所以服务器A创建并存储了Session。然后,客户端1又向服务器发送了一个请求,但是这次请求是负载均衡到服务器B的,此时服务器B没有保存服务器A的session,导致session失效。用户在上一个界面登录,跳到下一个界面后又退出,这显然是不合理的。分布式集群session共享当然是,这个其实有很多解决方案,其实就是如何解决多台服务器之间session共享的问题:SessionReplication是最容易想到的,因为服务器B没有服务器A存储Session,然后在各个服务器之间同步Session数据,就结束了。这种方案的问题也很明显:同步Session数据会带来额外的网络带宽开销。只要会话数据发生变化,就需要将数据同步到所有其他机器上。机器越多,同步带来的网络带宽开销就越大。每个Web服务器都必须保存所有会话数据。如果整个集群的session数据非常多(比如很多人同时访问网站),每个服务器保存session数据的内存占用会很严重。SessionSticky从名字也能看出来。Sticky允许负载均衡器根据每个请求的会话ID转发请求,确保一个会话中的每个请求都可以落在同一台服务器上。存在的问题是:如果一台服务器宕机或者重启,存储在上面的Session数据就会丢失,用户需要重新登录。负载均衡器成为有状态节点,因为它需要保存从Session到特定服务器的映射。与之前的无状态节点相比,内存消耗会更大,容灾也更麻烦。Session数据的集中存储使用外部存储(Redis、MySQL等)将Session数据集中存储,然后所有服务器从这个外部存储中获取Session。如果外存机器出现问题,会直接影响到我们的应用程序ThreadLocal。其实无论采用什么方案,使用Session机制都会导致服务器集群难以扩展。因此,Session适用于中小型Web应用。对于大型Web应用,通常需要避免使用Session机制。所以,在Echo项目中,我们决定放弃Session,一个ThreadLocal解决一切问题(狗头)!ThreadLocal线程本地内存,很好理解,就是每个访问ThreadLocal变量的线程都有自己的“本地”实例副本,各个线程之间相互隔离,互不干扰。我不会在这里详细解释基本原理。ThreadLocal适用于以下两种场景:每个线程都需要有自己独立的实例(数据)实例(数据)需要在多个方法之间共享,但又不想被多个线程共享。看看如何使用ThreadLocal来实现我们的需求:显示登录信息,并在这个请求中保存当前用户数据。首先,我们需要明白的是,ThreadLocal只和它所属的线程有关。当一个线程死亡时,其对应的ThreadLocal中存储的信息会被清除(绑定的用户数据必须在线程死亡前释放,否则会出现OOM问题),也就是说ThreadLocal只是用来保存数据的要求。简单来说,我们将用户数据存储在ThreadLocal中。这样只要不处理这个请求,这个线程就会一直存在,一直持有当前的用户数据。当服务器响应这个请求时,线程就会被销毁。同一个用户发送的两个请求可能会被两个不同的线程处理。如何让两个线程的ThreadLocal持有相同的用户信息?筛选。具体的,我们定义一个过滤器,在每次请求前对用户进行判断(为了避免每次请求都经过过滤器,可以将登录成功的用户信息暂存在Redis中),然后将登录成功的用户信息存储在ThreadLocal中,这样线程就保存了本次请求中的用户信息。