本文将从Web应用从传统认证到基于token认证的演进过程来介绍Session、Cookie和Token。很久以前,Web应用程序主要用于浏览文档,例如互联网黄页。由于只是浏览,服务器不需要记录特定用户在一定时间内浏览了哪些文档。每个请求都是一个新的HTTP协议,对服务器来说是全新的。基于会话的身份验证伴随着交互式Web应用程序的兴起,例如需要登录的网站,例如购物。这就引出了一个新的问题,就是记录哪些用户登录了系统,进行了哪些操作,也就是管理session(什么是session?简单的说,如果一个用户需要登录,那么它可以可以简单理解为一个session,如果没有Login,那么就是一个简单的连接。),比如不同的用户在购物车里加入不同的商品,也就是说要区分每个用户。因为HTTP请求是无状态的,所以我想出了一个办法,给每个用户分配一个会话ID(SessionID)。简单的说,就是一个既不重复又不易被模仿的sessionID。一个随机字符串,以便每个用户收到不同的会话标识符。每次用户从客户端向服务器发起HTTP请求时,将这个字符串一起发送,以便服务器区分谁是谁?至于客户端(浏览器)如何保存这个“身份”,一般默认采用cookie的方式,会话标识(Sessionid)会保存在客户端的cookie中。这样虽然解决了区分用户的问题,但是又导致了一个新的问题,即每个用户(客户端)只需要保存自己的会话标识(Sessionid),而服务端需要保存所有用户的会话标识(会话ID)。如果访问服务器的用户逐渐增多,需要节省几千甚至几千万,这对服务器来说是不可接受的开销。再比如,服务器是由两台服务器组成的集群。小明通过服务器A登录系统,sessionid会保存在服务器A上,如果小明的下一个请求转发到服务器B怎么办?服务器B可以没有小明的sessionid。可能有人会说,如果小明在登录的时候一直登录在服务器A(stickysession),这个问题不就解决了吗?服务器A挂了怎么办?尽管如此,小明的请求还是会转发给服务器B的上级。这样只能做集群间的session复制和共享,即sessionid在两台机器之间复制,如下图,但这对服务器的性能和内存提出了巨大的挑战。因此,我也想到了将所有的用户session集中存储起来,想到了缓存服务Memcached——由于Memcached是一个分布式内存对象缓存系统,可以利用它来实现session同步。将sessionid集中存储在一台服务器上,所有的服务器都会访问这个地方的数据,从而避免了复制的方式,但是这种“集万千宠于一身”的做法,使得单点的访问成为可能failure,也就是说负责存储session的服务器挂了,所有用户都得重新登录,这是用户无法接受的。那么简单存放Session的服务器也进行集群,增加可靠性,避免单点故障,但不管怎样,Session带来的问题层出不穷。所以有人在想,为什么服务器端一定要保存session,难道每个client都保存还不够吗?但是如果服务端不保存这些sessionid,它如何验证客户端发送的sessionid确实是一个服务呢?如果不通过验证,则服务器无法判断是否为合法登录用户。是的,这里的问题是验证。Session只是解决这个验证问题。还有其他解决方案吗?基于Token的认证比如小明登录了系统,服务器发给他一个令牌(Token),里面包含了小明的用户id。当小明再次请求通过Http访问服务器时,会通过Http头带上这个Token。如果你不来这里也没关系。服务端需要验证Token是自己生成的,不是伪造的。如果任何人都可以在不验证的情况下伪造,那么这个令牌(token)和sessionid就没有本质区别了。别人怎么能不伪造呢?然后对数据进行签名(Sign),例如服务器使用HMAC-SHA256加密算法,加上一个只有服务器知道的密钥,对数据进行签名,将这个签名和数据一起作为Token发送给客户端,客户端收到Token后就可以存储了,比如存储在Cookie中或者LocalStorage中,由于这个密钥对于除了服务器以外的任何其他用户都是未知的,所以不可能伪造令牌(Token)。这样服务器就不需要保存Token了。当小明将Token发送给服务器时,服务器使用相同的HMAC-SHA256算法和相同的密钥再次计算数据上的签名,并与Token进行签名比对。如果相同,则说明小明已经登录,即验证成功。如果不相同,则说明该请求是伪造的。这样,服务端只需要生成Token,不需要保存Token,只需验证Token,实现时间换空间(CPU计算时间换session存储空间)。没有了sessionid的限制,当用户访问量增加时,可以通过直接加机器的方式轻松横向扩展,也大大提高了可扩展性。
