{T}

概述

Web 应用安全是开发过程中不可忽视的重要环节。Vue 应用主要面临以下安全威胁:

安全威胁风险等级主要影响
XSS(跨站脚本攻击)数据泄露、会话劫持
CSRF(跨站请求伪造)非法操作、权限滥用
敏感数据泄露隐私泄露、财产损失
点击劫持用户误操作
第三方库漏洞供应链攻击

XSS 防护

什么是 XSS

XSS(Cross-Site Scripting)攻击者通过注入恶意脚本代码,在用户浏览器中执行,从而窃取用户信息或进行恶意操作。

Vue 的默认防护

Vue 提供了基本的 XSS 防护机制:

js
// Vue 会自动转义 HTML 内容
<template>
  <!-- 安全:内容会被转义 -->
  <div>{{ userInput }}</div>
</template>

// 等价于
<div>&lt;script&gt;alert('XSS')&lt;/script&gt;</div>

危险的 v-html

v-html 会直接渲染 HTML 内容,存在 XSS 风险:

Vue SFC
<template>
  <!-- 危险:不要渲染用户输入的内容 -->
  <div v-html="userInput"></div>
</template>

<script>
export default {
  data() {
    return {
      // 恶意脚本会被执行
      userInput: '<script>alert("XSS")<\/script>'
    }
  }
}
</script>

安全使用 v-html

如果必须使用 v-html,请确保内容已消毒:

js
// 安装 DOMPurify
// npm install dompurify

import DOMPurify from 'dompurify'

export default {
  methods: {
    sanitizeHtml(html) {
      // 清理恶意脚本
      return DOMPurify.sanitize(html, {
        ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u'],
        ALLOWED_ATTR: []
      })
    }
  }
}
Vue SFC
<template>
  <div v-html="sanitizeHtml(content)"></div>
</template>

URL 安全处理

js
// 危险:用户输入的 URL 可能包含 javascript: 协议
<template>
  <a :href="userUrl">点击链接</a>
</template>

<!-- 攻击示例 -->
userUrl: "javascript:alert('XSS')"

// 安全:验证 URL 协议
export default {
  methods: {
    isSafeUrl(url) {
      // 白名单协议
      const safeProtocols = ['http:', 'https:', 'mailto:']
      try {
        const parsedUrl = new URL(url, window.location.origin)
        return safeProtocols.includes(parsedUrl.protocol)
      } catch {
        return false
      }
    },
    
    getSafeUrl(url) {
      if (this.isSafeUrl(url)) {
        return url
      }
      return '#' // 或返回安全默认值
    }
  }
}
Vue SFC
<template>
  <a :href="getSafeUrl(userUrl)">点击链接</a>
</template>

样式注入防护

js
// 危险:用户输入的样式可能包含表达式
<template>
  <div :style="userStyle">内容</div>
</template>

<!-- 攻击示例 -->
userStyle: {
  background: 'url(javascript:alert("XSS"))'
}

// 安全:样式白名单
export default {
  methods: {
    sanitizeStyle(style) {
      const allowed = ['color', 'fontSize', 'fontWeight', 'textAlign']
      const sanitized = {}
      
      for (const [key, value] of Object.entries(style)) {
        if (allowed.includes(key)) {
          // 移除可能的危险字符
          sanitized[key] = String(value).replace(/[<>]/g, '')
        }
      }
      
      return sanitized
    }
  }
}

CSRF 防护

什么是 CSRF

CSRF(Cross-Site Request Forgery)攻击者诱导用户在已登录的网站上执行非预期操作。

CSRF 攻击示例

html
<!-- 攻击者网站 -->
<form action="https://bank.com/transfer" method="POST">
  <input type="hidden" name="to" value="attacker" />
  <input type="hidden" name="amount" value="10000" />
  <input type="submit" value="领取红包" />
</form>

CSRF Token 防护

js
// 后端生成 CSRF Token
// 在请求头中携带 Token

// axios 配置
import axios from 'axios'

// 从 meta 标签获取 CSRF Token
function getCsrfToken() {
  const meta = document.querySelector('meta[name="csrf-token"]')
  return meta ? meta.getAttribute('content') : ''
}

