首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >IM分布式架构系列(11) TCP 握手鉴权时机 | 握手前还是后?

IM分布式架构系列(11) TCP 握手鉴权时机 | 握手前还是后?

原创
作者头像
拉丁解牛说技术
发布2026-06-06 17:25:08
发布2026-06-06 17:25:08
760
举报

查理芒格:激情和天分,哪个更重要?在伯克希尔公司里充满了对自己事业有激情的人。我认为激情比头脑的能力更加重要。


一、鉴权时机不应被忽略

二、三档时机的取舍与首包鉴权的落地

2.1 时机 A:握手前的传输层准入

2.2 时机 B:握手过程中完成认证

2.3 时机 C:握手后首包携带凭证

2.4 首包鉴权的密钥标识与轮换

2.5 登录包顺序约束与伪装包探测

三、大厂APP的做法

四、如何优化


一、鉴权时机不应被忽略

讨论 IM 连接鉴权,更多考虑"用什么凭证"——token 还是证书,对称还是非对称。但真正决定接入层形态的是什么时候鉴权?鉴权这一步,应该放在连接生命周期的哪个时刻、哪个环节。是连接还没建起来就把非法方挡在门外,是在握手协商里顺手认掉,还是连接先建好、等第一个业务包再校验身份?

这里有三种时机的选择,一一道来。

1.1 鉴权在接入层链路中的位置

一个客户端要和 IM 服务端通上消息,依次过几道关:传输层把连接建起来(TCP 三次握手、TLS 协商)、应用层把身份认掉、然后写在线表、收发消息。鉴权就夹在"连接建立"和"身份生效"之间。

鉴权夹在"连接建立"与"身份生效(写在线表)"之间

这里鉴权的边界:鉴权发生在写在线表之前。连接管理、死连接判定那一套(本系列另有专篇)都建立在"这条连接已认过、知道是谁"的前提上。身份没认掉,连接就是一条匿名管道。

1.2 三种时机

把上面那条链路按时间切三刀,就是以下三种时机:

  • 时机 A(握手前 / 传输层鉴权):连接还没交给应用层,就在传输层或 LB 把非法方挡掉——mTLS 校验客户端证书、LB 层校验凭证、IP 白名单。挡得最早,脏流量根本进不来。
  • 时机 B(握手中鉴权):在握手过程里完成认证——WebSocket Upgrade 的 header / URL 带 token、握手协议协商密钥时顺带认掉身份。建立和认证是同一个动作。
  • 时机 C(握手后首包鉴权):连接先建好,第一个业务包携带凭证,服务端收到首包才校验身份。连接先在线、后具名。

二、三档时机的取舍与首包鉴权方式实践

2.1 时机 A:握手前的传输层准入

最朴素的想法是"把坏人挡在最外面":连接还没爬到应用层,就在传输层或 LB 把它毙掉。常见三种做法——TLS 双向认证(mTLS,客户端也出示证书)、LB 层校验轻量凭证、IP 白名单。

优势:非法方连应用层的门都摸不到,接入层不为脏连接耗资源。在设备可控的场景——IoT、企业内网客户端、固定机房对接——这一档很合适,给每台设备预置证书,mTLS 天然把"未授权设备"挡在握手阶段。

劣势:首先证书的签发、下发、轮换、吊销是一整套 工程,对动辄几万台、版本碎片化的移动端是个无底洞。第二个,mTLS 认的是"设备"不是"用户"——同设备多账号、账号级权限这层认不出来,最后还得在应用层再认一次。

2.2 时机 B:握手过程中完成认证

第二档把认证塞进握手本身。WebSocket 场景下,Upgrade 请求的 header 或 URL 带上 token,服务端在响应 101 之前先校验;自研握手协议则在协商对称密钥的同一轮交互里顺带验掉对端身份。连接"建立成功"本身就等价于"认证通过"。

这一档的优雅在于没有"已连接但未认证"的中间态:握手没过连接就不成立,省掉了 C 档那种"匿名连接"窗口。某信的 MMTLS,以及据公开分析 Whats某App 采用的 Noise 协议,都属于这一类——握手即认证。

但这有2个问题,一是握手协议被改造:标准 WebSocket 塞 token 还算轻,要做到"握手即认证 + 密钥协商"往往得自研握手交互,客户端 SDK 和服务端要严格对齐协议版本。二是与重连的张力:移动端频繁断连重连,每次重连都要走完整握手认证,认证若依赖外部服务,重连风暴会直接压到鉴权链路。

维度

详情

优势

无"已连接未认证"中间态,安全窗口最小;认证与密钥协商一体,适合做端到端加密的连接层;握手过即可信

代价

多需自研握手协议,客户端 / 服务端协议强耦合、版本对齐成本高;频繁重连下握手认证压力大

2.3 时机 C:握手后首包携带凭证

连接先建好,应用层的建连回调里不做任何身份校验——只记建连时间、端类型(web / desktop / mobile)。TLS 由更外层的 LB 集中终结,接入层自己不碰传输层证书。身份认证推迟到第一个业务包:服务端收到首包,取出凭证校验,通过了才把用户身份绑到连接上。

