第5章用户登录服务——5.6 登录状态管理

第5章用户登录服务——5.6 登录状态管理
John Yaml客户端与服务端之间的通信是基于HTTP的,而HTTP是无状态的,即客户端每次发出请求时都永远无法得知上一次请求的状态数据,任何请求之间都是相互隔离的。比如客户端发起一次用户登录请求,之后再发起请求时并不知晓用户已经登录,更不知道自己的请求来自哪个用户。
然而,请求是否是已登录用户发起的是一个强诉求,对于绝大多数的写数据请求(如关注某人、发文章、发评论、转账等)来说,服务端都必须要求从请求中能识别出发起的用户,不然服务端根本不知道这些请求应该为谁执行;对于读数据请求也应该有类似的诉求。因此,所有的客户端请求都应该携带某些数据,以便服务端能够判断请求的发起者是否已登录,以及对应的用户ID。
这个问题似乎很容易解决:用户登录后,客户端把返回的用户ID保存起来,然后每次发起请求时都将用户ID强制作为参数之一传递即可。但是这种做法太过直白,任何稍有HTTP知识的人都可以把作为参数的用户ID随意修改后发送给服务端,使用户受到攻击。比如某个黑客通过本地网络抓包工具得知查询用户账户余额的HTTP path是api.friendy.com/wallet/get?user_id=xxx
,他将任意用户ID作为入参就可以直接查询到此用户的账户余额。可见,这种解决问题的方式无法被接受。
若要真正安全且正确地识别出请求的发起用户ID,则需要使用专门的用户登录态方案来管理,其目标是可以做到认证用户:用户是否已登录,以及请求是否真的是此用户发起的。
5.6.1 存储型方案:Session
Session一般被称为会话,我们把客户端与服务端之间一系列的交互动作称为Session。Session技术是HTTP状态保持的解决方案,它通过在服务端保存用户会话上下文信息,使得无状态的HTTP请求之间可以共享信息。Session技术的典型应用场景就是登录态。
在Session技术中,Session有时也指代一个用户会话的存储结构。客户端登录后,Session被创建,且在整个用户会话中一直存在,直到客户端退出登录或超时才被销毁。Session作为存储结构时,就像一个哈希表,其保存了与用户相关的若干变量。一个登录用户的Session—般包括(但不限于)如下内容。
- 用户ID
- 用户登录设备ID
- 用户登录设备IP地址
- 用户登录设备平台类型,如Android、iOS、iPad、PC端等
- 用户登录方式,如账号登录、微信登录、扫码登录等
- Session的创建时间和更新时间
使用Session技术实现用户登录态时,需要考虑的主要问题有两个:
Session如何存储
用户请求如何查询到对应的Session
在解决这两个问题前,我们需要先介绍一下SessionlD。SessionlD是一个Session的唯一标识,是对分布式唯一ID的应用,并且是在Session创建时生成的。
对于Session存储方式,几乎所有的用户请求都会查询用户对应的Session,所以Session存储需要满足高性能、高可用性和可扩展性。我们可以使用Redis存储Session数据,其中Key为SessionlD,Value为Hash类型,以保存Session数据哈希表中的每个键值对。
对于请求查询Session,服务端与客户端之间通过Cookie来传递SessionlD,用户登录时服务端会创建Session,并将对应的SessionID通过Set-Cookie字段响应给客户端;客户端收到响应数据后,之后的请求会在Cookie中携带服务端下发的SessionlD,服务端收到请求后,根据Cookie中保存的SessionlD查询用户对应的Session数据。
如上所述,通过Cookie技术与Session技术的结合就可以实现用户登录态检查,其完整流程如下所述,如图5-8所示。
(1)客户端发起用户登录请求,如账号登录、手机验证码登录、扫码登录等。
(2)服务端(用户登录服务)接收用户登录请求,在完成登录流程后,在Redis中创建Session。
(3)用户登录服务响应客户端,并下发SessionlD:
1 | HTTP/1.0 200 OK |
(4)客户端登录后发起的任何请求,在Cookie的有效期内会一直携带SessionlD:
1 | GET / HTTP/1.0 |
(5)服务端收到用户请求,发现在请求的Cookie中SessionlD字段有数据,于是使用SessionlD查询Redis。
(6)如果查询到Session,则说明请求来自已登录用户,于是从Session中读取用户ID,进行正常的请求处理;否则,说明用户未登录,或者用户已较长时间不活跃,请求被服务端阻断,并告知客户端需要重新登录。
Cookie是存储在客户端的,且用户可以对其随意修改。如果用户故意修改了Cookie中的SessionlD值,则接下来的用户请求会携带不属于自己的SessionlD访问数据,于是用户登录服务将请求发起者识别为其他用户,导致其他用户数据被意外暴露,且数据有被恶意篡改的风险。为了识别SessionlD是否被篡改,可以为SessionlD绑定一个数字签名。具体操作如下。
- 用户登录服务在下发SessionlD前,使用某加密算法(如MD5)对SessionlD进行加密,将所得到的结果字符串选取前N位作为数字签名。
- 用户登录服务把SessionlD与数字签名用连字符等组装后再下发给客户端,即
Set-Cookie:SessionID=sid-secret
,其中sid是SessionlD,secret是数字签名。 - 服务端在处理后续客户端请求时,使用同样的加密算法再次为请求Cookie中的sid计算数字签名。如果计算结果与secret相同,则认为Cookie未被篡改,即sid是有效的SessionlD;如果计算结果与secret不同,则说明Cookie被篡改了,客户端必须重新登录。
Session方案的优点:
- 简单清晰、易于实现,服务端可以完全决策用户是否登录,甚至可以主动踢用户下线,统计某时刻登录人数。
Session方案的缺点:
要求Session数据使用Redis存储,即Redis是该方案的强依赖,只要Redis响应变慢或者发生网络抖动,就会导致出现用户登录判断不可用的情况。
另外,几乎所有的用户请求都要从Redis中查询Session,对于拥有海量用户的应用来说,查询Redis的请求量会极其庞大,虽然硬件资源足够支撑这么大的请求量,但是会非常浪费存储资源。
5.6.2 令牌
在互联网后台服务架构设计中,会将业务场景划分为两种主要类型。
- I/O密集型:是指需要大量输入/输出操作的场景,例如读/写文件、网络通信、数据库操作等。这类场景的特点是需要等待I/O操作完成后才能继续执行,因此CPU的利用率比较低,且比较依赖存储系统。
- 计算密集型:是指需要大量计算操作的场景,例如图像处理、视频编码、科学计算等。这类场景的特点是需要大量的CPU计算资源,而不需要存储系统。
Session技术就是将用户登录态设计为一种I/O密集型业务,所以Redis成为这类场景的核心依赖。而如果通过某种技术方案把用户登录态设计为计算密集型业务,那么就可以完全不依赖存储系统,进而大幅度提高用户登录态判断的可用性与性能,同时可以大量节约存储资源,这就是令牌方案。
令牌是用户登录的凭证,用户在完成登录后,服务端会基于用户身份信息加密生成一个安全令牌返回给客户端,客户端的后续用户请求在请求头中携带此令牌给服务端,服务端通过验证令牌的合法性来验证用户是否已登录,对合法的令牌解密后,即可获取到用户信息。
令牌设计应该是相对灵活的,我们需要尽可能遵循的原则是令牌认证不宜太简单,且能包含业务常用的用户信息。一种可能的令牌设计如图5-9所示。
上面的令牌总长度为32字节,其中分别使用8字节来保存用户ID、用户登录设备ID、过期时间,然后对这24字节的数据进行加密计算得到数字签名,并使用8字节来保存签名结果。
验证此令牌的合法性需要依次满足如下条件。
- 令牌长度为32字节。
- 对令牌前24字节的数据进行加密计算得到的值,与最后8字节的数据相同。
- 从第1个8字节的数据中解析用户ID,得到非0值。
- 从第2个8字节的数据中解析用户登录设备ID,得到非0值。
- 从第3个8字节的数据中解析过期时间,此时间应该大于当前时间戳值。
如果任意一个条件不满足,则令牌被视为非法的或无效的,服务端均会要求客户端重新登录;如果所有条件均满足,则令牌通过合法性验证,解析得到的用户ID就是请求发起者。
令牌方案的优点:
- 令牌方案不依赖任何存储系统,服务端通过本地计算就可以判断用户是否已登录,以及识别用户ID,所以它的性能很好,延时开销极低。
令牌方案的缺点:
- 不过,由于用户状态完全由分散的令牌管理,服务端很难管理登录用户,比如无法主动踢用户下线,无法统计某时刻处于登录状态的用户信息。
5.6.3 长短令牌方案
考虑到Session方案和令牌方案各自的优劣,我们可以将这两种方案结合起来:
- 用户登录依然在Redis中创建Session,也依然生成令牌,只不过Session被设置了较长的过期时间,而令牌被设置了较短的过期时间;
- 然后服务端把SessionlD和令牌都下发到客户端;
- 客户端发起后续用户请求时会携带这两个信息,服务端优先认证令牌,如果发现令牌过期,那么再使用SessionlD从Redis中查询用户Session。
在这种结合方案中,令牌相当于Session数据的“缓存”,既保持了令牌方案的高性能优势,又兼顾了Session方案的服务端对用户登录可控的能力。此方案名为“长短令牌方案”,其短令牌指的是5.6.2节介绍的令牌,而长令牌就是SessionlD。
下面详细描述一下基于长短令牌方案的用户登录态工作流程。
客户端发起登录请求(无论以何种方式登录)。
用户登录服务处理请求,分别执行长令牌和短令牌的逻辑。
长令牌:生成SessionlD,并在Redis中创建用户Session数据,设置过期时间为30天。这个过期时间表示用户登录在30天内有效。
短令牌:根据用户ID、用户登录设备ID和一个较短的过期时间如1天,生成短令牌。
用户登录服务对长短令牌分别加密得到数字签名后成功响应客户端,并下发长短令牌。客户端的后续用户请求都会在HTTP请求头中携带长短令牌。
- 长令牌:long_token=SessioriID+数字签名。
- 短令牌:short_token=用户ID+用户登录设备ID+过期时间+数字签名。
客户端登录成功后,所发起的任何请求都携带long_token和short_token字段。
服务端收到请求,先以5.6.2节所列的条件验证短令牌(即请求头中的short_token值),如果验证通过,则用户登录态识别完成;如果验证不通过,原因是令牌已过期,则执行下一步,否则认为令牌非法,要求客户端重新登录。
如果服务端发现短令牌已过期,则读取长令牌(即请求头中的long_token值)。 如果校验数字签名后认为SessionlD被篡改了,则要求客户端重新登录;否则,从用户登录服务中查询SessionlD对应的用户Session数据。
如果Session数据不存在,则说明用户登录态过期,客户端需要重新登录;否则,认为用户登录态识别完成,同时用户登录服务重新生成一个短令牌,再次随着请求的响应下发到客户端。
客户端请求成功得到响应,更新短令牌,便于后续使用,此时用户完全感知不到短令牌的更新。
当服务端需要强制某用户下线时,直接删除用户Session数据即可——当用户请求中携带的短令牌过期时,由于查询不到Session数据,因此可以产生用户下线的效果。短令牌的过期时间影响强制用户下线的生效时间,短令牌的过期时间越短,强制用户下线越及时。但是短令牌还承担了 Session数据缓存的角色,如果其过期时间太短,那么用户登录服务和Redis的访问压力就会增加,所以短令牌的过期时间需要根据业务的实际情况和资源压力综合权衡。
总之,拥有海量用户的应用非常适合采用长短令牌方案来管理用户登录态,因为它结合了Session方案和令牌方案的优势,同时补齐了两者的短板。
- 高性能、高可用性:短令牌相当于长令牌的缓存,用户请求在短令牌的有效期内,可以通过本地计算高效识别用户登录态,使得真正需要长令牌访问Session数据的请求量大大降低。
- 服务端管理用户登录:长令牌作为登录态的最后决策者,依然可以操作和统计用户登录态,只不过降低了一些时效性。
总结
通过Cookie技术与Session技术的结合就可以实现用户登录态检查,其完整流程?
(1)客户端发起用户登录请求,如账号登录、手机验证码登录、扫码登录等。
(2)服务端(用户登录服务)接收用户登录请求,在完成登录流程后,在Redis中创建Session。
(3)用户登录服务响应客户端,并下发SessionlD:
1 | HTTP/1.0 200 OK |
(4)客户端登录后发起的任何请求,在Cookie的有效期内会一直携带SessionlD:
1 | GET / HTTP/1.0 |
(5)服务端收到用户请求,发现在请求的Cookie中SessionlD字段有数据,于是使用SessionlD查询Redis。
(6)如果查询到Session,则说明请求来自已登录用户,于是从Session中读取用户ID,进行正常的请求处理;否则,说明用户未登录,或者用户已较长时间不活跃,请求被服务端阻断,并告知客户端需要重新登录。
Session方案的优点:
- 简单清晰、易于实现,服务端可以完全决策用户是否登录,甚至可以主动踢用户下线,统计某时刻登录人数。
Session方案的缺点:
要求Session数据使用Redis存储,即Redis是该方案的强依赖,只要Redis响应变慢或者发生网络抖动,就会导致出现用户登录判断不可用的情况。
另外,几乎所有的用户请求都要从Redis中查询Session,对于拥有海量用户的应用来说,查询Redis的请求量会极其庞大,虽然硬件资源足够支撑这么大的请求量,但是会非常浪费存储资源。
什么是I/O密集型和计算密集型?
- I/O密集型:是指需要大量输入/输出操作的场景,例如读/写文件、网络通信、数据库操作等。这类场景的特点是需要等待I/O操作完成后才能继续执行,因此CPU的利用率比较低,且比较依赖存储系统。
- 计算密集型:是指需要大量计算操作的场景,例如图像处理、视频编码、科学计算等。这类场景的特点是需要大量的CPU计算资源,而不需要存储系统。
用户登录的令牌设计一般是啥样的呢?
- 令牌总长度为32字节,其中分别使用8字节来保存用户ID、用户登录设备ID、过期时间,然后对这24字节的数据进行加密计算得到数字签名,并使用8字节来保存签名结果。
验证此令牌的合法性需要依次满足如下条件。
- 令牌长度为32字节。
- 对令牌前24字节的数据进行加密计算得到的值,与最后8字节的数据相同。
- 从第1个8字节的数据中解析用户ID,得到非0值。
- 从第2个8字节的数据中解析用户登录设备ID,得到非0值。
- 从第3个8字节的数据中解析过期时间,此时间应该大于当前时间戳值。
令牌方案的优点:
- 令牌方案不依赖任何存储系统,服务端通过本地计算就可以判断用户是否已登录,以及识别用户ID,所以它的性能很好,延时开销极低。
令牌方案的缺点:
- 不过,由于用户状态完全由分散的令牌管理,服务端很难管理登录用户,比如无法主动踢用户下线,无法统计某时刻处于登录状态的用户信息。
什么是长短令牌方案?
考虑到Session方案和令牌方案各自的优劣,我们可以将这两种方案结合起来:
- 用户登录依然在Redis中创建Session,也依然生成令牌,只不过Session被设置了较长的过期时间,而令牌被设置了较短的过期时间;
- 然后服务端把SessionlD和令牌都下发到客户端;
- 客户端发起后续用户请求时会携带这两个信息,服务端优先认证令牌,如果发现令牌过期,那么再使用SessionlD从Redis中查询用户Session。
在这种结合方案中,令牌相当于Session数据的“缓存”,既保持了令牌方案的高性能优势,又兼顾了Session方案的服务端对用户登录可控的能力。此方案名为“长短令牌方案”,其短令牌指的是5.6.2节介绍的令牌,而长令牌就是SessionlD。
总之,拥有海量用户的应用非常适合采用长短令牌方案来管理用户登录态,因为它结合了Session方案和令牌方案的优势,同时补齐了两者的短板。
- 高性能、高可用性:短令牌相当于长令牌的缓存,用户请求在短令牌的有效期内,可以通过本地计算高效识别用户登录态,使得真正需要长令牌访问Session数据的请求量大大降低。
- 服务端管理用户登录:长令牌作为登录态的最后决策者,依然可以操作和统计用户登录态,只不过降低了一些时效性。
基于长短令牌方案的用户登录态工作流程?
客户端发起登录请求(无论以何种方式登录)。
用户登录服务处理请求,分别执行长令牌和短令牌的逻辑。
长令牌:生成SessionlD,并在Redis中创建用户Session数据,设置过期时间为30天。这个过期时间表示用户登录在30天内有效。
短令牌:根据用户ID、用户登录设备ID和一个较短的过期时间如1天,生成短令牌。
用户登录服务对长短令牌分别加密得到数字签名后成功响应客户端,并下发长短令牌。客户端的后续用户请求都会在HTTP请求头中携带长短令牌。
- 长令牌:long_token=SessioriID+数字签名。
- 短令牌:short_token=用户ID+用户登录设备ID+过期时间+数字签名。
客户端登录成功后,所发起的任何请求都携带long_token和short_token字段。
服务端收到请求,先以5.6.2节所列的条件验证短令牌(即请求头中的short_token值),如果验证通过,则用户登录态识别完成;如果验证不通过,原因是令牌已过期,则执行下一步,否则认为令牌非法,要求客户端重新登录。
如果服务端发现短令牌已过期,则读取长令牌(即请求头中的long_token值)。 如果校验数字签名后认为SessionlD被篡改了,则要求客户端重新登录;否则,从用户登录服务中查询SessionlD对应的用户Session数据。
如果Session数据不存在,则说明用户登录态过期,客户端需要重新登录;否则,认为用户登录态识别完成,同时用户登录服务重新生成一个短令牌,再次随着请求的响应下发到客户端。
客户端请求成功得到响应,更新短令牌,便于后续使用,此时用户完全感知不到短令牌的更新。