// 配置 axios
const api = axios.create({
  baseURL: '/api',
  headers: {
    'X-CSRF-Token': getCsrfToken()
  }
})

// 请求拦截器
api.interceptors.request.use(config => {
  config.headers['X-CSRF-Token'] = getCsrfToken()
  return config
})

export default api
html
<!-- 在 HTML 头部放置 CSRF Token -->
<head>
  <meta name="csrf-token" content="<%= csrfToken %>">
</head>
js
// 后端设置 Cookie
// Set-Cookie: sessionId=xxx; SameSite=Strict; Secure; HttpOnly

// SameSite 选项说明:
// - Strict: 完全禁止跨站 Cookie
// - Lax: 允许安全的跨站请求(GET 链接、预加载)
// - None: 允许跨站 Cookie(需配合 Secure)
js
// 前端发送请求时携带 Cookie 值
export default {
  methods: {
    async submitForm(data) {
      // 获取 Cookie 中的验证值
      const csrfCookie = this.getCookie('csrf_token')
      
      const response = await api.post('/api/submit', data, {
        headers: {
          'X-CSRF-Token': csrfCookie
        }
      })
      
      return response.data
    },
    
    getCookie(name) {
      const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'))
      return match ? match[2] : ''
    }
  }
}

敏感数据处理

不要在前端存储敏感信息

js
// 危险:在前端存储敏感信息
localStorage.setItem('password', '123456')
localStorage.setItem('token', 'sensitive-token')

// 安全:使用 httpOnly Cookie 存储敏感 Token
// 后端设置:Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict

// 只在前端存储非敏感信息
sessionStorage.setItem('username', 'user123')
sessionStorage.setItem('preferences', JSON.stringify({ theme: 'dark' }))

数据脱敏

js
// 敏感数据脱敏显示
export default {
  methods: {
    // 手机号脱敏
    maskPhone(phone) {
      return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
    },
    
    // 邮箱脱敏
    maskEmail(email) {
      const [name, domain] = email.split('@')
      const maskedName = name.slice(0, 2) + '***'
      return `${maskedName}@${domain}`
    },
    
    // 身份证号脱敏
    maskIdCard(idCard) {
      return idCard.replace(/(\d{4})\d{10}(\d{4})/, '$1**********$2')
    },
    
    // 银行卡号脱敏
    maskBankCard(cardNo) {
      return cardNo.replace(/(\d{4})\d+(\d{4})/, '$1 **** **** $2')
    }
  }
}
Vue SFC
<template>
  <div>
    <p>手机号:{{ maskPhone(user.phone) }}</p>
    <p>邮箱:{{ maskEmail(user.email) }}</p>
    <p>身份证:{{ maskIdCard(user.idCard) }}</p>
  </div>
</template>

密码安全处理

js
// 前端密码加密(传输层)
import CryptoJS from 'crypto-js'

export default {
  methods: {
    // 密码加密(配合后端公钥)
    encryptPassword(password, publicKey) {
      // 使用 RSA 或 AES 加密
      // 注意:这只是传输加密,不是存储加密
      return CryptoJS.AES.encrypt(password, publicKey).toString()
    },
    
    async login(username, password) {
      // 获取后端公钥
      const { publicKey } = await api.get('/api/public-key')
      
      // 加密密码后传输
      const encryptedPassword = this.encryptPassword(password, publicKey)
      
      return api.post('/api/login', {
        username,
        password: encryptedPassword
      })
    }
  }
}

安全的 API 请求

js
// API 请求安全配置
import axios from 'axios'

const api = axios.create({
  baseURL: '/api',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器
api.interceptors.request.use(
  config => {
    // 添加认证 Token(如果使用 Header 方式)
    const token = sessionStorage.getItem('access_token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    
    return config
  },
  error => Promise.reject(error)
)

// 响应拦截器
api.interceptors.response.use(
  response => response.data,
  error => {
    // 处理认证错误
    if (error.response?.status === 401) {
      // 清除本地存储的认证信息
      sessionStorage.clear()
      // 跳转到登录页
      window.location.href = '/login'
    }
    
    return Promise.reject(error)
  }
)

export default api

其他安全措施

内容安全策略(CSP)

html
<!-- 在 HTML 头部添加 CSP -->
<head>
  <meta http-equiv="Content-Security-Policy" 
        content="
          default-src 'self';
          script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;
          style-src 'self' 'unsafe-inline';
          img-src 'self' data: https:;
          font-src 'self' https://fonts.gstatic.com;
          connect-src 'self' https://api.example.com;
        ">
</head>
js
// Vue CLI 配置 CSP
// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.plugin('html').tap(args => {
      args[0].cspPlugin = {
        enabled: true,
        policy: {
          'default-src': "'self'",
          'script-src': "'self' 'unsafe-inline' https://cdn.jsdelivr.net",
          'style-src': "'self' 'unsafe-inline'",
          'img-src': "'self' data: https:",
        }
      }
      return args
    })
  }
}

