vue3 + element-plus
1.原理
使用h函数重写el-input组件,注册到vue组件中
2.代码
inputInterceptor.ts
// src/directive/inputInterceptor.ts
// 输入拦截器 - 用于防止XSS攻击和恶意代码注入的安全组件
// 导入Vue应用类型,用于类型检查
import type { App } from 'vue'
// 导入Element Plus的消息组件,用于显示警告信息
import { ElMessage } from 'element-plus'
// 导入Vue的组合式API函数
import { defineComponent, h, ref, watch } from 'vue'
// 定义安全输入模式数组 - 包含所有需要拦截的危险字符和模式
// 预编译正则表达式以提高性能
const SAFE_INPUT_PATTERNS = [
// JavaScript危险函数 - 可能被用于执行恶意代码
{ pattern: /alert\s*\(/gi, name: 'alert', level: 'high' }, // 弹窗函数
{ pattern: /eval\s*\(/gi, name: 'eval', level: 'high' }, // 动态执行代码函数
{ pattern: /confirm\s*\(/gi, name: 'confirm', level: 'high' }, // 确认对话框函数
{ pattern: /prompt\s*\(/gi, name: 'prompt', level: 'high' }, // 输入对话框函数
{ pattern: /setTimeout\s*\(/gi, name: 'setTimeout', level: 'high' }, // 定时器函数
{ pattern: /setInterval\s*\(/gi, name: 'setInterval', level: 'high' }, // 定时器函数
{ pattern: /Function\s*\(/gi, name: 'function', level: 'high' }, // 函数构造函数
// HTML危险标签 - 可能被用于注入恶意HTML
{ pattern: /<script\b[^>]*>/gi, name: '<script>', level: 'high' }, // 脚本标签
{ pattern: /<\/script>/gi, name: '</script>', level: 'high' }, // 脚本标签结束
{ pattern: /<svg\b[^>]*>/gi, name: '<svg>', level: 'medium' }, // SVG标签
{ pattern: /<\/svg>/gi, name: '</svg>', level: 'medium' }, // SVG标签结束
{ pattern: /<img\b[^>]*>/gi, name: '<img>', level: 'medium' }, // 图片标签
{ pattern: /<iframe\b[^>]*>/gi, name: '<iframe>', level: 'high' }, // iframe标签
{ pattern: /<object\b[^>]*>/gi, name: '<object>', level: 'high' }, // object标签
{ pattern: /<embed\b[^>]*>/gi, name: '<embed>', level: 'high' }, // embed标签
// 事件处理器属性 - 可能被用于执行恶意事件
{ pattern: /\bon\w+\s*=/gi, name: 'on', level: 'high' }, // 所有on事件
{ pattern: /\bonerror\s*=/gi, name: 'onerror', level: 'high' }, // 错误事件
{ pattern: /\bonload\s*=/gi, name: 'onload', level: 'high' }, // 加载事件
{ pattern: /\boninput\s*=/gi, name: 'oninput', level: 'medium' }, // 输入事件
{ pattern: /\bonclick\s*=/gi, name: 'onclick', level: 'medium' }, // 点击事件
{ pattern: /\bonmouse\w+\s*=/gi, name: 'mouse', level: 'medium' }, // 鼠标相关事件
// 属性选择器 - 可能被用于注入恶意属性
{ pattern: /\bsrc\s*=/gi, name: 'src', level: 'medium' }, // 源属性
{ pattern: /\bhref\s*=/gi, name: 'href', level: 'medium' }, // 链接属性
{ pattern: /\bdata\s*=/gi, name: 'data', level: 'low' }, // data属性
// URL编码字符 - 可能被用于绕过检测
{ pattern: /%3[cC]/gi, name: '<', level: 'medium' }, // URL编码的 '<'
{ pattern: /%3[eE]/gi, name: '>', level: 'medium' }, // URL编码的 '>'
{ pattern: /%2[bB]/gi, name: '+', level: 'low' }, // URL编码的 '+'
{ pattern: /%2[fF]/gi, name: '/', level: 'low' }, // URL编码的 '/'
// { pattern: /%2[27]/gi, name: '"', level: 'medium' }, // URL编码的 '"'
// { pattern: /%2[79]/gi, name: "'", level: 'medium' }, // URL编码的 "'"
// JavaScript协议 - 可能被用于执行恶意代码
{ pattern: /javascript\s*:/gi, name: 'javascript:', level: 'high' }, // JavaScript协议
{ pattern: /vbscript\s*:/gi, name: 'vbscript:', level: 'high' }, // VBScript协议
{ pattern: /data\s*:\s*text\/html/gi, name: 'data:text/html', level: 'high' }, // data协议
// 特殊字符组合 - 可能被用于构造恶意代码
{ pattern: /<[^>]*>/gi, name: 'HTML', level: 'medium' }, // 任何HTML标签
{ pattern: /\\x[0-9a-fA-F]{2}/gi, name: 'Ox', level: 'medium' }, // 十六进制编码
{ pattern: /\\u[0-9a-fA-F]{4}/gi, name: 'unicode', level: 'medium' }, // Unicode编码
// 特殊字符 - 可能被用于构造恶意代码或绕过检测
{ pattern: /@/gi, name: '@', level: 'low' }, // @符号,常用于邮箱地址
{ pattern: /!/gi, name: '!', level: 'low' }, // 感叹号,可能用于构造特殊语法
]
// 配置选项接口
interface InterceptorConfig {
enableWarning?: boolean // 是否启用警告提示
enableLogging?: boolean // 是否启用日志记录
strictMode?: boolean // 严格模式,检测更多模式
whitelist?: string[] // 白名单,允许特定模式
maxLength?: number // 最大输入长度
customPatterns?: Array<{
// 自定义检测模式
pattern: RegExp
name: string
level: 'high' | 'medium' | 'low'
}>
customValidator?: (value: string) => {
// 自定义校验函数
isValid: boolean
message?: string
cleanedValue?: string
}
}
// 默认配置
const DEFAULT_CONFIG: InterceptorConfig = {
enableWarning: true,
enableLogging: false,
strictMode: false,
whitelist: [],
maxLength: 1000,
}
/**
* 安全检测函数
* @param value 要检测的值
* @param config 配置选项
* @returns 检测结果
*/
function securityCheck(
value: string,
config: InterceptorConfig = DEFAULT_CONFIG
) {
if (!value || typeof value !== 'string') {
return { isSafe: true, cleanedValue: value, detectedPatterns: [] }
}
// 检查长度限制
if (config.maxLength && value.length > config.maxLength) {
return {
isSafe: false,
cleanedValue: value.substring(0, config.maxLength),
detectedPatterns: [`输入长度超过限制(${config.maxLength}字符)`],
level: 'medium',
}
}
let cleanedValue = value
let detectedPatterns: Array<{ name: string; level: string }> = []
// 遍历所有危险模式进行检测
SAFE_INPUT_PATTERNS.forEach(({ pattern, name, level }) => {
// 检查是否在白名单中
if (config.whitelist && config.whitelist.includes(name)) {
return
}
// 使用正则表达式测试当前值是否包含危险模式
if (pattern.test(value)) {
detectedPatterns.push({ name, level })
// 从值中移除检测到的危险字符
cleanedValue = cleanedValue.replace(pattern, '')
}
})
// 检测自定义模式
if (config.customPatterns) {
config.customPatterns.forEach(({ pattern, name, level }) => {
if (pattern.test(value)) {
detectedPatterns.push({ name, level })
cleanedValue = cleanedValue.replace(pattern, '')
}
})
}
// 在严格模式下进行额外检查
if (config.strictMode) {
// 检查SQL注入模式
const sqlPatterns = [
{
pattern: /(\b(select|insert|update|delete|drop|create|alter)\b)/gi,
name: 'SQL关键字',
level: 'medium',
},
{ pattern: /(--|\/\*|\*\/)/gi, name: 'SQL注释', level: 'medium' },
{
pattern: /(\b(union|exec|execute)\b)/gi,
name: 'SQL命令',
level: 'high',
},
]
sqlPatterns.forEach(({ pattern, name, level }) => {
if (pattern.test(value)) {
detectedPatterns.push({ name, level })
cleanedValue = cleanedValue.replace(pattern, '')
}
})
}
// 执行自定义校验函数
if (config.customValidator) {
try {
const customResult = config.customValidator(cleanedValue)
if (!customResult.isValid) {
detectedPatterns.push({
name: customResult.message || '自定义校验失败',
level: 'medium',
})
if (customResult.cleanedValue !== undefined) {
cleanedValue = customResult.cleanedValue
}
}
} catch (error) {
console.warn('自定义校验函数执行失败:', error)
}
}
return {
isSafe: detectedPatterns.length === 0,
cleanedValue,
detectedPatterns,
level: detectedPatterns.length > 0 ? detectedPatterns[0].level : 'safe',
}
}
/**
* 设置输入拦截器的主函数
* @param app Vue应用实例
* @param config 配置选项
*/
export function setupInputInterceptor(
app: App,
config: InterceptorConfig = DEFAULT_CONFIG
) {
// 合并配置
const finalConfig = { ...DEFAULT_CONFIG, ...config }
// 获取应用中已注册的原始ElInput组件
const originalInput = app.component('ElInput')
// 如果原始组件不存在,直接返回,不进行拦截
if (!originalInput) {
console.warn('ElInput组件未找到,输入拦截器未启用')
return
}
// 定义拦截后的输入组件,继承原始组件的所有功能
const InterceptedInput = defineComponent({
name: 'ElInput', // 保持原始组件名称
inheritAttrs: false, // 不自动继承属性,手动处理
// 定义组件属性,与原始ElInput保持一致
props: {
modelValue: {
type: [String, Number], // 支持字符串和数字类型
default: '', // 默认值为空字符串
},
placeholder: {
type: String,
default: '', // 占位符文本
},
disabled: {
type: Boolean,
default: false, // 是否禁用
},
readonly: {
type: Boolean,
default: false, // 是否只读
},
clearable: {
type: Boolean,
default: false, // 是否可清空
},
showPassword: {
type: Boolean,
default: false, // 是否显示密码切换按钮
},
type: {
type: String,
default: 'text', // 输入框类型
},
size: {
type: String,
default: 'default', // 组件大小
},
// 新增安全配置属性
securityConfig: {
type: Object as () => InterceptorConfig,
default: () => ({}),
},
},
// 定义组件可以触发的事件
emits: [
'update:modelValue',
'input',
'change',
'focus',
'blur',
'clear',
'security-violation',
],
// 组件的设置函数,使用组合式API
setup(props, { emit, attrs }) {
// 响应式变量用于跟踪安全违规次数
const violationCount = ref(0)
const lastViolationTime = ref(0)
// 合并组件级别的安全配置
const getComponentConfig = () => {
return { ...finalConfig, ...props.securityConfig }
}
/**
* 处理模型值更新的核心函数
* @param value 用户输入的新值
*/
const handleUpdateModelValue = (value: string) => {
const componentConfig = getComponentConfig()
// 进行安全检查
const checkResult = securityCheck(value, componentConfig)
// 如果检测到安全问题
if (!checkResult.isSafe) {
// 更新违规计数
violationCount.value++
lastViolationTime.value = Date.now()
// 更新模型值为清理后的安全值
emit('update:modelValue', checkResult.cleanedValue)
emit('input', checkResult.cleanedValue)
// 触发安全违规事件
emit('security-violation', {
originalValue: value,
cleanedValue: checkResult.cleanedValue,
detectedPatterns: checkResult.detectedPatterns,
violationCount: violationCount.value,
timestamp: Date.now(),
})
// 显示警告消息
if (componentConfig.enableWarning) {
const patternNames = checkResult.detectedPatterns
.map((p) => p.name)
.join('、')
const level = checkResult.level === 'high' ? 'error' : 'warning'
ElMessage({
type: level,
message: `检测到${
checkResult.level === 'high' ? '高危' : '中危'
}安全威胁: ${patternNames},已自动清除`,
duration: 3000,
})
}
// 记录日志
if (componentConfig.enableLogging) {
console.warn('安全拦截:', {
originalValue: value,
cleanedValue: checkResult.cleanedValue,
detectedPatterns: checkResult.detectedPatterns,
timestamp: new Date().toISOString(),
})
}
return // 提前返回,不执行后续的正常更新逻辑
}
// 如果没有检测到危险字符,正常更新值
emit('update:modelValue', value)
emit('input', value)
}
// 监听props变化,重新检查当前值
watch(
() => props.modelValue,
(newValue) => {
if (newValue && typeof newValue === 'string') {
const componentConfig = getComponentConfig()
const checkResult = securityCheck(newValue, componentConfig)
if (!checkResult.isSafe) {
// 如果当前值不安全,自动清理
emit('update:modelValue', checkResult.cleanedValue)
}
}
}
)
// 返回渲染函数
return () => {
// 使用Vue的h函数创建虚拟DOM
return h(originalInput, {
...attrs, // 展开所有传入的属性
...props, // 展开所有props
'onUpdate:modelValue': handleUpdateModelValue, // 重写模型值更新处理器
onInput: attrs.onInput, // 透传原始的input事件处理器
})
}
},
})
// 将拦截后的组件重新注册为ElInput,覆盖原始组件
app.component('ElInput', InterceptedInput)
// 记录拦截器启用日志
if (finalConfig.enableLogging) {
console.log('输入拦截器已启用,配置:', finalConfig)
}
}
// 导出配置类型和默认配置,供外部使用
export type { InterceptorConfig }
export { DEFAULT_CONFIG, securityCheck }
3.使用
注册
main.js
import {createApp} from 'vue';
import App from '/@/App.vue';
import {setupInputInterceptor} from '/@components/interceptor/inputInterceptor';
import ElementPlus from 'element-plus';
const app = createApp(App)
// 必须在Element-Plus之后注册
app.use(pinia).use(router).use(ElementPlus);
setupInputInterceptor(app);
app.mount('#app')
4.示例
执行顺序总结:
安全拦截器检测 → 清理危险字符
自定义模式检测 → 清理自定义模式
自定义校验函数 → 执行业务逻辑校验
触发input事件 → 执行你的@input方法
(1)### 并存模式
<template>
<el-input
v-model="email"
@input="handleEmailValidation"
:security-config="{
enableWarning: true,
whitelist: ['@'],
customValidator: validateEmailFormat
}"
/>
</template>
<script setup>
const email = ref('')
// 在安全拦截器内部执行的校验(优先)
const validateEmailFormat = (value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (value && !emailRegex.test(value)) {
return {
isValid: false,
message: '邮箱格式不正确',
cleanedValue: ''
}
}
return { isValid: true }
}
// 在@input中执行的额外逻辑(后执行)
const handleEmailValidation = (value) => {
// 可以执行一些副作用操作
console.log('邮箱输入:', value)
// 或者执行一些不需要阻止输入的校验
if (value && value.length > 50) {
ElMessage.warning('邮箱地址过长')
}
}
</script>
(1)只使用安全拦截器
<template>
<el-input
v-model="email"
:security-config="{
enableWarning: true,
whitelist: ['@']
}"
<!-- 不添加 @input 事件 -->
/>
</template>
(2)只使用自定义校验
<template>
<el-input
v-model="email"
@input="handleEmailValidation"
:security-config="{
enableWarning: false, <!-- 关闭安全拦截器 -->
whitelist: ['@']
}"
/>
</template>
(3)使用自定义校验函数替代@input
<template>
<el-input
v-model="email"
:security-config="{
enableWarning: true,
whitelist: ['@'],
customValidator: validateEmail <!-- 在安全拦截器内部执行 -->
}"
/>
</template>
<script setup>
const validateEmail = (value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (value && !emailRegex.test(value)) {
return {
isValid: false,
message: '请输入有效的邮箱地址',
cleanedValue: '' // 清空无效输入
}
}
return { isValid: true }
}
</script>
(4)条件性执行
<template>
<el-input
v-model="email"
@input="shouldValidate ? handleEmailValidation : undefined"
:security-config="{
enableWarning: true,
whitelist: ['@']
}"
/>
</template>
<script setup>
const shouldValidate = ref(true)
const handleEmailValidation = (value) => {
// 只在需要时执行校验
if (shouldValidate.value) {
// 校验逻辑
}
}
</script>