PWA 的安全性非常重要,因为 PWA 可以像原生应用一样安装到设备上,并且可以访问更多设备功能。以下是 PWA 安全性的关键方面和最佳实践:
1. HTTPS 的必要性
为什么 PWA 必须使用 HTTPS
- Service Worker 要求:Service Worker 只能在 HTTPS 环境下运行(localhost 除外)
- 数据安全:保护用户数据在传输过程中的安全
- 信任度:HTTPS 提供身份验证,防止中间人攻击
- 浏览器要求:现代浏览器要求 PWA 必须使用 HTTPS
- 推送通知:Web Push API 要求 HTTPS
配置 HTTPS
nginx# Nginx 配置示例 server { listen 443 ssl http2; server_name example.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; # SSL 配置 ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; # HSTS add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; } # HTTP 重定向到 HTTPS server { listen 80; server_name example.com; return 301 https://$server_name$request_uri; }
2. Content Security Policy (CSP)
CSP 的作用
CSP 可以帮助防止跨站脚本攻击(XSS)、点击劫持等安全威胁。
配置 CSP
html<!-- 通过 HTTP 头配置 --> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; font-src 'self' https://fonts.gstatic.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';"> <!-- 通过服务器配置 -->
nginx# Nginx 配置 add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; font-src 'self' https://fonts.gstatic.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';";
CSP 指令说明
default-src:默认策略script-src:脚本来源style-src:样式来源img-src:图片来源connect-src:网络请求来源font-src:字体来源object-src:插件来源frame-ancestors:允许嵌入的父页面form-action:表单提交目标
3. Service Worker 安全
限制 Service Worker 作用域
javascript// 将 Service Worker 放在根目录,控制整个应用 navigator.serviceWorker.register('/sw.js'); // 或者放在特定目录,只控制该目录 navigator.serviceWorker.register('/app/sw.js', { scope: '/app/' });
验证 Service Worker 更新
javascript// sw.js self.addEventListener('install', event => { event.waitUntil( caches.open('my-cache').then(cache => { return cache.addAll([ '/', '/styles/main.css', '/scripts/app.js' ]); }) ); }); self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { // 只删除属于当前应用的缓存 if (cacheName.startsWith('my-app-')) { return caches.delete(cacheName); } }) ); }) ); });
防止缓存污染
javascriptself.addEventListener('fetch', event => { const url = new URL(event.request.url); // 只缓存同源资源 if (url.origin !== self.location.origin) { event.respondWith(fetch(event.request)); return; } // 处理同源请求 event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request); }) ); });
4. 数据安全
敏感数据加密
javascript// 使用 Web Crypto API 加密数据 async function encryptData(data, key) { const encoder = new TextEncoder(); const encodedData = encoder.encode(data); const iv = crypto.getRandomValues(new Uint8Array(12)); const encryptedData = await crypto.subtle.encrypt( { name: 'AES-GCM', iv: iv }, key, encodedData ); return { iv: Array.from(iv), data: Array.from(new Uint8Array(encryptedData)) }; } async function decryptData(encryptedData, key) { const iv = new Uint8Array(encryptedData.iv); const data = new Uint8Array(encryptedData.data); const decryptedData = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: iv }, key, data ); const decoder = new TextDecoder(); return decoder.decode(decryptedData); }
安全存储
javascript// 使用 IndexedDB 存储敏感数据 const dbPromise = idb.open('secure-db', 1, upgradeDB => { upgradeDB.createObjectStore('secure-data', { keyPath: 'id' }); }); async function storeSecureData(id, data) { const db = await dbPromise; await db.put('secure-data', { id: id, data: data, timestamp: Date.now() }); } // 避免在 localStorage 中存储敏感信息 // ❌ 不安全 localStorage.setItem('token', 'sensitive-token'); // ✅ 使用 IndexedDB 或加密后存储
5. 推送通知安全
VAPID 密钥管理
javascript// 服务器端安全存储 VAPID 密钥 const vapidKeys = { publicKey: process.env.VAPID_PUBLIC_KEY, privateKey: process.env.VAPID_PRIVATE_KEY }; // 不要在前端暴露私钥 // ❌ 错误 const privateKey = 'my-private-key'; // 不要这样做 // ✅ 正确:私钥只在服务器端使用
验证推送订阅
javascript// 服务器端验证订阅信息 async function validateSubscription(subscription) { // 检查必需字段 if (!subscription.endpoint || !subscription.keys) { throw new Error('Invalid subscription'); } // 检查密钥格式 if (!subscription.keys.p256dh || !subscription.keys.auth) { throw new Error('Invalid keys'); } // 验证 endpoint 格式 try { new URL(subscription.endpoint); } catch (error) { throw new Error('Invalid endpoint'); } return true; }
6. 跨域安全
CORS 配置
nginx# Nginx 配置 CORS location /api/ { add_header 'Access-Control-Allow-Origin' 'https://your-pwa.com'; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization'; add_header 'Access-Control-Allow-Credentials' 'true'; if ($request_method = 'OPTIONS') { return 204; } }
javascript// 前端请求配置 fetch('https://api.example.com/data', { method: 'GET', credentials: 'include', // 包含 cookies headers: { 'Content-Type': 'application/json' } });
7. 认证和授权
使用 JWT 进行认证
javascript// 登录后获取 JWT async function login(username, password) { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const data = await response.json(); // 安全存储 JWT if (data.token) { await storeSecureToken(data.token); } return data; } // 在请求中携带 JWT async function fetchData() { const token = await getSecureToken(); const response = await fetch('/api/data', { headers: { 'Authorization': `Bearer ${token}` } }); return response.json(); }
Token 刷新机制
javascript// 自动刷新过期 Token async function fetchWithAuth(url, options = {}) { let token = await getSecureToken(); // 检查 Token 是否即将过期 if (isTokenExpiringSoon(token)) { token = await refreshToken(); } const response = await fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${token}` } }); // 如果 Token 无效,尝试刷新 if (response.status === 401) { token = await refreshToken(); return fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${token}` } }); } return response; }
8. 安全最佳实践
1. 输入验证
javascript// 验证用户输入 function validateInput(input) { // 防止 XSS const sanitized = input.replace(/[<>]/g, ''); // 验证格式 if (!/^[a-zA-Z0-9]+$/.test(sanitized)) { throw new Error('Invalid input'); } return sanitized; }
2. 防止 CSRF
javascript// 使用 CSRF Token async function submitForm(data) { const csrfToken = await getCsrfToken(); const response = await fetch('/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify(data) }); return response.json(); }
3. 安全的 Service Worker 更新
javascript// 检查 Service Worker 更新 navigator.serviceWorker.addEventListener('controllerchange', () => { // 通知用户应用已更新 showUpdateNotification(); }); // 提示用户刷新页面 function showUpdateNotification() { const notification = document.createElement('div'); notification.textContent = '应用有新版本可用,点击刷新'; notification.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: #007bff; color: white; padding: 15px 20px; border-radius: 4px; cursor: pointer; z-index: 9999; `; notification.addEventListener('click', () => { window.location.reload(); }); document.body.appendChild(notification); }
9. 安全审计和监控
使用 Lighthouse 进行安全审计
bash# 运行 Lighthouse 安全审计 lighthouse https://your-pwa.com --view --only-categories=security
监控安全事件
javascript// 监控可疑活动 function logSecurityEvent(event) { fetch('/api/security-log', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ event: event.type, timestamp: Date.now(), userAgent: navigator.userAgent, url: window.location.href }) }); } // 监控 Service Worker 错误 navigator.serviceWorker.addEventListener('error', (event) => { logSecurityEvent({ type: 'service-worker-error', error: event.error }); });
总结
PWA 安全性的关键点:
- 必须使用 HTTPS:Service Worker 和推送通知都要求 HTTPS
- 配置 CSP:防止 XSS 和其他注入攻击
- 限制 Service Worker 作用域:防止缓存污染
- 安全存储敏感数据:使用 IndexedDB 和加密
- 验证推送订阅:防止恶意推送
- 正确配置 CORS:防止跨域攻击
- 使用安全的认证机制:JWT + Token 刷新
- 输入验证和输出编码:防止注入攻击
- 定期安全审计:使用 Lighthouse 等工具
- 监控安全事件:及时发现和响应安全威胁