邵珠庆の日记 生命只有一次,你可以用它来做很多伟大的事情–Make the world a little better and easier

148月/25

只允许单用户登录的处理方式

只允许单用户登录问题的处理方式,详细实现流程:

通过两个关键场景来详细描述实现流程:

场景一:用户在设备A首次登录

  1. 客户端请求登录:

    • 用户在设备A上输入用户名和密码,发送登录请求到认证服务

  2. 认证服务处理:

    • 验证用户名和密码是否正确。

    • 验证通过后,生成一个唯一的会话标识(Session ID),例如使用JWT (JSON Web Token)。这个Session ID将作为后续所有请求的凭证。

    • 【核心步骤】 将用户的活跃会话信息存入 Redis。这里使用一个简单的Key-Value结构:

      • Key: user_active_session:<UserID> (例如: user_active_session:12345)

      • Value: SessionID_A (例如: eyJhbGciOiJIUzI1NiIsIn...)

      • Redis命令: SET user_active_session:12345 "SessionID_A"

  3. 建立实时连接:

    • 认证服务将生成的 SessionID_A 返回给设备A。

    • 设备A的客户端收到 SessionID_A 后,立即向实时通信网关 (WebSocket Gateway) 发起WebSocket连接请求,并在请求中携带 SessionID_A 进行身份验证。

  4. 通信网关注册连接:

    • WebSocket网关收到连接请求,验证 SessionID_A 的合法性(例如解析JWT)。

    • 验证通过后,WebSocket网关会维护一个映射关系,用于未来能根据UserID找到对应的连接。这个映射可以存在网关的内存中,或者也存入Redis中(尤其是在网关是集群部署时)。

      • 映射: UserID -> WebSocketConnectionID (例如: 12345 -> conn_xyz123)

    • 至此,设备A登录成功并保持在线。

场景二:用户在设备B进行新登录(踢出设备A)

  1. 客户端请求登录:

    • 用户在设备B上输入用户名和密码,发送登录请求到认证服务

  2. 认证服务处理(踢出逻辑):

    • 验证用户名和密码。

    • 验证通过后,生成一个新的会话标识 SessionID_B

    • 【核心步骤】 在将新会话写入Redis之前,先获取并替换旧的会话。Redis的 GETSET 命令是原子性的,非常适合此场景。

      • 原子操作: GETSET user_active_session:12345 "SessionID_B"

      • 结果: 这个命令会返回旧的值 SessionID_A,同时将Key的值更新为 SessionID_B。现在,SessionID_B 成为了唯一合法的会话。

  3. 发布“强制下线”事件:

    • 认证服务拿到了旧的 SessionID_A(如果存在的话)。它会立即通过一个内部的消息队列 (如Kafka或RabbitMQ) 发布一个“强制下线”事件。

    • 事件内容: {"event": "FORCE_LOGOUT", "userId": "12345", "oldSessionId": "SessionID_A"}

    • 消息队列为了解耦。认证服务不应该直接与WebSocket网关通信,通过消息队列可以提高系统的健壮性和可扩展性。

  4. 通信网关处理下线事件:

    • 实时通信网关 (WebSocket Gateway) 订阅了“强制下线”事件。

    • 当它收到该事件后,根据 userId: 12345 查找到对应的旧连接 conn_xyz123

    • 【主动踢出】 网关通过 conn_xyz123 这条WebSocket连接,向设备A的客户端主动发送一条消息。

      • 消息内容: {"type": "force_logout", "message": "您已在其他设备登录"}

    • 发送消息后,服务器主动关闭这条WebSocket连接。

  5. 设备A响应:

    • 设备A的客户端收到 force_logout 消息后,应立即执行下线操作:清除本地存储的 SessionID_A 和用户信息,并跳转到登录页面

    • 即使客户端代码出现异常未能正确处理该消息,由于服务器已主动断开连接,设备A也无法再进行任何实时操作。

  6. 设备B完成登录:

    • 与此同时,认证服务已将新的 SessionID_B 返回给设备B。

    • 设备B走与场景一相同的流程,建立新的WebSocket连接并保持在线。

安全性与健壮性:最后的防线

仅仅踢出WebSocket连接是不够的。如果设备A的网络恰好在被踢出前断开,它可能不知道自己已下线。当网络恢复后,它可能会尝试使用旧的 SessionID_A 去请求普通的HTTP API(例如查询订单历史)。

因此,必须有后端防线:

  • API网关/后端服务强校验:

    • 所有需要登录才能访问的API,都必须在API网关或服务内部对请求携带的Session ID进行验证。

    • 验证逻辑:从Redis中根据 user_active_session:<UserID> 取出当前合法的 Session ID,与请求中携带的 Session ID进行比对。

    • 如果请求携带的 SessionID_A 与Redis中存储的 SessionID_B 不匹配,则立即拒绝该请求,返回401 Unauthorized错误。

总结

通过以上设计,我们构建了一个三层防御体系来确保单点登录的实现:

  1. 权威状态层 (Redis): 利用Redis作为唯一、高速的会话状态记录中心。

  2. 主动通知层 (WebSocket): 在新登录发生时,通过WebSocket主动、实时地通知旧客户端下线。

  3. 被动验证层 (API校验): 对每一次API请求都进行会话有效性校验,作为最终的、最可靠的防线,杜绝任何使用旧会话操作的可能性。

这样不仅解决了实时T出旧客户端的问题,而且通过组件解耦和多层防御,保证了系统整体的高性能、高可用和高安全性。

喜欢这个文章吗?

考虑订阅我们的RSS Feed吧!

发布在 邵珠庆

评论 (0) 引用 (0)

很抱歉,评论表单已关闭.

引用被禁用.