把 WebSocket 长期密钥移出前端:短期票据改造与上线闭环
关闭 Source Map 不能让浏览器中的长期密钥变得安全。本文复盘一次 STOMP 鉴权改造:登录后签发短期票据、服务端兼容旧框架,再通过负向测试和密钥轮换完成上线闭环。
🎧 文章导读
前端环境变量,不是密钥保险箱
这次排查的入口,是生产前端环境文件里的 WebSocket 固定凭证。它随着构建进入 JavaScript,再作为 STOMP CONNECT 头发送到服务端。只要浏览器需要使用它,用户就能够从资源或运行过程里取得它。
关闭 Source Map 可以减少源码暴露,混淆可以增加阅读成本;两者都不能让发送给浏览器的长期密钥保密。修复方向应是:长期密钥留在服务端,浏览器只拿短期、限定用途的连接票据。
图 1:票据由已登录用户申请;长期凭证不再随前端构建分发。
这篇复盘记录的是一次兼容既有框架的增量改造,示例中的凭证、用户和服务名称均做了替换,不包含原环境的真实密钥。
先划清 HTTP 握手与 STOMP CONNECT
WebSocket 建连成功,不等于 STOMP 认证通过,更不等于用户有权订阅任意主题。排查时至少要区分三个层次:HTTP/SockJS 传输、STOMP 连接认证、后续消息与目的地授权。
浏览器原生 WebSocket 和 SockJS 对自定义 HTTP 请求头存在限制。Spring 官方给出的一个接入方式是,在 STOMP 连接帧中传认证信息,再通过 ChannelInterceptor 处理。这里应当理解为消息协议层的头,不要与握手 HTTP 头混用。参见 Spring Token Authentication。
脱敏后的前端示意如下。getWsTicket 必须使用已有登录态,生产传输应使用 HTTPS/WSS:
1 | async function connectAlarmChannel(stompClient) { |
握手路径的放行也要和协议层认证配合。本次曾遇到一个服务的 SockJS 路径没有进入预期白名单,导致请求还没走到票据拦截器就被挡住。修复路径规则时,应逐项确认传输与消息认证仍然存在,不能用“一律放开 WebSocket”替代鉴权。
票据校验,要检查的不只是签名
本次票据由用户标识、服务用途、过期时间和 HMAC-SHA256 签名组成,默认有效期 5 分钟。它是项目自定义格式,不应直接称为标准 JWT。
1 | payload = userId + "." + audience + "." + expiresAt |
这只是字段结构示意,实际实现还需约定编码、分隔符和时间单位。服务端校验应包含格式、签名、过期时间和用途;用户身份来自受保护的签发接口,不能由浏览器随意指定。
| 检查项 | 解决的问题 |
|---|---|
| 签发接口要求登录 | 匿名用户不能直接申请票据 |
| 签名验证 | 字段不能被随意修改 |
expiresAt |
缩短被窃取票据的可用时间 |
audience |
一个服务的票据不能直接用于另一个服务 |
| 常量时间比较 | 降低签名比较引入的时序差异 |
短 TTL 只缩短重放窗口,不意味着一次性消费。本次没有实现 nonce 去重或主动吊销,也没有在这次记录中证明所有订阅主题都已按用户隔离。连接认证和业务授权仍须分别检查。
没有框架源码,怎样渐进接入
原认证逻辑封装在既有框架 jar 内,按配置中的静态客户端凭证校验。直接替换框架不在此次范围内,所以业务层注册优先执行的拦截器:先校验票据,再在服务端内部补齐旧框架需要的认证头,让已有后续流程继续工作。
这里最重要的约束是“服务端内部”。长期密钥不能出现在取票接口响应,也不能回写给前端。适配层的执行顺序则必须用实际配置和测试证明,不能只看到一个 @Order 注解就认为一定正确。
1 | // 适配思路示意:省略具体框架 API 和异常处理。 |
上面是便于解释的伪代码,不能直接当成可复制的完整实现。项目里的真实改动还涉及可变消息头和原框架拦截器兼容;迁移到其他 Spring 版本时,应重新验证注册顺序与消息头生命周期。
图 2:新前端上线只是中间节点,关闭旧认证和轮换泄露密钥才形成闭环。
上线最容易漏掉的是密钥轮换
本次为了渐进发布,提供了旧凭证兼容开关。先上线支持票据的后端,再升级前端,观察旧认证回落日志是否仍出现,最后关闭旧认证。
但还有一步不能漏:轮换已经暴露过的长期密钥。 本次历史设计复用该密钥进行票据 HMAC 签名;如果只从前端删掉它,知道旧密钥的人仍可能签出合法外观的票据。
1 | # 示例配置,不包含真实凭证。 |
稳妥的发布顺序是:后端兼容上线、前端取票上线、确认连接和业务消息、观察旧客户端、关闭兼容并轮换密钥、再次验证。若准备回滚,也应明确恢复旧认证会重新引入什么风险,不能把兼容开关永远留在打开状态。
原笔记列出的 E2E 检查包含匿名取票拒绝、两服务取票和连接、旧凭证兼容、假票据、空凭证、跨服务票据与篡改票据等,记录为 10/10 通过;另外验证了关闭兼容后的行为。本篇未重新运行这些业务测试。
连接正常,为什么还是没有告警弹窗
最后一个排障转折很有代表性:浏览器取票成功、连接成功,界面却没有弹窗。继续查看发现,规则中的弹窗接收人名单并不包含测试用户,前端因此正确过滤了消息。
所以验收不能只看“连上了”,也不能只看“没弹窗”。应逐层确认:取票结果、协议连接、业务事件生成、通知接收人、消息到达与页面展示。WebSocket 连接修复,不会自动修好一条错误的通知名单。
这次改造记录了编码、测试与合并,但上线关闭兼容、轮换密钥仍列为待办。写博客时保留这一状态,比一句“安全整改完成”更有价值:它提醒我们,凭证整改需要走完分发、校验、迁移和撤销的完整生命周期。
延伸阅读
- 命令发送成功,数据却没回来:一次设备同步故障与补录设计
- 从一条告警到真正闭环:规则、站内信、工单与权限的整合实践
- 8.7 秒到 231 毫秒:保留统计口径的 MySQL 窗口函数优化
- 上传上限放到 1GB,为什么仍然 OOM:附件复用与流式传输实践
本文根据 2026 年 9 月 22 日至 10 月 22 日的工程笔记整理。账号、凭证、地址和业务样本已脱敏;通用示例已在正文标明,历史验证结果以原记录为准。