前端安全防护最佳实践:构建安全的Web应用

前端安全威胁概述 前端安全是Web应用的重要组成部分。主要的安全威胁包括: XSS(跨站脚本攻击) CSRF(跨站请求伪造) 点击劫持 中间人攻击 敏感信息泄露 XSS防护 内容安全策略(CSP) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // 通过HTTP头设置CSP // Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com // 在HTML中设置 <meta http-equiv="Content-Security-Policy" content=" default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.trusted.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; "> // Nuxt配置 export default defineNuxtConfig({ app: { head: { meta: [ { 'http-equiv': 'Content-Security-Policy', content: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" } ] } } }) 输入转义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // HTML转义函数 function escapeHtml(unsafe) { return unsafe .replace(/&/g, "&amp;") .replace(/</g, "&lt;") .replace(/>/g, "&gt;") .replace(/"/g, "&quot;") .replace(/'/g, "&#039;") } // URL转义 function escapeUrl(unsafe) { return encodeURIComponent(unsafe) } // 使用DOM API而非innerHTML const div = document.createElement('div') div.textContent = userInput // 自动转义 document.body.appendChild(div) Vue自动转义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <!-- Vue自动转义 --> <div>{{ userInput }}</div> <!-- 使用v-html时需要特别小心 --> <div v-html="sanitizedHtml"></div> </template> <script setup> import DOMPurify from 'dompurify' const userInput = ref('<script>alert("XSS")<\/script>') // 使用DOMPurify清理HTML const sanitizedHtml = computed(() => { return DOMPurify.sanitize(userInput.value) }) </script> CSRF防护 CSRF Token 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 生成CSRF Token function generateCsrfToken() { const array = new Uint8Array(32) crypto.getRandomValues(array) return Array.from(array, b => b.toString(16).padStart(2, '0')).join('') } // 存储Token document.cookie = `csrf_token=${generateCsrfToken()}; SameSite=Strict; Secure` // 发送请求时携带Token fetch('/api/data', { method: 'POST', headers: { 'X-CSRF-Token': getCsrfToken() }, body: JSON.stringify(data) }) SameSite Cookie 1 2 3 4 5 // 设置SameSite属性 document.cookie = 'session_id=xxx; SameSite=Strict; Secure; HttpOnly' // 服务器端设置Cookie Set-Cookie: session_id=xxx; SameSite=Strict; Secure; HttpOnly 点击劫持防护 X-Frame-Options 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 通过HTTP头设置 X-Frame-Options: DENY X-Frame-Options: SAMEORIGIN // Nuxt配置 export default defineNuxtConfig({ routeRules: { '/**': { headers: { 'X-Frame-Options': 'DENY' } } } }) Frame Ancestors CSP 1 2 3 // 使用CSP frame-ancestors Content-Security-Policy: frame-ancestors 'none'; Content-Security-Policy: frame-ancestors 'self'; 敏感信息保护 不在本地存储敏感信息 1 2 3 4 5 6 7 8 9 10 11 12 // ❌ 不要存储密码 localStorage.setItem('password', password) // ❌ 不要存储敏感token localStorage.setItem('api_key', api_key) // ✅ 使用httpOnly cookie // 服务器端设置 Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict // ✅ 使用sessionStorage(关闭浏览器即清除) sessionStorage.setItem('temp_data', data) 安全的数据传输 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 始终使用HTTPS fetch('https://api.example.com/data', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) // 启用HSTS Strict-Transport-Security: max-age=31536000; includeSubDomains // 子域名资源限制 Content-Security-Policy: frame-ancestors 'self' https://trusted.example.com; 密码安全 前端密码验证 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function validatePassword(password) { const checks = { length: password.length >= 8, uppercase: /[A-Z]/.test(password), lowercase: /[a-z]/.test(password), numbers: /\d/.test(password), special: /[!@#$%^&*]/.test(password) } return { valid: Object.values(checks).every(Boolean), checks } } // 密码强度评估 function getPasswordStrength(password) { let score = 0 if (password.length >= 8) score++ if (password.length >= 12) score++ if (/[a-z]/.test(password)) score++ if (/[A-Z]/.test(password)) score++ if (/\d/.test(password)) score++ if (/[!@#$%^&*]/.test(password)) score++ return ['weak', 'fair', 'good', 'strong'][score] } 安全的密码输入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <template> <div class="password-input"> <input :type="showPassword ? 'text' : 'password'" v-model="password" @input="validate" /> <button @click="showPassword = !showPassword"> {{ showPassword ? 'Hide' : 'Show' }} </button> <div v-if="error" class="error">{{ error }}</div> </div> </template> <script setup> const password = ref('') const showPassword = ref(false) const error = ref('') function validate() { const result = validatePassword(password.value) if (!result.valid) { const failedChecks = Object.entries(result.checks) .filter(([, valid]) => !valid) .map(([check]) => check) error.value = `Password fails: ${failedChecks.join(', ')}` } else { error.value = '' } } </script> API安全 请求签名 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 async function makeSecureRequest(url, data) { const timestamp = Date.now() const nonce = generateNonce() // 构建签名内容 const signatureContent = `${timestamp}${nonce}${JSON.stringify(data)}` const signature = await hmac(signatureContent, secretKey) return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Timestamp': timestamp, 'X-Nonce': nonce, 'X-Signature': signature }, body: JSON.stringify(data) }) } 响应数据验证 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 async function fetchAndValidate(url) { const response = await fetch(url) const data = await response.json() // 验证响应数据结构 const validatedData = validateResponseSchema(data, { required: ['id', 'name'], types: { id: 'number', name: 'string', email: 'string' } }) return validatedData } function validateResponseSchema(data, schema) { const errors = [] for (const field of schema.required) { if (!(field in data)) { errors.push(`Missing required field: ${field}`) } } for (const [field, type] of Object.entries(schema.types)) { if (field in data && typeof data[field] !== type) { errors.push(`Invalid type for ${field}: expected ${type}`) } } if (errors.length > 0) { throw new Error(`Validation error: ${errors.join(', ')}`) } return data } 第三方资源安全 Subresource Integrity (SRI) 1 2 3 4 5 6 <!-- 使用SRI确保CDN资源未被篡改 --> <script src="https://cdn.example.com/library.js" integrity="sha384-..." crossorigin="anonymous" ></script> 预加载安全检查 1 2 3 4 5 6 7 8 9 10 11 12 13 // 验证外部脚本 async function validateExternalScript(url) { const response = await fetch(url) const content = await response.text() // 检查是否包含可疑代码 if (content.includes('eval') || content.includes('innerHTML')) { console.warn('Potentially unsafe script detected') return false } return true } 安全审计 依赖检查 1 2 3 4 5 6 7 8 # 检查依赖漏洞 npm audit # 自动修复漏洞 npm audit fix # 使用Snyk进行深度扫描 npx snyk test 代码安全扫描 1 2 3 4 5 6 7 8 // ESLint安全规则 // .eslintrc.js module.exports = { extends: [ 'plugin:security/recommended' ], plugins: ['security'] } 安全监控 实时监控 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 class SecurityMonitor { constructor() { this.init() } init() { // 监控XSS尝试 this.monitorXSS() // 监控异常请求 this.monitorRequests() // 监控控制台错误 this.monitorErrors() } monitorXSS() { const originalCreateElement = document.createElement document.createElement = function(tagName) { const element = originalCreateElement.call(this, tagName) if (tagName === 'script') { let originalSrc = '' Object.defineProperty(element, 'src', { get() { return originalSrc }, set(value) { // 检查脚本来源 if (!this.isTrustedSource(value)) { console.warn('Untrusted script source:', value) securityEvent('untrusted_script', { src: value }) } originalSrc = value } }) } return element } } monitorRequests() { const originalFetch = window.fetch window.fetch = function(...args) { const url = args[0] // 检查请求目标 if (!this.isTrustedDomain(url)) { console.warn('Request to untrusted domain:', url) securityEvent('untrusted_request', { url }) } return originalFetch.apply(this, args) } } monitorErrors() { window.addEventListener('error', (event) => { if (event.message.includes('Script error')) { securityEvent('possible_xss', { message: event.message, filename: event.filename }) } }) } isTrustedSource(url) { const trusted = ['https://cdn.trusted.com', 'https://api.trusted.com'] return trusted.some(domain => url.startsWith(domain)) } isTrustedDomain(url) { try { const urlObj = new URL(url, window.location.origin) return urlObj.origin === window.location.origin } catch { return false } } } 最佳实践总结 永远不要信任用户输入 - 始终验证和转义 使用HTTPS - 保护数据传输 设置CSP - 限制资源加载 使用HttpOnly Cookie - 防止XSS窃取 定期审计 - 检查依赖和代码安全性 总结 前端安全是一个持续的过程。通过实施这些安全措施,可以大大降低应用的安全风险,保护用户数据安全。 ...

