From 309a4bf4d0b52d4ae28922972ce0438276c76578 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 24 May 2026 21:24:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(im):=20=E5=BC=BA=E5=8C=96=E5=A5=BD=E5=8F=8B?= =?UTF-8?q?=E5=85=B3=E7=B3=BB=E3=80=81=E6=B6=88=E6=81=AF=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E5=92=8C=E5=89=8D=E7=AB=AF=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 校验群资料字段长度,并在同意好友申请时复验双方用户 - 仅向双向有效好友推送资料更新通知 - WebSocket 推送收件人去重,并忽略空用户编号 - 群聊和私聊历史保留撤回消息记录 - 校验群通话排除发起人后仍需存在被邀请人 - 统一 IM 前端接口参数传递方式 - 抽取全局 URL 安全打开工具,并复用到消息预览 - 防止好友申请同意和拒绝按钮重复操作 - 补充好友、消息、RTC、WebSocket 相关测试 --- src/api/im/channel/material/index.ts | 2 +- src/api/im/face/useritem/index.ts | 2 +- src/api/im/manager/channel/index.ts | 4 ++-- src/api/im/manager/channel/material/index.ts | 7 +++--- src/api/im/manager/channel/message/index.ts | 2 +- src/api/im/manager/face/item/index.ts | 4 ++-- src/api/im/manager/face/pack/index.ts | 4 ++-- src/api/im/manager/face/userItem/index.ts | 2 +- src/api/im/manager/group/index.ts | 8 +++---- src/api/im/manager/message/group/index.ts | 2 +- src/api/im/manager/message/private/index.ts | 2 +- src/api/im/manager/rtc/index.ts | 2 +- src/api/im/manager/sensitiveword/index.ts | 4 ++-- src/utils/url.ts | 22 +++++++++++++++++++ .../pages/contact/FriendRequestDetail.vue | 6 +++-- .../components/message/MaterialBubble.vue | 4 ++-- .../components/message/MessageBubble.vue | 4 ++-- .../manager/message/MessageContentPreview.vue | 4 ++-- 18 files changed, 55 insertions(+), 30 deletions(-) create mode 100644 src/utils/url.ts diff --git a/src/api/im/channel/material/index.ts b/src/api/im/channel/material/index.ts index 5023f0481..a01ed7e50 100644 --- a/src/api/im/channel/material/index.ts +++ b/src/api/im/channel/material/index.ts @@ -14,5 +14,5 @@ export interface ImChannelMaterialRespVO { // 获取频道素材详情;用于客户端点击图文卡片渲染详情页 export const getChannelMaterial = (id: number) => { - return request.get({ url: '/im/channel/material/get?id=' + id }) + return request.get({ url: '/im/channel/material/get', params: { id } }) } diff --git a/src/api/im/face/useritem/index.ts b/src/api/im/face/useritem/index.ts index c3743b0aa..ab3599689 100644 --- a/src/api/im/face/useritem/index.ts +++ b/src/api/im/face/useritem/index.ts @@ -29,5 +29,5 @@ export const createFaceUserItem = (data: ImFaceUserItemSaveReqVO) => { // 删除个人表情 export const deleteFaceUserItem = (id: number) => { - return request.delete({ url: '/im/face-user-item/delete?id=' + id }) + return request.delete({ url: '/im/face-user-item/delete', params: { id } }) } diff --git a/src/api/im/manager/channel/index.ts b/src/api/im/manager/channel/index.ts index 5897bee69..86c0434ca 100644 --- a/src/api/im/manager/channel/index.ts +++ b/src/api/im/manager/channel/index.ts @@ -17,7 +17,7 @@ export const getManagerChannelPage = (params: PageParam) => { // 获得频道详情 export const getManagerChannel = (id: number) => { - return request.get({ url: '/im/manager/channel/get?id=' + id }) + return request.get({ url: '/im/manager/channel/get', params: { id } }) } // 新增频道 @@ -32,7 +32,7 @@ export const updateManagerChannel = (data: ImManagerChannelVO) => { // 删除频道 export const deleteManagerChannel = (id: number) => { - return request.delete({ url: '/im/manager/channel/delete?id=' + id }) + return request.delete({ url: '/im/manager/channel/delete', params: { id } }) } // 获得启用的频道精简列表(表单选择用) diff --git a/src/api/im/manager/channel/material/index.ts b/src/api/im/manager/channel/material/index.ts index e8eae29a5..ef207ee7e 100644 --- a/src/api/im/manager/channel/material/index.ts +++ b/src/api/im/manager/channel/material/index.ts @@ -21,13 +21,14 @@ export const getManagerChannelMaterialPage = (params: PageParam) => { // 获得指定频道下的素材精简列表 export const getSimpleManagerChannelMaterialList = (channelId: number) => { return request.get({ - url: '/im/manager/channel-material/simple-list?channelId=' + channelId + url: '/im/manager/channel-material/simple-list', + params: { channelId } }) } // 获得素材详情 export const getManagerChannelMaterial = (id: number) => { - return request.get({ url: '/im/manager/channel-material/get?id=' + id }) + return request.get({ url: '/im/manager/channel-material/get', params: { id } }) } // 新增素材 @@ -42,5 +43,5 @@ export const updateManagerChannelMaterial = (data: ImManagerChannelMaterialVO) = // 删除素材 export const deleteManagerChannelMaterial = (id: number) => { - return request.delete({ url: '/im/manager/channel-material/delete?id=' + id }) + return request.delete({ url: '/im/manager/channel-material/delete', params: { id } }) } diff --git a/src/api/im/manager/channel/message/index.ts b/src/api/im/manager/channel/message/index.ts index 257e72042..91c30beb8 100644 --- a/src/api/im/manager/channel/message/index.ts +++ b/src/api/im/manager/channel/message/index.ts @@ -25,7 +25,7 @@ export const sendManagerChannelMessage = (data: ImManagerChannelMessageSendReqVO // 删除频道消息 export const deleteManagerChannelMessage = (id: number) => { - return request.delete({ url: '/im/manager/channel-message/delete?id=' + id }) + return request.delete({ url: '/im/manager/channel-message/delete', params: { id } }) } // 获得频道消息分页 diff --git a/src/api/im/manager/face/item/index.ts b/src/api/im/manager/face/item/index.ts index 4d2d9718e..7609e9fa4 100644 --- a/src/api/im/manager/face/item/index.ts +++ b/src/api/im/manager/face/item/index.ts @@ -19,7 +19,7 @@ export const getManagerFacePackItemPage = (params: PageParam) => { // 获得表情详情 export const getManagerFacePackItem = (id: number) => { - return request.get({ url: '/im/manager/face-pack-item/get?id=' + id }) + return request.get({ url: '/im/manager/face-pack-item/get', params: { id } }) } // 新增表情 @@ -34,7 +34,7 @@ export const updateManagerFacePackItem = (data: ImManagerFacePackItemVO) => { // 删除表情 export const deleteManagerFacePackItem = (id: number) => { - return request.delete({ url: '/im/manager/face-pack-item/delete?id=' + id }) + return request.delete({ url: '/im/manager/face-pack-item/delete', params: { id } }) } // 批量删除表情 diff --git a/src/api/im/manager/face/pack/index.ts b/src/api/im/manager/face/pack/index.ts index 5c221941c..61a7c0175 100644 --- a/src/api/im/manager/face/pack/index.ts +++ b/src/api/im/manager/face/pack/index.ts @@ -16,7 +16,7 @@ export const getManagerFacePackPage = (params: PageParam) => { // 获得表情包详情 export const getManagerFacePack = (id: number) => { - return request.get({ url: '/im/manager/face-pack/get?id=' + id }) + return request.get({ url: '/im/manager/face-pack/get', params: { id } }) } // 新增表情包 @@ -31,7 +31,7 @@ export const updateManagerFacePack = (data: ImManagerFacePackVO) => { // 删除表情包 export const deleteManagerFacePack = (id: number) => { - return request.delete({ url: '/im/manager/face-pack/delete?id=' + id }) + return request.delete({ url: '/im/manager/face-pack/delete', params: { id } }) } // 批量删除表情包 diff --git a/src/api/im/manager/face/userItem/index.ts b/src/api/im/manager/face/userItem/index.ts index e7d6aebbc..becc8cf68 100644 --- a/src/api/im/manager/face/userItem/index.ts +++ b/src/api/im/manager/face/userItem/index.ts @@ -18,5 +18,5 @@ export const getManagerFaceUserItemPage = (params: PageParam) => { // 删除用户表情 export const deleteManagerFaceUserItem = (id: number) => { - return request.delete({ url: '/im/manager/face-user-item/delete?id=' + id }) + return request.delete({ url: '/im/manager/face-user-item/delete', params: { id } }) } diff --git a/src/api/im/manager/group/index.ts b/src/api/im/manager/group/index.ts index 6c5588bac..20114a8b8 100644 --- a/src/api/im/manager/group/index.ts +++ b/src/api/im/manager/group/index.ts @@ -38,7 +38,7 @@ export const getManagerGroupPage = (params: PageParam) => { // 获得群详情 export const getManagerGroup = (id: number) => { - return request.get({ url: '/im/manager/group/get?id=' + id }) + return request.get({ url: '/im/manager/group/get', params: { id } }) } // 封禁群 @@ -48,15 +48,15 @@ export const banManagerGroup = (data: { id: number; reason: string }) => { // 解封群 export const unbanManagerGroup = (id: number) => { - return request.put({ url: '/im/manager/group/unban?id=' + id }) + return request.put({ url: '/im/manager/group/unban', params: { id } }) } // 解散群 export const dissolveManagerGroup = (id: number) => { - return request.delete({ url: '/im/manager/group/dissolve?id=' + id }) + return request.delete({ url: '/im/manager/group/dissolve', params: { id } }) } // 获得群成员列表(含已退群成员,由前端按需过滤) export const getManagerGroupMemberList = (groupId: number) => { - return request.get({ url: '/im/manager/group/member/list?groupId=' + groupId }) + return request.get({ url: '/im/manager/group/member/list', params: { groupId } }) } diff --git a/src/api/im/manager/message/group/index.ts b/src/api/im/manager/message/group/index.ts index 598193093..d56ed13c5 100644 --- a/src/api/im/manager/message/group/index.ts +++ b/src/api/im/manager/message/group/index.ts @@ -25,5 +25,5 @@ export const getManagerGroupMessagePage = (params: PageParam) => { // 获得群聊消息详情 export const getManagerGroupMessage = (id: number) => { - return request.get({ url: '/im/manager/message/group/get?id=' + id }) + return request.get({ url: '/im/manager/message/group/get', params: { id } }) } diff --git a/src/api/im/manager/message/private/index.ts b/src/api/im/manager/message/private/index.ts index ed97e3db8..3ccf70b79 100644 --- a/src/api/im/manager/message/private/index.ts +++ b/src/api/im/manager/message/private/index.ts @@ -21,5 +21,5 @@ export const getManagerPrivateMessagePage = (params: PageParam) => { // 获得私聊消息详情 export const getManagerPrivateMessage = (id: number) => { - return request.get({ url: '/im/manager/message/private/get?id=' + id }) + return request.get({ url: '/im/manager/message/private/get', params: { id } }) } diff --git a/src/api/im/manager/rtc/index.ts b/src/api/im/manager/rtc/index.ts index 579256977..d5c4cc7a3 100644 --- a/src/api/im/manager/rtc/index.ts +++ b/src/api/im/manager/rtc/index.ts @@ -36,5 +36,5 @@ export const getManagerRtcCallPage = (params: PageParam) => { // 获得通话参与者列表 export const getManagerRtcCallParticipantList = (id: number) => { - return request.get({ url: '/im/manager/rtc/participant-list?id=' + id }) + return request.get({ url: '/im/manager/rtc/participant-list', params: { id } }) } diff --git a/src/api/im/manager/sensitiveword/index.ts b/src/api/im/manager/sensitiveword/index.ts index 4cc4fb57b..165bc76db 100644 --- a/src/api/im/manager/sensitiveword/index.ts +++ b/src/api/im/manager/sensitiveword/index.ts @@ -16,7 +16,7 @@ export const getManagerSensitiveWordPage = (params: PageParam) => { // 获得敏感词详情 export const getManagerSensitiveWord = (id: number) => { - return request.get({ url: '/im/manager/sensitive-word/get?id=' + id }) + return request.get({ url: '/im/manager/sensitive-word/get', params: { id } }) } // 新增敏感词 @@ -31,7 +31,7 @@ export const updateManagerSensitiveWord = (data: ImManagerSensitiveWordVO) => { // 删除敏感词 export const deleteManagerSensitiveWord = (id: number) => { - return request.delete({ url: '/im/manager/sensitive-word/delete?id=' + id }) + return request.delete({ url: '/im/manager/sensitive-word/delete', params: { id } }) } // 批量删除敏感词 diff --git a/src/utils/url.ts b/src/utils/url.ts new file mode 100644 index 000000000..1225623ca --- /dev/null +++ b/src/utils/url.ts @@ -0,0 +1,22 @@ +const OPENABLE_PROTOCOLS = new Set(['http:', 'https:', 'blob:']) + +/** 判断 URL 是否允许在新窗口打开 */ +export function isOpenableUrl(url?: string | null): boolean { + if (!url) { + return false + } + try { + const parsed = new URL(url, window.location.origin) + return OPENABLE_PROTOCOLS.has(parsed.protocol) + } catch { + return false + } +} + +/** 安全打开 URL */ +export function openSafeUrl(url?: string | null): void { + if (!url || !isOpenableUrl(url)) { + return + } + window.open(url, '_blank', 'noopener,noreferrer') +} diff --git a/src/views/im/home/pages/contact/FriendRequestDetail.vue b/src/views/im/home/pages/contact/FriendRequestDetail.vue index 564879426..2758ce1f5 100644 --- a/src/views/im/home/pages/contact/FriendRequestDetail.vue +++ b/src/views/im/home/pages/contact/FriendRequestDetail.vue @@ -69,8 +69,10 @@ 已拒绝 diff --git a/src/views/im/home/pages/conversation/components/message/MaterialBubble.vue b/src/views/im/home/pages/conversation/components/message/MaterialBubble.vue index 252ded517..764383b04 100644 --- a/src/views/im/home/pages/conversation/components/message/MaterialBubble.vue +++ b/src/views/im/home/pages/conversation/components/message/MaterialBubble.vue @@ -80,6 +80,7 @@ import { parseMessage, type MaterialMessage } from '@/views/im/utils/message' import { useConversationStore } from '@/views/im/home/store/conversationStore' import { useChannelStore } from '@/views/im/home/store/channelStore' import { ImConversationType } from '@/views/im/utils/constants' +import { openSafeUrl } from '@/utils/url' const props = defineProps<{ content: string @@ -110,8 +111,7 @@ const detailHtml = ref('') /** 点击行为:url 非空跳外链;为空则按 payload.materialId 拉富文本正文,全屏 dialog 渲染 */ const onClick = async () => { if (payload.value.url) { - // 外链强制 noopener,noreferrer,阻断目标页面 window.opener 篡改 / referrer 泄露 - window.open(payload.value.url, '_blank', 'noopener,noreferrer') + openSafeUrl(payload.value.url) return } if (!payload.value.materialId) { diff --git a/src/views/im/home/pages/conversation/components/message/MessageBubble.vue b/src/views/im/home/pages/conversation/components/message/MessageBubble.vue index baf77c786..d766f2b12 100644 --- a/src/views/im/home/pages/conversation/components/message/MessageBubble.vue +++ b/src/views/im/home/pages/conversation/components/message/MessageBubble.vue @@ -184,6 +184,7 @@ import { type VideoMessage } from '@/views/im/utils/message' import { summarizeMessageContent } from '@/views/im/utils/conversation' +import { openSafeUrl } from '@/utils/url' import CardBubble from '@/views/im/home/components/card/CardBubble.vue' import TipSegments from './TipSegments.vue' import { useVoicePlayer } from '@/views/im/home/composables/useVoicePlayer' @@ -322,8 +323,7 @@ function handleFileClick() { if (isUploading.value || !filePayload.value?.url) { return } - // noopener,noreferrer 切断新窗口对原页面的 window.opener 引用,防 Tabnabbing - window.open(filePayload.value.url, '_blank', 'noopener,noreferrer') + openSafeUrl(filePayload.value.url) } /** 语音点击:托管给 useVoicePlayer 全局互斥播放,新点的语音会停掉旧的 */ diff --git a/src/views/im/manager/message/MessageContentPreview.vue b/src/views/im/manager/message/MessageContentPreview.vue index 59458addd..fbb0078d4 100644 --- a/src/views/im/manager/message/MessageContentPreview.vue +++ b/src/views/im/manager/message/MessageContentPreview.vue @@ -164,6 +164,7 @@ import { type MergeMessage } from '@/views/im/utils/message' import { buildFacePreviewText, summarizeMessageContent } from '@/views/im/utils/conversation' +import { openSafeUrl } from '@/utils/url' defineOptions({ name: 'ImMessageContentPreview' }) @@ -227,8 +228,7 @@ const mergePreviewLines = computed(() => { function openVideo() { const url = videoPayload.value?.url if (url) { - // noopener,noreferrer 切断新窗口对原页面的 window.opener 引用,防 Tabnabbing - window.open(url, '_blank', 'noopener,noreferrer') + openSafeUrl(url) } }