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, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#x27;')
      .replace(/\//g, '&#x2F;')
  },
  
  // 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}&timestamp=${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')

四、安全检查清单

✅ 必做项:

  1. 输入验证:所有用户输入必须验证和过滤

  2. 输出编码:动态内容必须正确编码

  3. HTTPS:生产环境必须使用HTTPS

  4. CSP配置:配置合适的内容安全策略

  5. CSRF防护:重要操作必须有CSRF Token

  6. 敏感数据加密:本地存储的敏感数据必须加密

  7. 依赖检查:定期更新依赖,检查安全漏洞

  8. 错误处理:不要暴露系统信息给用户

五、应急响应方案

// 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前端安全防护需要多层次、全方位的保护:

  1. 预防为主:输入验证、输出编码

  2. 监控为辅:实时检测、日志记录

  3. 应急响应:快速反应、最小化损失

  4. 持续改进:定期审计、更新策略

记住:没有100%的安全,但可以有100%的努力。安全是一个持续的过程,需要开发者始终保持警惕和更新知识。

posted @ 2026-02-10 17:26  麻辣~香锅  阅读(2)  评论(0)    收藏  举报