点击劫持防护

js
// 后端设置 X-Frame-Options 响应头
// X-Frame-Options: DENY
// 或
// X-Frame-Options: SAMEORIGIN

// 前端检测(作为补充)
if (window.self !== window.top) {
  // 页面被嵌入 iframe 中
  document.body.innerHTML = ''
  alert('此页面不允许在 iframe 中显示')
}

第三方库安全

bash
# 定期检查依赖漏洞
npm audit

# 自动修复可修复的漏洞
npm audit fix

# 检查过时的依赖
npm outdated

# 使用安全审计工具
npx better-npm-audit audit
js
// package.json 配置依赖版本锁定
{
  "dependencies": {
    "vue": "2.7.14", // 使用精确版本
    "axios": "1.6.0"
  }
}

输入验证

js
// 前端输入验证(用户体验优化)
export default {
  data() {
    return {
      form: {
        username: '',
        email: '',
        password: ''
      },
      rules: {
        username: [
          { required: true, message: '请输入用户名' },
          { min: 3, max: 20, message: '用户名长度 3-20 个字符' },
          { pattern: /^[a-zA-Z0-9_]+$/, message: '只能包含字母、数字和下划线' }
        ],
        email: [
          { required: true, message: '请输入邮箱' },
          { pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '请输入有效的邮箱地址' }
        ],
        password: [
          { required: true, message: '请输入密码' },
          { min: 8, message: '密码至少 8 个字符' },
          { 
            validator: (rule, value) => {
              const hasUpper = /[A-Z]/.test(value)
              const hasLower = /[a-z]/.test(value)
              const hasNumber = /\d/.test(value)
              const hasSpecial = /[!@#$%^&*]/.test(value)
              return hasUpper && hasLower && hasNumber && hasSpecial
            },
            message: '密码必须包含大小写字母、数字和特殊字符'
          }
        ]
      }
    }
  },
  methods: {
    validateForm() {
      for (const [field, rules] of Object.entries(this.rules)) {
        for (const rule of rules) {
          const value = this.form[field]
          
          if (rule.required && !value) {
            this.$message.error(rule.message)
            return false
          }
          
          if (rule.pattern && !rule.pattern.test(value)) {
            this.$message.error(rule.message)
            return false
          }
          
          if (rule.validator && !rule.validator(null, value)) {
            this.$message.error(rule.message)
            return false
          }
        }
      }
      return true
    }
  }
}

⚠️ 重要提示:前端验证仅用于提升用户体验,所有安全验证必须在后端再次执行。

安全最佳实践总结

安全检查清单

code
□ XSS 防护
  □ 避免使用 v-html 渲染用户输入
  □ 对必要的内容进行 HTML 消毒
  □ 验证 URL 协议白名单
  □ 限制样式注入

□ CSRF 防护
  □ 实现 CSRF Token 验证
  □ 配置 SameSite Cookie
  □ 敏感操作要求二次验证

□ 敏感数据保护
  □ 不在前端存储敏感信息
  □ 实施数据脱敏显示
  □ 密码传输加密
  □ Token 安全存储(httpOnly Cookie)

□ 其他安全措施
  □ 配置 CSP 策略
  □ 防止点击劫持
  □ 定期更新依赖
  □ 实施输入验证

安全优先级

优先级安全措施说明
P0XSS 防护最常见的前端安全漏洞
P0敏感数据保护直接影响用户财产安全
P1CSRF 防护认证相关操作必须防护
P1输入验证防止注入攻击的基础
P2CSP 配置深度防御策略
P2依赖安全供应链安全