为什么不少中小 IM 项目最后落在这一档?因为它把鉴权和传输层彻底解耦了:二进制协议和 WebSocket 首包里都按同一套格式带凭证,鉴权逻辑收口在一处(多端统一);换 LB、换接入协议都不影响鉴权,鉴权服务独立部署、独立扩缩容(解耦、可扩缩容);鉴权只是应用层的一个包,改协议比改握手交互轻得多(演进很灵活)。

这样的代价主要有一处:连接先建后认,脏连接和扫描流量会先占住接入层资源。首包没到之前这条连接是"在线但匿名"的——既要防它一直不发首包白占连接(首包超时 + 连接数限流),又要防端口扫描、协议探测这类脏流量,靠应用层主动识别报文特征拦掉。时机 A 在握手前就可以解决这个问题,这里就得自己在应用层扛回来。

连接先建立、处于匿名态,首包到达才向独立鉴权服务查身份,校验通过后连接才"具名"并写在线表。

2.4 首包鉴权的密钥标识与轮换

首包里到底带什么?一个很省事的设计是:首包起始的几个字节放一个"密钥标识",而不是凭证本身。比如约定前 8 字节是一个 keyId——它不是 token,只是指向"哪一把密钥"的索引。服务端拿到它,去独立鉴权服务查出对应的 authKey(真正的密钥)和 uid,再用 authKey 配合首包里的其他信息解密 payload ,并校验消息是否被篡改;解密通过才确认对端是合法 uid,把身份绑上连接。

代码语言:javascript
复制
on_first_packet(conn, packet):
    auth_key_id = packet.read_bytes(0, 32)           // 起始 32 字节是密钥标识,非凭证本身
    cached = conn.cached_auth_key
    if cached is None or key_id_of(cached) != auth_key_id:
        auth_key, uid = auth_service.lookup(auth_key_id)   
        conn.cached_auth_key = auth_key
    else:
        auth_key, uid = cached, conn.uid
    if not verify_and_decrypt(packet, auth_key):    // 关键:msgKey 校验失败即拒,防篡改
        close(conn); return
    bind_identity(conn, uid)

这个"标识而非凭证"的设计,有另一个好处,这个可以顺手解决了密钥轮换需要重连问题:当连接上缓存记录的authKey,和最新包里密钥索引不一致的时候,说明密钥轮换了,重新去秘钥鉴权服务拉新的。轮换全程不用断连接,这个keyId充当了"密钥版本路由"。这是把凭证本身放进首包做不到的:凭证直接放包里,轮换就得重走完整认证。

2.5 登录包顺序约束与伪装包探测

时机 C 留了两个安全口子,得在应用层补上。

第一个是顺序约束:既然连接先建后认,就必须强制"第一个业务包必须是登录 / 注册包"。后续非登录包到达时先查连接管理器——这条连接是否已完成登录?没登录直接拒,否则一条匿名连接就能发消息。

第二个是伪装包探测,这是 C 档"连接已建、脏流量已进来"必须自己扛的代价。端口扫描器、协议探测工具会往端口发各种试探报文——HTTP 请求行、SSH 协议头之类——根本不是合法首包。务实的做法是在解码器里加一层 magic / 报文头特征识别:

代码语言:javascript
复制
on_first_bytes(conn, bytes):
    if looks_like_http(bytes) or looks_like_ssh(bytes):   // 识别探测/扫描报文
        close(conn); return            // 或回一个假响应,不暴露真实协议
    if bytes[0..N] != EXPECTED_MAGIC:  // 不符合自有协议帧头
        close(conn); return
    proceed_to_decode(conn, bytes)

另外心跳包处理问题:心跳包通常在解码阶段就可以被单独识别,可以直接绕过鉴权(心跳判死那套机制本系列另有专篇),它不需要也不应该承载身份。

把这几点串起来,时机 C 的完整形态是:连接匿名建立 → 首包带密钥标识 → 查鉴权服务解密验身 → 绑定身份写在线表,外加顺序约束和伪装包探测两道应用层防线兜住"连接先建"的代价。

三、大厂APP的做法

3.1 某信:握手内认证的 MMTLS

某信基于 TLS 1.3 草案自研了通信安全协议 MMTLS,它把认证和密钥协商放进握手协议里完成:内部分 Record、Handshake、Alert 三个子协议,Handshake 负责客户端与服务端的握手协商,协商出对称密钥及其他密码材料,后续数据全程加密。这是典型的时机 B——握手成功,身份和密钥就一并定下来。

大厂自研而非直接上标准 TLS 的理由很实在:标准 TLS 1.2 每建一个安全连接要额外 1~2 个 RTT,对某信这种频繁短连接、频繁重连的 IM 体验伤害明显;TLS 1.3 草案的 0-RTT 能省掉这部分延迟。

维度

详情

优势

