
查理芒格:激情和天分,哪个更重要?在伯克希尔公司里充满了对自己事业有激情的人。我认为激情比头脑的能力更加重要。
一、鉴权时机不应被忽略
二、三档时机的取舍与首包鉴权的落地
2.1 时机 A:握手前的传输层准入
2.2 时机 B:握手过程中完成认证
2.3 时机 C:握手后首包携带凭证
2.4 首包鉴权的密钥标识与轮换
2.5 登录包顺序约束与伪装包探测
三、大厂APP的做法
四、如何优化
讨论 IM 连接鉴权,更多考虑"用什么凭证"——token 还是证书,对称还是非对称。但真正决定接入层形态的是什么时候鉴权?鉴权这一步,应该放在连接生命周期的哪个时刻、哪个环节。是连接还没建起来就把非法方挡在门外,是在握手协商里顺手认掉,还是连接先建好、等第一个业务包再校验身份?
这里有三种时机的选择,一一道来。
一个客户端要和 IM 服务端通上消息,依次过几道关:传输层把连接建起来(TCP 三次握手、TLS 协商)、应用层把身份认掉、然后写在线表、收发消息。鉴权就夹在"连接建立"和"身份生效"之间。

鉴权夹在"连接建立"与"身份生效(写在线表)"之间
这里鉴权的边界:鉴权发生在写在线表之前。连接管理、死连接判定那一套(本系列另有专篇)都建立在"这条连接已认过、知道是谁"的前提上。身份没认掉,连接就是一条匿名管道。
把上面那条链路按时间切三刀,就是以下三种时机:
最朴素的想法是"把坏人挡在最外面":连接还没爬到应用层,就在传输层或 LB 把它毙掉。常见三种做法——TLS 双向认证(mTLS,客户端也出示证书)、LB 层校验轻量凭证、IP 白名单。
优势:非法方连应用层的门都摸不到,接入层不为脏连接耗资源。在设备可控的场景——IoT、企业内网客户端、固定机房对接——这一档很合适,给每台设备预置证书,mTLS 天然把"未授权设备"挡在握手阶段。
劣势:首先证书的签发、下发、轮换、吊销是一整套 工程,对动辄几万台、版本碎片化的移动端是个无底洞。第二个,mTLS 认的是"设备"不是"用户"——同设备多账号、账号级权限这层认不出来,最后还得在应用层再认一次。
第二档把认证塞进握手本身。WebSocket 场景下,Upgrade 请求的 header 或 URL 带上 token,服务端在响应 101 之前先校验;自研握手协议则在协商对称密钥的同一轮交互里顺带验掉对端身份。连接"建立成功"本身就等价于"认证通过"。
这一档的优雅在于没有"已连接但未认证"的中间态:握手没过连接就不成立,省掉了 C 档那种"匿名连接"窗口。某信的 MMTLS,以及据公开分析 Whats某App 采用的 Noise 协议,都属于这一类——握手即认证。
但这有2个问题,一是握手协议被改造:标准 WebSocket 塞 token 还算轻,要做到"握手即认证 + 密钥协商"往往得自研握手交互,客户端 SDK 和服务端要严格对齐协议版本。二是与重连的张力:移动端频繁断连重连,每次重连都要走完整握手认证,认证若依赖外部服务,重连风暴会直接压到鉴权链路。
维度 | 详情 |
|---|---|
优势 | 无"已连接未认证"中间态,安全窗口最小;认证与密钥协商一体,适合做端到端加密的连接层;握手过即可信 |
代价 | 多需自研握手协议,客户端 / 服务端协议强耦合、版本对齐成本高;频繁重连下握手认证压力大 |
连接先建好,应用层的建连回调里不做任何身份校验——只记建连时间、端类型(web / desktop / mobile)。TLS 由更外层的 LB 集中终结,接入层自己不碰传输层证书。身份认证推迟到第一个业务包:服务端收到首包,取出凭证校验,通过了才把用户身份绑到连接上。
为什么不少中小 IM 项目最后落在这一档?因为它把鉴权和传输层彻底解耦了:二进制协议和 WebSocket 首包里都按同一套格式带凭证,鉴权逻辑收口在一处(多端统一);换 LB、换接入协议都不影响鉴权,鉴权服务独立部署、独立扩缩容(解耦、可扩缩容);鉴权只是应用层的一个包,改协议比改握手交互轻得多(演进很灵活)。
这样的代价主要有一处:连接先建后认,脏连接和扫描流量会先占住接入层资源。首包没到之前这条连接是"在线但匿名"的——既要防它一直不发首包白占连接(首包超时 + 连接数限流),又要防端口扫描、协议探测这类脏流量,靠应用层主动识别报文特征拦掉。时机 A 在握手前就可以解决这个问题,这里就得自己在应用层扛回来。

