概述
Web 应用安全是开发过程中不可忽视的重要环节。Vue 应用主要面临以下安全威胁:
| 安全威胁 | 风险等级 | 主要影响 |
|---|---|---|
| XSS(跨站脚本攻击) | 高 | 数据泄露、会话劫持 |
| CSRF(跨站请求伪造) | 高 | 非法操作、权限滥用 |
| 敏感数据泄露 | 高 | 隐私泄露、财产损失 |
| 点击劫持 | 中 | 用户误操作 |
| 第三方库漏洞 | 中 | 供应链攻击 |
XSS 防护
什么是 XSS
XSS(Cross-Site Scripting)攻击者通过注入恶意脚本代码,在用户浏览器中执行,从而窃取用户信息或进行恶意操作。
Vue 的默认防护
Vue 提供了基本的 XSS 防护机制:
js
// Vue 会自动转义 HTML 内容
<template>
<!-- 安全:内容会被转义 -->
<div>{{ userInput }}</div>
</template>
// 等价于
<div><script>alert('XSS')</script></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 apihtml
<!-- 在 HTML 头部放置 CSRF Token -->
<head>
<meta name="csrf-token" content="<%= csrfToken %>">
</head>SameSite Cookie
js
// 后端设置 Cookie
// Set-Cookie: sessionId=xxx; SameSite=Strict; Secure; HttpOnly
// SameSite 选项说明:
// - Strict: 完全禁止跨站 Cookie
// - Lax: 允许安全的跨站请求(GET 链接、预加载)
// - None: 允许跨站 Cookie(需配合 Secure)双重 Cookie 验证
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 auditjs
// 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 策略
□ 防止点击劫持
□ 定期更新依赖
□ 实施输入验证安全优先级
| 优先级 | 安全措施 | 说明 |
|---|---|---|
| P0 | XSS 防护 | 最常见的前端安全漏洞 |
| P0 | 敏感数据保护 | 直接影响用户财产安全 |
| P1 | CSRF 防护 | 认证相关操作必须防护 |
| P1 | 输入验证 | 防止注入攻击的基础 |
| P2 | CSP 配置 | 深度防御策略 |
| P2 | 依赖安全 | 供应链安全 |