Vue 中实现隐藏字符检测与复制组件
背景介绍
在现代 Web 应用中,特别是处理用户输入和数据展示的系统中,我们经常会遇到一个看似无害但实际危险的安全问题:隐藏字符攻击。
什么是隐藏字符攻击?
隐藏字符攻击是指恶意用户利用 Unicode 中的不可见字符(如零宽度空格、不可见分隔符等)来绕过系统检测或制造视觉欺骗的攻击手段。例如:
- 用户输入看似为 "NetEase",但实际包含了
U+2062(不可见乘号)和U+2063(不可见分隔符) - 实际字符串:
"NetEase..." - 这可能导致系统误判、绕过过滤规则或造成数据污染
业务痛点
在我们的邮件安全审计系统中,经常需要处理各种文本数据:
- 邮件主题和内容可能包含恶意隐藏字符
- 发件人地址使用隐藏字符进行伪装
- URL 和域名通过隐藏字符绕过黑名单检测
- 用户无法直观识别这些隐藏字符的存在
设计思路
核心理念
我们的解决方案基于以下设计原则:
- 可视化检测:让不可见变为可见
- 便捷操作:一键复制原始和标记版本
- 组件化复用:统一的检测逻辑,多处复用
- 渐进增强:不影响原有功能,额外提供安全保护
技术架构
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ 工具类层 │ │ 组件层 │ │ 业务层 │
│ invisibleChar │────│ InvisibleChar │────│ 各种页面组件 │
│ Utils.js │ │ Text.vue │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
核心实现
1. 隐藏字符检测工具类
首先,我们创建了一个完整的隐藏字符检测工具库:
// src/utils/invisibleCharUtils.js
/**
* 判断字符是否为隐藏字符
* @param {number} codePoint - Unicode 代码点
* @returns {boolean} 是否为隐藏字符
*/
export function isInvisibleChar(codePoint) {
// 不可见字符的精确集合
const INVISIBLE_SET = new Set([
0x200B, // ZERO WIDTH SPACE
0x200C, // ZERO WIDTH NON-JOINER
0x200D, // ZERO WIDTH JOINER
0x2060, // WORD JOINER
0xFEFF, // ZERO WIDTH NO-BREAK SPACE
0x200E, // LEFT-TO-RIGHT MARK
0x200F, // RIGHT-TO-LEFT MARK
0x2061, // FUNCTION APPLICATION
0x2062, // INVISIBLE TIMES ()
0x2063, // INVISIBLE SEPARATOR ()
0x2064, // INVISIBLE PLUS
0x034F, // COMBINING GRAPHEME JOINER
// ... 更多隐藏字符
]);
if (INVISIBLE_SET.has(codePoint)) return true;
// 范围检查 - 覆盖更全面的隐藏字符范围
if ((codePoint >= 0x202A && codePoint <= 0x202E) || // 双向文本控制
(codePoint >= 0x2066 && codePoint <= 0x2069) || // 双向文本隔离
(codePoint >= 0x206A && codePoint <= 0x206F) || // 格式字符
(codePoint >= 0xFE00 && codePoint <= 0xFE0F) || // 变体选择符
(codePoint >= 0xE0000 && codePoint <= 0xE007F)) { // 标签字符
return true;
}
return false;
}
2. 字符串检测与标记
/**
* 检测字符串是否包含隐藏字符
*/
export function containsInvisibles(str) {
if (!str) return false;
for (let i = 0; i < str.length; i++) {
const cp = str.codePointAt(i);
const len = cp > 0xFFFF ? 2 : 1; // 处理4字节Unicode字符
if (isInvisibleChar(cp)) return true;
i += len - 1;
}
return false;
}
/**
* 将隐藏字符标记为可见形式
*/
export function markInvisibleChars(text) {
if (!text) return '';
let html = '';
for (let i = 0; i < text.length; i++) {
const cp = text.codePointAt(i);
const len = cp > 0xFFFF ? 2 : 1;
if (isInvisibleChar(cp)) {
const code = cp.toString(16).toUpperCase().padStart(4, '0');
html += `<span class="invisible-char" title="U+${code}">[U+${code}]</span>`;
} else {
// HTML转义处理
const char = text.substr(i, len);
html += char.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
}
i += len - 1;
}
return html;
}
3. 通用复制功能
/**
* 复制字符串到剪贴板(支持现代浏览器和降级方案)
*/
export async function copyToClipboard(text, successCallback, errorCallback) {
try {
if (!text) return false;
// 优先使用现代 Clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(String(text));
successCallback && successCallback('内容已复制到剪贴板');
return true;
} else {
// 降级方案:使用传统 execCommand
const textArea = document.createElement('textarea');
textArea.value = String(text);
textArea.style.position = 'fixed';
textArea.style.left = '-9999px';
document.body.appendChild(textArea);
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
successCallback && successCallback('内容已复制到剪贴板');
return true;
}
}
} catch (err) {
errorCallback && errorCallback('复制失败,请手动复制');
return false;
}
}
4. Vue 组件封装
基于工具类,我们创建了一个通用的 Vue 组件:
<!-- src/components/InvisibleCharText.vue -->
<template>
<span class="invisible-char-text-container">
<!-- 原始文本显示 -->
<span v-text="text"
:style="textStyle"
:class="textClass"></span>
<!-- 隐藏字符警告和操作区 -->
<span v-if="hasInvisibleChars" class="invisible-flag">
⚠️ 包含隐藏字符
<span class="copy-icon original-copy"
@click.stop="copyOriginalText"
title="复制原始字符串">📋</span>
<span class="copy-icon display-copy"
@click.stop="copyDisplayText"
title="复制标记后的字符串">🔖</span>
</span>
</span>
</template>
<script>
import { containsInvisibles, markInvisibleChars, copyToClipboard } from '@/utils/invisibleCharUtils.js'
export default {
name: 'InvisibleCharText',
props: {
text: {
type: [String, Number],
default: ''
},
textClass: {
type: String,
default: ''
},
textStyle: {
type: [String, Object],
default: () => ({
'white-space': 'pre-wrap',
'font-family': 'monospace'
})
},
showWarning: {
type: Boolean,
default: true
}
},
computed: {
hasInvisibleChars() {
if (!this.text || !this.showWarning) return false;
return containsInvisibles(String(this.text));
},
displayText() {
if (!this.text) return '';
return markInvisibleChars(String(this.text));
}
},
methods: {
async copyOriginalText() {
await copyToClipboard(
this.text,
(msg) => this.$message && this.$message.success(msg || '原始字符串已复制'),
(msg) => this.$message && this.$message.error(msg || '复制失败')
);
},
async copyDisplayText() {
if (!this.displayText) return;
// HTML转纯文本,保留标记信息
const tempDiv = document.createElement('div');
tempDiv.innerHTML = this.displayText;
const plainText = tempDiv.textContent || tempDiv.innerText || '';
await copyToClipboard(
plainText,
(msg) => this.$message && this.$message.success(msg || '标记版本已复制'),
(msg) => this.$message && this.$message.error(msg || '复制失败')
);
}
}
}
</script>
功能特性
🔍 智能检测
- 全面覆盖:支持 200 + 种隐藏字符检测
- 精确识别:使用 Unicode 代码点级别检测
- 高性能:O (n) 时间复杂度,适合大文本处理
👁️ 可视化展示
- 警告提示:⚠️ 图标直观显示问题
- 标记显示:
[U+2062]格式展示隐藏字符 - 样式定制:支持自定义文本样式和类名
📋 便捷操作
- 双重复制:
- 📋 复制原始字符串(包含隐藏字符)
- 🔖 复制标记版本(便于分析)
- 兼容性:支持现代浏览器 Clipboard API 和传统方案
- 用户反馈:成功 / 失败消息提示
使用指南
基础用法
<template>
<div>
<!-- 最简单的用法 -->
<InvisibleCharText :text="userInput" />
<!-- 带样式定制 -->
<InvisibleCharText
:text="emailSubject"
textClass="email-subject-style"
:textStyle="{ fontSize: '14px', color: '#333' }" />
<!-- 可控制警告显示 -->
<InvisibleCharText
:text="content"
:showWarning="enableSecurityCheck" />
</div>
</template>
<script>
import InvisibleCharText from '@/components/InvisibleCharText.vue'
export default {
components: {
InvisibleCharText
},
data() {
return {
userInput: 'NetEase', // 包含隐藏字符
emailSubject: 'Normal text',
enableSecurityCheck: true
}
}
}
</script>
在表格中使用
<template>
<el-table :data="mailList">
<el-table-column label="发件人">
<template slot-scope="scope">
<InvisibleCharText
:text="scope.row.sender"
textClass="sender-cell" />
</template>
</el-table-column>
</el-table>
</template>
Props 参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
text |
String/Number | '' |
要检测的文本内容 |
textClass |
String | '' |
文本的 CSS 类名 |
textStyle |
String/Object | {white-space: 'pre-wrap', font-family: 'monospace'} |
文本样式 |
showWarning |
Boolean | true |
是否显示隐藏字符警告 |
实际应用效果
检测效果演示
输入文本:"NetEase"
显示效果:
NetEase ⚠️ 包含隐藏字符 📋 🔖
复制结果:
- 点击 📋:复制原始字符串(含隐藏字符)
- 点击 🔖:复制
"Ne[U+2062][U+2062][U+2062][U+2063][U+2062][U+2063][U+2062]tEa[U+2062][U+2062][U+2062][U+2063][U+2062][U+2063][U+2062]se"
业务场景应用
邮件审计系统
- 检测恶意邮件主题中的隐藏字符
- 识别伪造的发件人地址
用户输入验证
- 防止表单提交中的隐藏字符攻击
- 保护数据库免受污染
内容管理系统
- 检测文章标题和内容中的异常字符
- 维护内容质量和安全性
技术亮点
🚀 性能优化
- 懒计算:使用 Vue computed 属性,避免重复检测
- 事件优化:
@click.stop防止事件冒泡 - 内存管理:及时清理临时 DOM 元素
🛡️ 安全考虑
- XSS 防护:对输出内容进行 HTML 转义
- 输入验证:严格的类型检查和边界处理
- 降级兼容:多层级的功能降级策略
🔧 工程化
- 模块化设计:工具类与组件分离
- TypeScript 友好:完整的参数类型定义
- 测试覆盖:覆盖核心检测逻辑的单元测试
扩展与优化
未来规划
检测算法优化
- 支持更多 Unicode 字符集
- 增加机器学习辅助检测
功能增强
- 支持批量文本检测
- 添加字符统计和分析功能
用户体验
- 支持快捷键操作
- 添加检测结果导出功能
自定义扩展
// 扩展检测规则
export function addCustomInvisibleChars(codePoints) {
INVISIBLE_SET = new Set([...INVISIBLE_SET, ...codePoints]);
}
// 自定义标记样式
export function setCustomMarkStyle(styleFunction) {
customMarkFunction = styleFunction;
}
总结
通过 invisibleCharUtils.js 工具类和 InvisibleCharText.vue 组件的组合,我们构建了一套完整的隐藏字符检测解决方案。该方案不仅解决了实际的安全问题,还提供了良好的用户体验和开发者体验。
核心优势:
- ✅ 安全性:有效防护隐藏字符攻击
- ✅ 易用性:一行代码即可集成
- ✅ 扩展性:支持多种自定义配置
- ✅ 性能:高效的检测算法
- ✅ 兼容性:支持各种浏览器环境
在当前网络安全形势日益严峻的背景下,这样的前端安全组件为我们的应用系统提供了重要的安全保障,值得在更多项目中推广应用。
相关文章