From eb96028029e58e24851dd5b33159bc32420bfbeb Mon Sep 17 00:00:00 2001 From: wx <2636507191@qq.com> Date: Sun, 19 Jul 2026 15:40:59 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=90=8E=E5=8F=B0=20SSE=20=E9=89=B4=E6=9D=83=E4=B8=8E=E9=87=8D?= =?UTF-8?q?=E8=BF=9E=E4=BF=AE=E5=A4=8D=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...¯SSE鉴æƒé‡è¿žä¸Žæ—§ä¼šè¯æ¢å¤-å‰ç«¯å¾…处ç†-管ç†åŽå°.md | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 changelogs-v2/2026-07/66_管ç†åŽå°æ¶ˆæ¯SSE鉴æƒé‡è¿žä¸Žæ—§ä¼šè¯æ¢å¤-å‰ç«¯å¾…处ç†-管ç†åŽå°.md diff --git a/changelogs-v2/2026-07/66_管ç†åŽå°æ¶ˆæ¯SSE鉴æƒé‡è¿žä¸Žæ—§ä¼šè¯æ¢å¤-å‰ç«¯å¾…处ç†-管ç†åŽå°.md b/changelogs-v2/2026-07/66_管ç†åŽå°æ¶ˆæ¯SSE鉴æƒé‡è¿žä¸Žæ—§ä¼šè¯æ¢å¤-å‰ç«¯å¾…处ç†-管ç†åŽå°.md new file mode 100644 index 0000000..1de6f27 --- /dev/null +++ b/changelogs-v2/2026-07/66_管ç†åŽå°æ¶ˆæ¯SSE鉴æƒé‡è¿žä¸Žæ—§ä¼šè¯æ¢å¤-å‰ç«¯å¾…处ç†-管ç†åŽå°.md @@ -0,0 +1,160 @@ +# ã€å‰ç«¯å¾…处ç†Â·ç®¡ç†åŽå°ã€‘管ç†åŽå°æ¶ˆæ¯ SSE 鉴æƒé‡è¿žä¸Žæ—§ä¼šè¯æ¢å¤ + +> **模å—**:管ç†åŽå°å…¨å±€æ¶ˆæ¯ / åœ¨çº¿çŠ¶æ€ / èŠå¤©ä¿¡ä»¤ | **æœåŠ¡**:`hl-gateway` + `hl-user-service`
+> **类型**:å‰ç«¯å¾…å¤„ç† + è”调告知 | **æ›´æ–°æ—¶é—´**:2026-07-19
+> **å½±å“范围**:管ç†åŽå°å…¨å±€ SSE 连接ã€é¡¶éƒ¨æœªè¯»è§’æ ‡ã€èŠå¤©ã€åœ¨çº¿çжæ€ä¸ŽæŠ¢å•池信令
+> **状æ€**:åŽç«¯å·²å®Œæˆæ ¹å› å®šä½ï¼›å‰ç«¯å°šæœªä¿®å¤ï¼›æŽ¥å£å¥‘çº¦æœªå˜ + +## 1. 结论与处ç†ä¼˜å…ˆçº§ + +> âš ï¸ 2026-07-19 测试环境å¯ç”¨ SSE 连接角色一致性校验åŽï¼Œå‘布å‰å·²ç­¾å‘且ä»åœ¨æœ‰æ•ˆæœŸå†…的旧登录会è¯å¯èƒ½ç¼ºå°‘当å‰è§’色标记。此时网关能够识别 access token,但用户æœåŠ¡ä¼šæ‹’ç»å»ºç«‹ SSE,å‰ç«¯å½“å‰å®žçŽ°ä¼šæŒç»­ä½¿ç”¨åŒä¸€ç™»å½•ä¼šè¯æ— é™é‡è¿žã€‚ + +- æŽ¥å£ URLã€HTTP 方法ã€äº‹ä»¶ç»“æž„å‡æœªä¿®æ”¹ã€‚ +- è¿™ä¸æ˜¯ `token` Query 傿•°å写错;当å‰å‰ç«¯ URL 拼接方å¼ä¸Žç½‘å…³è¯»å–æ–¹å¼ä¸€è‡´ã€‚ +- **ç”¨æˆ·ç«‹å³æ¢å¤æ–¹å¼**:退出当å‰è´¦å·ï¼Œé‡æ–°ç™»å½•并选择当å‰è§’色,å†å»ºç«‹ SSE。 +- **å‰ç«¯å¿…须处ç†**:Token/角色å˜åŒ–时主动é‡å»ºè¿žæŽ¥ã€é™åˆ¶è¿žç»­å¤±è´¥é‡è¯•ã€ç»™å‡ºé‡æ–°ç™»å½•æç¤ºï¼Œå¹¶æ¶ˆé™¤é»˜è®¤ `message` 事件的é‡å¤æ³¨å†Œã€‚ +- 本次现象包å«åŽç«¯å‘å¸ƒå‰æ—§ä¼šè¯å…¼å®¹é—®é¢˜ï¼›å‰ç«¯æ”¹é€ ç”¨äºŽæ­£ç¡®ç®¡ç†è¿žæŽ¥ç”Ÿå‘½å‘¨æœŸå’Œé¿å…æ— é™é‡è¯•,ä¸ä»£è¡¨æŠŠåŽç«¯å…¼å®¹è´£ä»»è½¬ç§»ç»™å‰ç«¯ã€‚ + +## 2. 当剿ޥå£å¥‘约 + +```http +GET /ws/admin-msg/stream?token= +Accept: text/event-stream +``` + +- 认è¯ï¼šç®¡ç†åŽå° access token。 +- 当å‰ä½¿ç”¨åŽŸç”Ÿ `EventSource`,æµè§ˆå™¨ API ä¸èƒ½è‡ªå®šä¹‰ `Authorization` Header,因此现有实现通过 Query 傿•°ä¼ é€’ token。 +- `token` å¿…é¡»ä½¿ç”¨å½“å‰ Store 中的 access token,并通过 `encodeURIComponent` åš URL ç¼–ç ã€‚ +- æˆåŠŸå»ºè¿žåŽï¼Œè¯·æ±‚åº”é•¿æœŸä¿æŒ `Pending`,å“应类型为 `text/event-stream`。 +- é¦–ä¸ªæ¡æ‰‹äº‹ä»¶ï¼š + +```text +event: connected +data: ok +``` + +- åŽç»­ä»æ²¿ç”¨çŽ°æœ‰å…·å事件,包括 `unread-count`ã€`im-chat`ã€`im-chat-read`ã€`presence` å’Œ `grab-pool-changed`;本次没有修改事件数æ®ç»“构。 + +## 3. 已确认的问题链路 + +### 3.1 旧登录会è¯ä¸Žæ–°è§’色标记ä¸å…¼å®¹ + +测试环境è¿è¡Œé“¾è·¯å·²ç¡®è®¤ï¼š + +1. 网关å¯ä»¥ä»Ž `?token=` 读å–并校验管ç†åŽå° JWT。 +2. 网关å‘用户æœåŠ¡è½¬å‘å¯ä¿¡çš„管ç†å‘˜èº«ä»½åŠè§’色信æ¯ã€‚ +3. 用户æœåŠ¡åœ¨ä¸‹å‘任何 SSE æ•°æ®å‰æ ¡éªŒâ€œè¿žæŽ¥è§’色是å¦ä»ä¸ºå½“å‰ç™»å½•角色â€ã€‚ +4. å‘布å‰ç­¾å‘çš„æ—§ç™»å½•ä¼šè¯æ²¡æœ‰åˆå§‹åŒ–新角色标记时,校验按安全策略失败并关闭连接。 +5. æ–°ç™»å½•æˆ–é‡æ–°é€‰æ‹©è§’è‰²ä¼šé‡æ–°å†™å…¥è§’è‰²æ ‡è®°ï¼Œå› æ­¤é‡æ–°ç™»å½•åŽå¯æ¢å¤ã€‚ + +该校验采用 fail-closed(失败时拒ç»ï¼‰ç­–略,目的是é¿å…角色切æ¢åŽæ—§ Token 继续接收ä¸å±žäºŽå½“å‰è§’色的消æ¯ã€‚ + +### 3.2 å‰ç«¯å½“å‰ä¼šæ— é™é‡è¯•åŒä¸€å¤±è´¥ä¼šè¯ + +å½“å‰ `src/composables/useAdminMessageSSE.js` 在 `EventSource.onerror` åŽæ‰§è¡Œå…³é—­å’ŒæŒ‡æ•°é€€é¿ï¼Œä½†æ²¡æœ‰è¿žç»­å¤±è´¥ä¸Šé™ï¼Œä¹Ÿæ²¡æœ‰è§¦å‘釿–°ç™»å½•æˆ–é‰´æƒæ¢å¤æµç¨‹ã€‚ + +原生 `EventSource.onerror` 䏿š´éœ² HTTP 状æ€ç å’Œå“应正文,å‰ç«¯ä¸èƒ½ä»…凭 `onerror` 精确区分 401/403ã€æœåŠ¡å¼‚å¸¸å’Œä¸´æ—¶æ–­ç½‘ã€‚å› æ­¤ä¸èƒ½æŠŠæ‰€æœ‰é”™è¯¯éƒ½ç›´æŽ¥åˆ¤å®šä¸º Token 失效,但必须é™åˆ¶æ— ä¼‘æ­¢é‡è¿žã€‚ + +### 3.3 默认 `message` 事件被é‡å¤æ³¨å†Œ + +当å‰å®žçŽ°åŒæ—¶æ³¨å†Œï¼š + +```js +es.onmessage = handleMessage +es.addEventListener('message', handleMessage) +``` + +两ç§å†™æ³•都会监å¬é»˜è®¤ `message` äº‹ä»¶ï¼Œå¹¶ä¸æ˜¯äº’斥兜底。åŽç«¯å‘é€é»˜è®¤ `message` 时,åŒä¸€æ•°æ®å¯èƒ½è¢«å¤„ç†ä¸¤æ¬¡ï¼Œå¿…é¡»åªä¿ç•™ä¸€ç§æ³¨å†Œæ–¹å¼ã€‚ + +## 4. ç”¨æˆ·ç«‹å³æ¢å¤æ­¥éª¤ + +1. 关闭当å‰é¡µé¢äº§ç”Ÿçš„æ—§ SSE 连接。 +2. 正常退出管ç†åŽå°ã€‚ +3. 釿–°ç™»å½•ï¼Œå¹¶é‡æ–°é€‰æ‹©å½“å‰éœ€è¦ä½¿ç”¨çš„角色。 +4. 进入主布局åŽé‡æ–°å»ºç«‹ `/ws/admin-msg/stream`。 +5. 在æµè§ˆå™¨ Network ä¸­ç¡®è®¤è¯·æ±‚ä¿æŒ `Pending`,并收到一次 `connected` 事件。 + +ä¸è¦é€šè¿‡æ‰‹å·¥å¤åˆ¶ã€ä¿®æ”¹æˆ–åœ¨åœ°å€æ ç²˜è´´å®Œæ•´ Token çš„æ–¹å¼æ¢å¤è¿žæŽ¥ã€‚ + +## 5. ã€å‰ç«¯Â·ç®¡ç†åŽå°ã€‘适酿¸…å• + +### 5.1 让 SSE 生命周期跟éšç™»å½•凭è¯å’Œè§’色 + +- [ ] ç›‘å¬ `userStore.token` å˜åŒ–;值å˜åŒ–时先关闭旧 `EventSource`,å†ä½¿ç”¨æœ€æ–° Token 建立唯一的新连接。 +- [ ] è§’è‰²åˆ‡æ¢æˆåŠŸå¹¶æ›´æ–° Token åŽï¼Œç«‹å³é‡å»º SSE,ä¸ç­‰å¾…旧连接自行报错。 +- [ ] 登出ã€ä¸»å¸ƒå±€å¸è½½æˆ– Token è¢«æ¸…ç©ºæ—¶ï¼Œå…³é—­è¿žæŽ¥ã€æ¸…ç†é‡è¿žå®šæ—¶å™¨å¹¶ç¦æ­¢å†æ¬¡æ‹‰èµ·ã€‚ +- [ ] ä¿è¯å…¨å±€æœ€å¤šåªæœ‰ä¸€ä¸ªç®¡ç†åŽå°æ¶ˆæ¯ SSE 实例,é¿å…布局é‡å¤æŒ‚载造æˆå¤šè¿žæŽ¥ã€‚ +- [ ] é‡å»ºè¿žæŽ¥æ—¶å§‹ç»ˆä»Ž Store çŽ°å– Token,ä¸ç¼“存旧登录会è¯ä¸­çš„ Token 字符串。 + +### 5.2 é™åˆ¶è¿žç»­å¤±è´¥ï¼Œé¿å…æ— é™é‡è¿ž + +- [ ] ä¿ç•™æŒ‡æ•°é€€é¿å’Œæœ€å¤§é—´éš”,但增加“连续失败次数/总时长â€ä¸Šé™ã€‚ +- [ ] **仅在收到åŽç«¯ `connected` 事件åŽ**清零连续失败计数;`EventSource.onopen` ä¸èƒ½ä½œä¸ºé‰´æƒæˆåŠŸä¾æ®ï¼Œä¹Ÿä¸èƒ½æ¸…零计数。 +- [ ] 达到上é™åŽåœæ­¢è‡ªåЍé‡è¯•,并显示中性ã€å¯æ“作的æç¤ºï¼Œä¾‹å¦‚“消æ¯è¿žæŽ¥è¿žç»­å¤±è´¥ï¼Œè¯·æ£€æŸ¥ç½‘ç»œæˆ–é‡æ–°ç™»å½•â€ã€‚ +- [ ] 用户完æˆé‡æ–°ç™»å½•ã€Token 刷新ã€è§’è‰²åˆ‡æ¢æˆ–主动点击é‡è¯•åŽï¼Œæ‰å¼€å¯æ–°ä¸€è½®è¿žæŽ¥ã€‚ +- [ ] 临时断网æ¢å¤åŽä»å…许é‡è¿žï¼›å¯ç»“åˆ `online` 事件或显å¼é‡è¯•入壿¢å¤ï¼Œè€Œä¸æ˜¯æ°¸ä¹…é™é»˜å¤±æ•ˆã€‚ + +> 注æ„:由于原生 `EventSource` 无法在 `onerror` 中读å–å“应状æ€ï¼Œå‰ç«¯ä¸è¦æ ¹æ®ä¸€æ¬¡ `onerror` ç«‹å³æ¸…空登录æ€ã€‚需è¦ä½¿ç”¨è¿žç»­å¤±è´¥é˜ˆå€¼ï¼Œå¹¶ç»“åˆæ™®é€šé‰´æƒæŽ¥å£ç»“果或既有 Token 刷新状æ€åˆ¤æ–­ã€‚ + +### 5.3 消除é‡å¤æ¶ˆæ¯å¤„ç† + +- [ ] `es.onmessage` 与 `es.addEventListener('message', ...)` åªä¿ç•™ä¸€ç§ã€‚ +- [ ] `connected`ã€`unread-count`ã€`im-chat`ã€`im-chat-read`ã€`presence`ã€`grab-pool-changed` 等具å事件继续分别注册。 +- [ ] 验è¯å•æ¡é»˜è®¤ `message`ã€èŠå¤©ä¿¡ä»¤å’Œæœªè¯»æ•°ä¿¡ä»¤éƒ½åªè¢«ä¸šåŠ¡å±‚æ¶ˆè´¹ä¸€æ¬¡ã€‚ + +### 5.4 失败信æ¯ä¸Žè”è°ƒå馈 + +- [ ] å‰ç«¯æç¤ºä¸­ä¸è¦å±•示 Tokenã€å®Œæ•´ SSE URLã€Cookie 或管ç†å‘˜æ ‡è¯†ã€‚ +- [ ] 如釿–°ç™»å½•åŽä»å¤±è´¥ï¼Œåªå馈å‘生时间ã€é¡µé¢ã€é”™è¯¯ `message` å’Œ `X-Trace-Id`。 +- [ ] è‹¥ Network 原始å“应确实为“未æä¾›æœ‰æ•ˆçš„Tokenâ€ï¼Œè¯·é™„ `X-Trace-Id` 交åŽç«¯ç»§ç»­æ£€æŸ¥è·¯ç”±/拦截器链;ä¸è¦é™„ Token。 + +## 6. 验收场景 + +| 场景 | 期望结果 | +|---|---| +| 釿–°ç™»å½•åŽé¦–次进入主布局 | åªå»ºç«‹ 1 æ¡ SSEï¼›è¯·æ±‚ä¿æŒ `Pending`;收到 1 次 `connected` | +| access token 刷新 | 旧连接关闭,使用新 Token åªé‡å»º 1 次 | +| 切æ¢ç®¡ç†åŽå°è§’色 | 旧角色连接立å³å…³é—­ï¼›æ–°è§’色 Token å»ºç«‹æ–°è¿žæŽ¥ï¼›ä¸æŽ¥æ”¶æ—§è§’è‰²åŽç»­æ•°æ® | +| å‘å¸ƒå‰æ—§ä¼šè¯æ— æ³•建连 | 退é¿é‡è¯•达到阈值åŽåœæ­¢ï¼Œå¹¶æ˜Žç¡®æç¤ºé‡æ–°ç™»å½•ï¼›ä¸æ— é™åˆ·è¯·æ±‚ | +| åªè§¦å‘ `onopen`ã€æœªæ”¶åˆ° `connected`ã€éšåŽè§¦å‘ `onerror` | ä»ç´¯è®¡è¿žç»­å¤±è´¥æ¬¡æ•°ï¼Œä¸å¾—被 `onopen` å夿¸…é›¶ | +| ä¸´æ—¶æ–­ç½‘åŽæ¢å¤ | åœ¨å—æŽ§é€€é¿æˆ–用户é‡è¯•åŽæ¢å¤è¿žæŽ¥ï¼Œä¸äº§ç”Ÿå¹¶å‘ SSE | +| 收到默认 `message` | åŒä¸€äº‹ä»¶åªå¤„ç† 1 次 | +| 正常登出 | SSE å’Œé‡è¿žå®šæ—¶å™¨å‡è¢«æ¸…ç†ï¼Œé€€å‡ºé¡µä¸å†å‘起连接 | +| 釿–°ç™»å½•åŽä»å¤±è´¥ | è”è°ƒææ–™ä»…åŒ…å«æ—¶é—´ã€é¡µé¢ã€é”™è¯¯æ¶ˆæ¯ã€`X-Trace-Id`,ä¸åŒ…å« Token | + +## 7. åŽç«¯çжæ€ä¸Žè¾¹ç•Œ + +- 当剿ޥå£è·¯å¾„ã€Query 傿•°åå’Œ SSE 事件结构未å˜ï¼Œä¸éœ€è¦å‰ç«¯è°ƒæ•´æ•°æ®æ¨¡åž‹ã€‚ +- 新登录/角色切æ¢é“¾è·¯ä¼šå†™å…¥å½“å‰è§’è‰²æ ‡è®°ï¼Œé‡æ–°ç™»å½•是当å‰å¯ç”¨çš„æ¢å¤æ‰‹æ®µã€‚ +- å‘å¸ƒå‰æ—§ä¼šè¯æ²¡æœ‰è¿ç§»æ ‡è®°æ˜¯æœ¬æ¬¡é—®é¢˜çš„è§¦å‘æ¡ä»¶ï¼›åŽç«¯å°šæœªäº¤ä»˜æ—§ä¼šè¯å…¼å®¹è¡¥ä¸ã€‚ +- 角色一致性校验必须ä¿ç•™ï¼Œä¸èƒ½ä¸ºäº†å…¼å®¹æ—§ä¼šè¯è€Œå…许旧角色 Token 接收消æ¯ã€‚ +- è‹¥åŽç»­æ”¹ä¸ºä¸€æ¬¡æ€§ SSE Ticketã€Fetch Streaming 或其他ä¸åœ¨ URL 中æºå¸¦ access token 的方案,将å¦å‘接å£å¥‘约,ä¸åœ¨æœ¬æ¬¡å‰ç«¯é€‚é…范围内。 + +## 8. å®‰å…¨è¦æ±‚ + +- ç¦æ­¢æŠŠå®Œæ•´ Tokenã€å¸¦ Token 的完整 SSE URLã€Cookie 或真实管ç†å‘˜ä¿¡æ¯å†™å…¥ Issueã€PRã€Changelogã€æ—¥å¿—和截图。 +- Token 一旦通过èŠå¤©ã€å·¥å•或截图暴露,应立å³åœæ­¢ä½¿ç”¨å’Œä¼ æ’­ï¼Œé€šçŸ¥åŽç«¯/è¿ç»´æŒ‰å½“å‰é‰´æƒç­–略显å¼åŠé”€æˆ–æ‹’ç»è¯¥æ—§ Token,并验è¯å®ƒå·²æ— æ³•访问;éšåŽé‡æ–°ç™»å½•èŽ·å–æ–° Token。 +- 釿–°ç™»å½•åªæ˜¯æ¢å¤ SSE å’ŒèŽ·å–æ–° Token,ä¸ç­‰äºŽæ—§ JWT 已自动åŠé”€ï¼›å°¤å…¶åœ¨ä»…校验 JWT ç­¾å的环境中,必须å•ç‹¬å®Œæˆæ—§ Token 的失效处置。 +- ä¸å¾—在å‰ç«¯ä»£ç ä¸­ç¡¬ç¼–ç  Token,也ä¸å¾—把 Token å†™å…¥é”™è¯¯ä¸ŠæŠ¥æˆ–åŸ‹ç‚¹å‚æ•°ã€‚ + +## 9. å½±å“范围 + +| 文件/能力 | 说明 | +|---|---| +| `src/composables/useAdminMessageSSE.js` | 连接ã€é‡è¿žã€äº‹ä»¶ç›‘å¬å’Œæ¸…ç†é€»è¾‘ | +| `src/layouts/BasicLayout.vue` | 主布局挂载ã€ç™»å‡ºå’Œ SSE 生命周期 | +| è§’è‰²åˆ‡æ¢æµç¨‹ | Token æ›´æ–°åŽä¸»åЍé‡å»º SSE | +| 顶部未读角标ã€èŠå¤©ã€åœ¨çº¿çжæ€ã€æŠ¢å•池信令 | 共用åŒä¸€ SSE,需防止连接缺失或事件é‡å¤æ¶ˆè´¹ | + +## 10. å‘布说明 + +- 本文是å‰ç«¯è”调和修å¤é€šçŸ¥ï¼Œä¸ä»£è¡¨å·²ä¿®æ”¹æˆ–å‘布å‰ç«¯ä»£ç ã€‚ +- 本文没有包å«ä»»ä½•真实 Tokenã€ç®¡ç†å‘˜ IDã€Cookie æˆ–å…¶ä»–æ•æ„Ÿä¿¡æ¯ã€‚ +- å‰ç«¯å®ŒæˆåŽåº”在 `mmg/hl-ui` 走自身 Issueã€åˆ†æ”¯ã€PRã€æµ‹è¯•å’Œå‘布æµç¨‹ã€‚ + +## 11. 相关历å²å¥‘约 + +| 文档 | 当å‰è¯´æ˜Ž | +|---|---| +| [内部员工站内信收件箱 + SSE 实时推é€](../2026-06/04_3414_内部员工站内信收件箱-SSE实时推é€-管ç†åŽå°.md) | SSE 路径ã€Query 鉴æƒå’Œäº‹ä»¶å¥‘çº¦ä»æœ‰æ•ˆï¼›å…¶ä¸­â€œæ–­çº¿è‡ªåЍé‡è¿žâ€çš„建议被本文补充为有上é™çš„嗿ާé‡è¿žï¼Œé‰´æƒæŒç»­å¤±è´¥æ—¶ä¸å¾—æ— é™è¯·æ±‚ | +| [切角色 / 刷新令牌原å­ä¿å­˜](../2026-06/38_4529_切角色与刷新token原å­ä¿å­˜_å‰ç«¯å¿…改-管ç†åŽå°.md) | `token` 与 `refreshToken` 原å­ä¿å­˜è¦æ±‚仿œ‰æ•ˆï¼›ä¿å­˜æ–° access token åŽè¿˜å¿…须关闭旧 SSE 并主动é‡å»º |