16前端安全防护方案
一、前端安全的重要性
想象一下你的 Vue 应用是一座城堡:
-
XSS攻击 = 敌人伪装成居民混入
-
CSRF攻击 = 敌人伪造你的指令
-
数据泄露 = 城堡机密文件被盗
-
接口攻击 = 城门守卫被欺骗
二、安全威胁与防护方案
1. XSS(跨站脚本攻击)防护
威胁:攻击者在网页中注入恶意脚本
防护方案:
<template>
<!-- 危险做法 ❌ -->
<div v-html="userContent"></div>
<!-- 安全做法 ✅ -->
<div>{{ userContent }}</div>
</template>
<script setup>
import { ref } from 'vue'
// XSS防护工具函数
const xssUtils = {
// 1. HTML转义
escapeHTML: (str) => {
if (!str) return ''
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\//g, '/')
},
// 2. 富文本过滤(白名单机制)
filterRichText: (html) => {
const allowTags = ['p', 'br', 'strong', 'em', 'ul', 'li', 'a']
const allowAttrs = ['href', 'target', 'title']
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
// 递归清理节点
const cleanNode = (node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// 检查标签是否允许
if (!allowTags.includes(node.tagName.toLowerCase())) {
node.remove()
return
}
// 清理属性
const attrs = Array.from(node.attributes)
attrs.forEach(attr => {
if (!allowAttrs.includes(attr.name)) {
node.removeAttribute(attr.name)
}
// 特别处理href,防止javascript:
if (attr.name === 'href') {
const href = attr.value.toLowerCase()
if (href.startsWith('javascript:') || href.startsWith('data:')) {
node.removeAttribute('href')
}
}
})
// 递归处理子节点
Array.from(node.childNodes).forEach(cleanNode)
}
}
cleanNode(doc.body)
return doc.body.innerHTML
}
}
// 使用示例
const userContent = ref('<script>alert("xss")</script>')
const safeContent = computed(() => xssUtils.escapeHTML(userContent.value))
</script>
进阶方案:使用专业库
npm install xss dompurify
// utils/security.js import DOMPurify from 'dompurify' import xss from 'xss' // 配置DOMPurify(推荐) const purifyConfig = { ALLOWED_TAGS: ['p', 'strong', 'em', 'a', 'ul', 'li', 'br'], ALLOWED_ATTR: ['href', 'target', 'rel'], FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed'], FORBID_ATTR: ['onerror', 'onload', 'onclick'] } export const sanitizeHTML = (html) => { return DOMPurify.sanitize(html, purifyConfig) } // 或者在Vue中全局使用指令 // directives/safeHtml.js import { directive } from 'vue' import DOMPurify from 'dompurify' export const vSafeHtml = { mounted(el, binding) { el.innerHTML = DOMPurify.sanitize(binding.value, { ALLOWED_TAGS: [], ALLOWED_ATTR: [] }) }, updated(el, binding) { el.innerHTML = DOMPurify.sanitize(binding.value, { ALLOWED_TAGS: [], ALLOWED_ATTR: [] }) } }
2. CSRF(跨站请求伪造)防护
威胁:攻击者利用用户的登录状态发起恶意请求
防护方案:
// 1. 后端设置CSRF Token,前端存储和发送 // utils/csrf.js export const csrf = { token: null, // 从Cookie或Meta标签获取Token getToken() { // 从Cookie获取 const cookieToken = document.cookie .split('; ') .find(row => row.startsWith('XSRF-TOKEN=')) ?.split('=')[1] // 从Meta标签获取 const metaToken = document.querySelector('meta[name="csrf-token"]')?.content return cookieToken || metaToken || null }, // 发送请求时自动携带 setupInterceptors(axiosInstance) { axiosInstance.interceptors.request.use(config => { const token = this.getToken() if (token && !config.headers['X-CSRF-Token']) { config.headers['X-CSRF-Token'] = token config.headers['X-Requested-With'] = 'XMLHttpRequest' } return config }) } } // 2. 双重Cookie验证 export const setupDoubleCookie = () => { // 在登录时设置随机Token const setCSRFCookie = () => { const token = Math.random().toString(36).substring(2) document.cookie = `csrf_token=${token}; Path=/; SameSite=Strict` return token } return { setCSRFCookie } } // 在main.js中使用 import axios from 'axios' import { csrf } from './utils/csrf' csrf.setupInterceptors(axios)
3. 接口安全防护
防护方案:
// api/interceptors.js import axios from 'axios' import { useUserStore } from '@/stores/user' import CryptoJS from 'crypto-js' const request = axios.create({ baseURL: process.env.VITE_API_URL, timeout: 15000 }) // 1. 请求签名(防篡改) const generateSignature = (params, timestamp, secret) => { const sortedParams = Object.keys(params) .sort() .map(key => `${key}=${params[key]}`) .join('&') const signStr = `${sortedParams}×tamp=${timestamp}&secret=${secret}` return CryptoJS.MD5(signStr).toString() } // 2. 请求参数加密 const encryptParams = (params, key) => { const dataStr = JSON.stringify(params) return CryptoJS.AES.encrypt(dataStr, key).toString() } // 请求拦截器 request.interceptors.request.use(config => { const userStore = useUserStore() const timestamp = Date.now() // 添加时间戳(防重放攻击) if (config.method === 'get') { config.params = { ...config.params, _t: timestamp } } else { config.data = { ...config.data, _t: timestamp } } // 添加签名 const secret = userStore.appSecret || 'default-secret' const signature = generateSignature(config.params || {}, timestamp, secret) config.headers['X-Signature'] = signature // 重要数据加密 if (config.needEncrypt) { const encrypted = encryptParams(config.data, secret) config.data = { encrypted } } return config }) // 响应拦截器 - 解密 request.interceptors.response.use(response => { if (response.data.encrypted) { // 解密响应数据 const userStore = useUserStore() const bytes = CryptoJS.AES.decrypt( response.data.encrypted, userStore.appSecret ) response.data = JSON.parse(bytes.toString(CryptoJS.enc.Utf8)) } return response }) export default request
4. 数据存储安全
// utils/storage.js import CryptoJS from 'crypto-js' // 安全存储密钥(应通过安全方式传输,不能硬编码) const STORAGE_KEY = import.meta.env.VITE_STORAGE_SECRET export const secureStorage = { // 加密存储 setItem(key, value) { try { const encrypted = CryptoJS.AES.encrypt( JSON.stringify(value), STORAGE_KEY ).toString() localStorage.setItem(key, encrypted) } catch (error) { console.error('存储失败:', error) } }, // 解密读取 getItem(key) { try { const encrypted = localStorage.getItem(key) if (!encrypted) return null const bytes = CryptoJS.AES.decrypt(encrypted, STORAGE_KEY) const decrypted = bytes.toString(CryptoJS.enc.Utf8) return JSON.parse(decrypted) } catch (error) { console.error('读取失败:', error) return null } }, // 清除敏感数据 clearSensitive() { const sensitiveKeys = ['token', 'userInfo', 'privateKey'] sensitiveKeys.forEach(key => { localStorage.removeItem(key) sessionStorage.removeItem(key) }) } } // 使用示例 import { secureStorage } from './utils/storage' // 存储用户信息(自动加密) secureStorage.setItem('userInfo', { id: 123, name: '张三', token: 'jwt-token-here' }) // 读取(自动解密) const userInfo = secureStorage.getItem('userInfo')
5. 认证与授权安全
// utils/auth.js import jwt_decode from 'jwt-decode' import { secureStorage } from './storage' export const auth = { // JWT Token验证 verifyToken(token) { try { const decoded = jwt_decode(token) const currentTime = Date.now() / 1000 // 检查过期时间 if (decoded.exp < currentTime) { console.warn('Token已过期') return false } // 检查签发时间 if (decoded.iat > currentTime) { console.warn('Token签发时间异常') return false } return decoded } catch (error) { console.error('Token解析失败:', error) return false } }, // 权限验证 checkPermission(requiredPermission, userPermissions) { if (!requiredPermission) return true // 支持数组权限(满足其一即可) if (Array.isArray(requiredPermission)) { return requiredPermission.some(perm => userPermissions?.includes(perm) ) } // 单权限检查 return userPermissions?.includes(requiredPermission) }, // 路由守卫 setupRouterGuard(router) { router.beforeEach((to, from, next) => { const userStore = useUserStore() // 验证Token有效性 if (userStore.token) { const isValid = this.verifyToken(userStore.token) if (!isValid) { userStore.logout() next('/login') return } } // 检查是否需要登录 if (to.meta.requiresAuth && !userStore.isLoggedIn) { next('/login') return } // 检查页面权限 if (to.meta.permissions) { const hasPermission = this.checkPermission( to.meta.permissions, userStore.permissions ) if (!hasPermission) { next('/403') // 无权限页面 return } } next() }) } } // Vue路由权限配置示例 const routes = [ { path: '/admin', component: () => import('@/views/Admin.vue'), meta: { requiresAuth: true, permissions: ['admin', 'super-admin'] // 需要这些权限之一 } } ]
6. 环境变量与配置安全
// .env.production VITE_API_URL=https://api.yourdomain.com VITE_APP_KEY=production_key_here # 敏感数据应该通过CI/CD注入,不应直接写在代码中 // config/security.js export const securityConfig = { // Content Security Policy (CSP) csp: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'", "https://trusted.cdn.com"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https://trusted.cdn.com"], connectSrc: ["'self'", process.env.VITE_API_URL], fontSrc: ["'self'"], objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'none'"] }, // HTTP安全头配置 headers: { 'X-Frame-Options': 'DENY', 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'strict-origin-when-cross-origin', 'Permissions-Policy': 'camera=(), microphone=(), geolocation=()' } } // 在index.html中添加CSP Meta标签 // <meta http-equiv="Content-Security-Policy" content="default-src 'self';">
7. 输入验证与过滤
<template>
<form @submit.prevent="handleSubmit">
<!-- 危险:直接绑定 -->
<input v-model="userInput" />
<!-- 安全:使用验证后的数据 -->
<input
:value="filteredInput"
@input="handleInput"
/>
<button type="submit">提交</button>
</form>
</template>
<script setup>
import { ref, computed } from 'vue'
const userInput = ref('')
const dangerousPatterns = [
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
/javascript:/gi,
/on\w+\s*=/gi,
/data:/gi
]
// 输入过滤
const filteredInput = computed(() => {
let safeText = userInput.value
dangerousPatterns.forEach(pattern => {
safeText = safeText.replace(pattern, '')
})
// 长度限制
if (safeText.length > 1000) {
safeText = safeText.substring(0, 1000)
}
return safeText
})
// 表单验证
const validateInput = (value) => {
const rules = {
required: value => !!value?.trim(),
maxLength: value => value.length <= 100,
noScript: value => !/<script/i.test(value),
safeChars: value => /^[\w\s@.-]+$/.test(value)
}
return Object.entries(rules).every(([rule, validate]) => validate(value))
}
const handleInput = (event) => {
const value = event.target.value
if (validateInput(value)) {
userInput.value = value
} else {
event.target.value = filteredInput.value
}
}
const handleSubmit = () => {
if (!validateInput(userInput.value)) {
alert('输入包含不安全内容')
return
}
// 提交安全数据
}
</script>
8. 前端监控与日志
// utils/securityMonitor.js export class SecurityMonitor { constructor() { this.threats = [] this.maxLogSize = 1000 } // 检测可疑行为 detectXSSAttempt(input) { const xssPatterns = [ /<script/i, /javascript:/i, /on\w+\s*=/i, /eval\(/i, /document\.cookie/i ] return xssPatterns.some(pattern => pattern.test(input)) } // 记录安全事件 logSecurityEvent(type, data) { const event = { type, data, timestamp: new Date().toISOString(), userAgent: navigator.userAgent, url: window.location.href } this.threats.unshift(event) // 限制日志大小 if (this.threats.length > this.maxLogSize) { this.threats.pop() } // 上报到服务器(生产环境) if (process.env.NODE_ENV === 'production') { this.reportToServer(event) } console.warn(`安全事件: ${type}`, event) } // 上报服务器 async reportToServer(event) { try { await fetch('/api/security/log', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(event), credentials: 'same-origin' }) } catch (error) { console.error('安全日志上报失败:', error) } } // 全局错误监控 setupErrorMonitoring() { window.addEventListener('error', (event) => { if (event.message.includes('Script error')) { this.logSecurityEvent('SCRIPT_ERROR', { message: event.message, filename: event.filename, lineno: event.lineno }) } }) // 捕获Promise错误 window.addEventListener('unhandledrejection', (event) => { this.logSecurityEvent('PROMISE_ERROR', { reason: event.reason?.toString() }) }) } } // 在main.js中使用 import { SecurityMonitor } from './utils/securityMonitor' const securityMonitor = new SecurityMonitor() securityMonitor.setupErrorMonitoring() // 全局异常处理 app.config.errorHandler = (err, instance, info) => { securityMonitor.logSecurityEvent('VUE_ERROR', { error: err.toString(), component: instance?.$options.name, info }) }
三、完整的安全方案集成
// security/index.js - 安全模块主入口 import { xssProtection } from './xss' import { csrfProtection } from './csrf' import { apiSecurity } from './api' import { storageSecurity } from './storage' import { SecurityMonitor } from './monitor' class VueSecurity { constructor(app, router) { this.app = app this.router = router this.monitor = new SecurityMonitor() this.init() } init() { // 1. 设置CSP this.setupCSP() // 2. 设置安全头 this.setupSecurityHeaders() // 3. 初始化各安全模块 xssProtection.setup(this.app) csrfProtection.setup() apiSecurity.setupInterceptors() storageSecurity.setup() // 4. 路由守卫 this.setupRouterGuard() // 5. 全局错误监控 this.monitor.setupErrorMonitoring() // 6. 开发环境警告 if (process.env.NODE_ENV === 'development') { this.setupDevWarnings() } } setupCSP() { // 添加CSP Meta标签 const meta = document.createElement('meta') meta.httpEquiv = 'Content-Security-Policy' meta.content = ` default-src 'self'; script-src 'self' 'unsafe-inline' https://trusted.cdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' ${process.env.VITE_API_URL}; font-src 'self'; object-src 'none'; frame-src 'none'; `.replace(/\s+/g, ' ') document.head.appendChild(meta) } setupRouterGuard() { this.router.beforeEach((to, from, next) => { // 安全检查 if (this.detectThreats(to)) { this.monitor.logSecurityEvent('ROUTE_THREAT', { to, from }) next('/security-warning') return } next() }) } detectThreats(route) { // 检测可疑路由参数 const params = route.query const suspiciousValues = Object.values(params).some(value => /<script|javascript:|on\w+=/i.test(value) ) return suspiciousValues } setupDevWarnings() { console.warn('🔐 安全模式已启用') console.warn('⚠️ 请不要在生产环境暴露敏感信息') } } // 在main.js中使用 import { createApp } from 'vue' import App from './App.vue' import router from './router' import { VueSecurity } from './security' const app = createApp(App) // 初始化安全模块 const security = new VueSecurity(app, router) app.use(router) app.mount('#app')
四、安全检查清单
✅ 必做项:
-
输入验证:所有用户输入必须验证和过滤
-
输出编码:动态内容必须正确编码
-
HTTPS:生产环境必须使用HTTPS
-
CSP配置:配置合适的内容安全策略
-
CSRF防护:重要操作必须有CSRF Token
-
敏感数据加密:本地存储的敏感数据必须加密
-
依赖检查:定期更新依赖,检查安全漏洞
-
错误处理:不要暴露系统信息给用户
五、应急响应方案
// utils/emergency.js export const emergency = { // 检测到攻击时的处理 handleAttack(type, details) { // 1. 记录日志 securityMonitor.logSecurityEvent(`ATTACK_${type}`, details) // 2. 清除敏感数据 secureStorage.clearSensitive() // 3. 通知用户 this.showSecurityAlert() // 4. 上报服务器 this.reportAttack(type, details) // 5. 重定向到安全页面 if (this.router.currentRoute.value.path !== '/security') { this.router.push('/security-warning') } }, showSecurityAlert() { const alert = document.createElement('div') alert.className = 'security-alert' alert.innerHTML = ` <h3>⚠️ 安全警告</h3> <p>检测到可疑活动,已启动保护措施</p> <button onclick="this.parentNode.remove()">确定</button> ` document.body.appendChild(alert) } }
总结
Vue前端安全防护需要多层次、全方位的保护:
-
预防为主:输入验证、输出编码
-
监控为辅:实时检测、日志记录
-
应急响应:快速反应、最小化损失
-
持续改进:定期审计、更新策略
记住:没有100%的安全,但可以有100%的努力。安全是一个持续的过程,需要开发者始终保持警惕和更新知识。

浙公网安备 33010602011771号