握手即认证 + 密钥协商,无匿名中间态;0-RTT 降低重连延迟;加密对业务层透明,包头包体全保护

代价

自研握手协议,客户端 / 服务端强耦合、维护成本高;密码学协议自研需持续投入审计;降级路径要专门防降级攻击

3.2 瓜子 IM:首包 token 与自包含凭证

瓜子某二手车 IM 团队曾公开过一套很有参考价值的方案是关于用JWT 技术解决 IM 系统 Socket 长连接的身份认证痛点。他们最初是标准的时机 C:客户端从统一登录系统 SSO 拿到 token,建长连接时传给 IM Server,IM Server 再请求 SSO 确认 token 合法性

这套在移动端暴露出一个尖锐痛点,印证了首包鉴权依赖外部服务的命门:移动端进地铁、换基站频繁断连重连,长连接被反复重建;每次重建都要 IM Server 调一次 SSO,重连越频繁,外部鉴权系统压力越大,SSO 一开小差新连接登录就受影响。他们的改良是引入 JWT——把身份和签名直接编进 token,IM Server 用本地公钥就能验签,不必每次都回调外部 SSO,把"每次重连都查外部服务"降成"本地校验"。

这条经验对所有走时机 C 的项目都通用:首包鉴权解耦的代价,是外部鉴权服务成了新连接登录的强依赖,要么像 JWT 那样做成自包含、要么做本地缓存兜底。

维度

详情

优势

首包 token 实现简单、与传输层解耦;JWT 自包含、本地验签,削掉重连时对外部鉴权服务的高频回调

代价

自包含 token 难即时吊销,签发后到期前一直有效;token 一旦泄露在有效期内可被冒用;密钥轮换需配套版本机制

四、如何优化提升

时机 C 的解耦和多端统一是真香,但"连接先建后认"的代价也真实。这一节顺着代价往下想:怎么用两级准入补脏流量、怎么让鉴权服务这个强依赖不成单点、密钥轮换窗口怎么兼容、匿名连接这段盲区怎么看得见。

4.1 粗筛加精认的两级准入

时机 C 最实在的短板是脏连接先占资源。但这不意味着只能在应用层硬扛——可以在握手前补一层轻量准入,和首包细粒度鉴权配成"粗筛 + 精认"两级

握手前这层不做身份认证,只做"明显非法挡掉":连接数限流、单 IP 建连频率限流、可选的 mTLS 做设备级粗粒度准入。它不认你是谁,只把端口扫描、单 IP 海量建连这类一眼可疑的流量在握手阶段拦掉,别让它们走到首包解析。首包那层继续做精细的用户认证。这样既保住 C 档"鉴权与传输层解耦",又借了 A 档"挡得早"——把两档的优点拼起来,而不是二选一。

4.2 鉴权服务可用性的兜底

3.2 那个瓜子的痛点点破了首包鉴权的命门:外部鉴权服务一抖,所有新连接登录全失败。已连上的不受影响,但新登录和重连会集体卡住——而 IM 的重连恰恰是高频事件。这个强依赖必须兜。

三条路各有取舍:其一,自包含凭证(如 JWT),把验签所需信息编进凭证,接入层本地验签,根本不调外部服务——代价是吊销难、泄露窗口长。其二,本地缓存密钥标识到身份的映射,鉴权服务不可用时用缓存放行近期活跃用户——代价是缓存期内吊销有延迟。

4.3 密钥轮换窗口期的兼容

秘钥keyId做密钥版本路由(2.4)解决了轮换不断连接的问题,但轮换有个容易翻车的窗口期:客户端已拿新密钥发包,鉴权服务这边新旧密钥生效有先后,可能短暂出现"客户端用新密钥、服务端只认得旧的"。

务实的做法是给轮换留双密钥并存的过渡窗:新密钥下发后,鉴权服务一段时间内新旧两把都认,等绝大多数客户端切到新密钥再作废旧的。窗口多长是个取舍——太短,慢速升级的客户端会被踢;太长,旧密钥的安全收益打折扣。经验上按客户端升级覆盖速度来定,而不是拍固定值。这和灰度发布一脉相承:任何"切换"都要给新旧并存的缓冲,别假设所有客户端同时切换。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、鉴权时机不应被忽略
    • 1.1 鉴权在接入层链路中的位置
    • 1.2 三种时机
  • 二、三档时机的取舍与首包鉴权方式实践
    • 2.1 时机 A:握手前的传输层准入
    • 2.2 时机 B:握手过程中完成认证
    • 2.3 时机 C:握手后首包携带凭证
    • 2.4 首包鉴权的密钥标识与轮换
    • 2.5 登录包顺序约束与伪装包探测
  • 三、大厂APP的做法
    • 3.1 某信:握手内认证的 MMTLS
    • 3.2 瓜子 IM:首包 token 与自包含凭证
  • 四、如何优化提升
    • 4.1 粗筛加精认的两级准入
    • 4.2 鉴权服务可用性的兜底
    • 4.3 密钥轮换窗口期的兼容
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档