连接先建立、处于匿名态,首包到达才向独立鉴权服务查身份,校验通过后连接才"具名"并写在线表。
首包里到底带什么?一个很省事的设计是:首包起始的几个字节放一个"密钥标识",而不是凭证本身。比如约定前 8 字节是一个 keyId——它不是 token,只是指向"哪一把密钥"的索引。服务端拿到它,去独立鉴权服务查出对应的 authKey(真正的密钥)和 uid,再用 authKey 配合首包里的其他信息解密 payload ,并校验消息是否被篡改;解密通过才确认对端是合法 uid,把身份绑上连接。
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充当了"密钥版本路由"。这是把凭证本身放进首包做不到的:凭证直接放包里,轮换就得重走完整认证。
时机 C 留了两个安全口子,得在应用层补上。
第一个是顺序约束:既然连接先建后认,就必须强制"第一个业务包必须是登录 / 注册包"。后续非登录包到达时先查连接管理器——这条连接是否已完成登录?没登录直接拒,否则一条匿名连接就能发消息。
第二个是伪装包探测,这是 C 档"连接已建、脏流量已进来"必须自己扛的代价。端口扫描器、协议探测工具会往端口发各种试探报文——HTTP 请求行、SSH 协议头之类——根本不是合法首包。务实的做法是在解码器里加一层 magic / 报文头特征识别:
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 的完整形态是:连接匿名建立 → 首包带密钥标识 → 查鉴权服务解密验身 → 绑定身份写在线表,外加顺序约束和伪装包探测两道应用层防线兜住"连接先建"的代价。
某信基于 TLS 1.3 草案自研了通信安全协议 MMTLS,它把认证和密钥协商放进握手协议里完成:内部分 Record、Handshake、Alert 三个子协议,Handshake 负责客户端与服务端的握手协商,协商出对称密钥及其他密码材料,后续数据全程加密。这是典型的时机 B——握手成功,身份和密钥就一并定下来。
大厂自研而非直接上标准 TLS 的理由很实在:标准 TLS 1.2 每建一个安全连接要额外 1~2 个 RTT,对某信这种频繁短连接、频繁重连的 IM 体验伤害明显;TLS 1.3 草案的 0-RTT 能省掉这部分延迟。
维度 | 详情 |
|---|---|
优势 | 握手即认证 + 密钥协商,无匿名中间态;0-RTT 降低重连延迟;加密对业务层透明,包头包体全保护 |
代价 | 自研握手协议,客户端 / 服务端强耦合、维护成本高;密码学协议自研需持续投入审计;降级路径要专门防降级攻击 |
瓜子某二手车 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 的解耦和多端统一是真香,但"连接先建后认"的代价也真实。这一节顺着代价往下想:怎么用两级准入补脏流量、怎么让鉴权服务这个强依赖不成单点、密钥轮换窗口怎么兼容、匿名连接这段盲区怎么看得见。
时机 C 最实在的短板是脏连接先占资源。但这不意味着只能在应用层硬扛——可以在握手前补一层轻量准入,和首包细粒度鉴权配成"粗筛 + 精认"两级。
握手前这层不做身份认证,只做"明显非法挡掉":连接数限流、单 IP 建连频率限流、可选的 mTLS 做设备级粗粒度准入。它不认你是谁,只把端口扫描、单 IP 海量建连这类一眼可疑的流量在握手阶段拦掉,别让它们走到首包解析。首包那层继续做精细的用户认证。这样既保住 C 档"鉴权与传输层解耦",又借了 A 档"挡得早"——把两档的优点拼起来,而不是二选一。
3.2 那个瓜子的痛点点破了首包鉴权的命门:外部鉴权服务一抖,所有新连接登录全失败。已连上的不受影响,但新登录和重连会集体卡住——而 IM 的重连恰恰是高频事件。这个强依赖必须兜。
三条路各有取舍:其一,自包含凭证(如 JWT),把验签所需信息编进凭证,接入层本地验签,根本不调外部服务——代价是吊销难、泄露窗口长。其二,本地缓存密钥标识到身份的映射,鉴权服务不可用时用缓存放行近期活跃用户——代价是缓存期内吊销有延迟。
秘钥keyId做密钥版本路由(2.4)解决了轮换不断连接的问题,但轮换有个容易翻车的窗口期:客户端已拿新密钥发包,鉴权服务这边新旧密钥生效有先后,可能短暂出现"客户端用新密钥、服务端只认得旧的"。
务实的做法是给轮换留双密钥并存的过渡窗:新密钥下发后,鉴权服务一段时间内新旧两把都认,等绝大多数客户端切到新密钥再作废旧的。窗口多长是个取舍——太短,慢速升级的客户端会被踢;太长,旧密钥的安全收益打折扣。经验上按客户端升级覆盖速度来定,而不是拍固定值。这和灰度发布一脉相承:任何"切换"都要给新旧并存的缓冲,别假设所有客户端同时切换。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。