WebCrypto API实战:前端加密与安全通信完全指南

WebCrypto API简介 WebCrypto API是现代浏览器提供的原生加密API,提供了安全的加密操作能力。与第三方库相比,它有以下优势: 浏览器原生支持,无需额外依赖 使用系统级加密库,性能更优 安全的密钥管理,密钥不暴露给JavaScript 同步和异步API支持 对称加密(AES) 生成密钥 1 2 3 4 5 6 7 8 9 10 11 12 // 生成AES密钥 async function generateAesKey() { const key = await crypto.subtle.generateKey( { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'] ) return key } 加密数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 async function encryptData(data, key) { const encoder = new TextEncoder() const iv = crypto.getRandomValues(new Uint8Array(12)) const encrypted = await crypto.subtle.encrypt( { name: 'AES-GCM', iv: iv }, key, encoder.encode(data) ) return { ciphertext: new Uint8Array(encrypted), iv: iv } } 解密数据 1 2 3 4 5 6 7 8 9 10 11 12 13 async function decryptData(encryptedData, key, iv) { const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: iv }, key, encryptedData ) const decoder = new TextDecoder() return decoder.decode(decrypted) } 非对称加密(RSA) 生成密钥对 1 2 3 4 5 6 7 8 9 10 11 12 13 14 async function generateRsaKeyPair() { const keyPair = await crypto.subtle.generateKey( { name: 'RSA-OAEP', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, true, ['encrypt', 'decrypt'] ) return keyPair } 导出和导入密钥 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 导出公钥 async function exportPublicKey(key) { const exported = await crypto.subtle.exportKey( 'spki', key ) return new Uint8Array(exported) } // 导入公钥 async function importPublicKey(keyData) { return await crypto.subtle.importKey( 'spki', keyData, { name: 'RSA-OAEP', hash: 'SHA-256' }, true, ['encrypt'] ) } 哈希算法 计算哈希 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 async function hashData(data, algorithm = 'SHA-256') { const encoder = new TextEncoder() const dataBuffer = encoder.encode(data) const hashBuffer = await crypto.subtle.digest( algorithm, dataBuffer ) const hashArray = Array.from(new Uint8Array(hashBuffer)) return hashArray.map(b => b.toString(16).padStart(2, '0')).join('') } // 使用 const hash = await hashData('Hello, WebCrypto!', 'SHA-256') console.log(hash) // 64位十六进制字符串 HMAC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 async function hmac(key, message) { const encoder = new TextEncoder() const cryptoKey = await crypto.subtle.importKey( 'raw', encoder.encode(key), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ) const signature = await crypto.subtle.sign( 'HMAC', cryptoKey, encoder.encode(message) ) const hashArray = Array.from(new Uint8Array(signature)) return hashArray.map(b => b.toString(16).padStart(2, '0')).join('') } 数字签名 生成签名 1 2 3 4 5 6 7 8 9 10 11 12 13 14 async function signData(data, privateKey) { const encoder = new TextEncoder() const signature = await crypto.subtle.sign( { name: 'RSA-PSS', saltLength: 32 }, privateKey, encoder.encode(data) ) return new Uint8Array(signature) } 验证签名 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 async function verifySignature(data, signature, publicKey) { const encoder = new TextEncoder() const result = await crypto.subtle.verify( { name: 'RSA-PSS', saltLength: 32 }, publicKey, signature, encoder.encode(data) ) return result } 密钥派生(PBKDF2) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 async function deriveKey(password, salt) { const encoder = new TextEncoder() const keyMaterial = await crypto.subtle.importKey( 'raw', encoder.encode(password), 'PBKDF2', false, ['deriveKey'] ) const key = await crypto.subtle.deriveKey( { name: 'PBKDF2', salt: encoder.encode(salt), iterations: 100000, hash: 'SHA-256' }, keyMaterial, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'] ) return key } 实战案例 端到端加密聊天 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 class E2EChat { constructor() { this.keyPair = null this.peerKeys = new Map() } async init() { // 生成密钥对 this.keyPair = await crypto.subtle.generateKey( { name: 'RSA-OAEP', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, true, ['encrypt', 'decrypt'] ) } async getPublicKey() { const exported = await crypto.subtle.exportKey('spki', this.keyPair.publicKey) return btoa(String.fromCharCode(...new Uint8Array(exported))) } async addPeer(id, publicKeyData) { const binaryString = atob(publicKeyData) const bytes = new Uint8Array(binaryString.length) for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i) } const publicKey = await crypto.subtle.importKey( 'spki', bytes, { name: 'RSA-OAEP', hash: 'SHA-256' }, true, ['encrypt'] ) this.peerKeys.set(id, publicKey) } async sendMessage(peerId, message) { const publicKey = this.peerKeys.get(peerId) if (!publicKey) throw new Error('Peer not found') // 生成会话密钥 const sessionKey = await this.generateSessionKey() // 加密消息 const encryptedMessage = await this.encryptMessage(message, sessionKey) // 加密会话密钥 const encryptedKey = await this.encryptSessionKey(sessionKey, publicKey) return { message: encryptedMessage, key: encryptedKey } } async receiveMessage(data) { // 解密会话密钥 const sessionKey = await this.decryptSessionKey(data.key) // 解密消息 return await this.decryptMessage(data.message, sessionKey) } async generateSessionKey() { return await crypto.subtle.generateKey( { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'] ) } async encryptMessage(message, key) { const encoder = new TextEncoder() const iv = crypto.getRandomValues(new Uint8Array(12)) const encrypted = await crypto.subtle.encrypt( { name: 'AES-GCM', iv: iv }, key, encoder.encode(message) ) return { data: btoa(String.fromCharCode(...new Uint8Array(encrypted))), iv: btoa(String.fromCharCode(...iv)) } } async decryptMessage(data, key) { const ciphertext = Uint8Array.from(atob(data.data), c => c.charCodeAt(0)) const iv = Uint8Array.from(atob(data.iv), c => c.charCodeAt(0)) const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: iv }, key, ciphertext ) const decoder = new TextDecoder() return decoder.decode(decrypted) } } 安全最佳实践 密钥管理 ...

Web 安全最佳实践:保护你的应用免受攻击

全面介绍 Web 应用安全开发的最佳实践,防范常见的安全威胁