𝑻𝒆𝒏𝑪𝒍𝒂𝒘正在头脑风暴···
𝑻𝒆𝒏𝑲𝒊𝑺𝒆𝒀𝒂の𝑨𝒈𝒆𝒏𝒕助手
𝑻𝒆𝒏-𝒇𝒍𝒂𝒔𝒉

前端安全防护进阶

随着Web应用的日益复杂,前端安全已经成为不可忽视的重要环节。前端安全不仅关系到用户数据的安全,还直接影响企业的声誉和法律责任。本文将深入探讨前端安全的核心概念、常见攻击手段以及防护策略,帮助你构建更安全的前端应用。

前端安全概述

什么是前端安全?

前端安全是指在浏览器端通过各种技术和措施保护Web应用免受恶意攻击的一系列实践。它包括:

  1. 输入验证:确保用户输入的数据是安全且有效的
  2. 输出编码:防止恶意代码被执行
  3. 访问控制:限制用户对特定资源的访问
  4. 数据保护:保护敏感数据不被泄露
  5. 安全配置:正确配置安全相关的HTTP头

为什么前端安全重要?

  1. 用户体验:防止用户数据被盗用
  2. 法律合规:满足数据保护法规要求
  3. 商业声誉:安全漏洞会损害企业声誉
  4. 成本控制:避免因安全事件造成的高额损失

主要安全威胁

1. 跨站脚本攻击 (XSS)

攻击原理

XSS(Cross-Site Scripting)攻击是指攻击者在网页中注入恶意脚本,当用户访问该页面时,恶意脚本会在用户的浏览器中执行。

攻击类型

反射型XSS
// 攻击示例
const searchQuery = req.query.q;
res.send(`<h1>搜索结果: ${searchQuery}</h1>`);

// 攻击者构造的URL
// http://example.com/search?q=<script>alert('XSS')</script>
存储型XSS
// 攻击者提交恶意评论
const maliciousComment = {
content: "<script>stealUserCookies()</script>",
user: "hacker"
};

// 其他用户查看评论时恶意脚本执行
function displayComments(comments) {
comments.forEach(comment => {
document.getElementById('comments').innerHTML +=
`<div class="comment">${comment.content}</div>`;
});
}

防护措施

输入验证
// 使用专门的验证库
import validator from 'validator';

function validateInput(input) {
// 检查输入是否包含HTML标签
if (validator.isHTML(input)) {
throw new Error('输入包含不安全内容');
}

// 检查输入长度
if (input.length > 1000) {
throw new Error('输入过长');
}

return validator.escape(input);
}

// 或者使用正则表达式
function sanitizeInput(input) {
return input
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/javascript:/gi, '')
.replace(/on\w+\s*=/gi, '');
}
输出编码
// 使用DOM API的安全方法
function createElement safely(tag, content) {
const element = document.createElement(tag);
element.textContent = content; // 自动转义HTML
return element;
}

// 使用模板字符串时注意
const userInput = '<script>alert("xss")</script>';
const safeHTML = `<div>${escapeHtml(userInput)}</div>`;

// 或者使用textContent而非innerHTML
document.getElementById('output').textContent = userInput;
Content Security Policy (CSP)
<!-- 在HTML中设置CSP -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self' data:;
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'self';
form-action 'self';">

<!-- 或者通过HTTP头设置 -->
Content-Security-Policy: default-src 'self'; script-src 'self'
使用XSS防护库
npm install xss
import xss from 'xss';

// 使用xss库清理用户输入
function sanitizeUserContent(content) {
const options = {
whiteList: { // 允许的标签和属性
a: ['href', 'title', 'target'],
img: ['src', 'alt'],
p: [],
br: []
},
stripIgnoreTag: true, // 清除不在白名单中的标签
css: false // 禁用CSS
};

return xss(content, options);
}

2. 跨站请求伪造 (CSRF)

攻击原理

CSRF(Cross-Site Request Forgery)是指攻击者诱导用户在已登录的状态下访问恶意网站,该网站会自动向目标网站发送请求,利用用户的身份执行恶意操作。

攻击示例

// 恶意网站代码
<img src="https://bank.com/transfer?to=hacker&amount=10000" style="display:none;">

// 当用户访问这个页面时,浏览器会自动发送请求
// 如果用户已经登录银行网站,这个请求会被执行

防护措施

CSRF Token
// 服务器端生成CSRF Token
function generateCSRFToken() {
return crypto.randomBytes(32).toString('hex');
}

// 在表单中添加Token
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<input type="number" name="amount" placeholder="金额">
<button type="submit">转账</button>
</form>

// 在API请求中添加Token
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCSRFToken()
},
body: JSON.stringify({ amount: 100 })
});
// 设置SameSite属性
app.use(session({
secret: 'your-secret',
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict' // 或 'lax'
}
}));
Referer和Origin验证
// 服务器端验证
function verifyCSRF(req, res, next) {
const csrfToken = req.headers['x-csrf-token'];
const sessionToken = req.session.csrfToken;

if (csrfToken !== sessionToken) {
return res.status(403).json({ error: 'CSRF验证失败' });
}

next();
}
SameSite属性详解
// SameSite选项
const cookieOptions = {
sameSite: 'strict' // 严格模式,完全阻止跨站请求
sameSite: 'lax' // 宽松模式,允许GET请求跨站
sameSite: 'none' // 无限制,但需要同时设置secure
};

// 完整Cookie配置
res.cookie('sessionId', 'value', {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000
});

3. 点击劫持 (Clickjacking)

攻击原理

点击劫持是指攻击者通过透明的iframe覆盖目标页面,诱导用户点击被隐藏的真实按钮。

攻击示例

<!-- 恶意页面 -->
<iframe src="https://bank.com/transfer"
style="position:absolute; top:0; left:0; width:100%; height:100%;
opacity:0.1; z-index:1; border:none;">
</iframe>
<div style="position:absolute; top:50px; left:100px; z-index:2;">
<button style="background: red; color: white; padding: 10px 20px;">
点击领取100元红包
</button>
</div>

防护措施

X-Frame-Options
<!-- HTML中设置 -->
<meta http-equiv="X-Frame-Options" content="DENY">

<!-- HTTP头设置 -->
X-Frame-Options: DENY
Content Security Policy
<meta http-equiv="Content-Security-Policy" 
content="frame-ancestors 'none';">
JavaScript防护
// 检测是否在iframe中
if (window.top !== window.self) {
// 页面被嵌入iframe中
window.top.location.href = window.self.location.href;
}

// 或者设置响应头
res.setHeader('X-Frame-Options', 'DENY');
Frame Busting Code
// 防止页面被嵌入iframe
(function() {
if (window.top !== window.self) {
window.top.location.href = window.location.href;
}
})();

4. 安全的HTTP头配置

常用安全头

// Express.js示例
const helmet = require('helmet');

app.use(helmet());

// 自定义安全头配置
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameSrc: ["'none'"],
frameAncestors: ["'none'"],
connectSrc: ["'self'", "https://api.example.com"],
formAction: ["'self'"],
manifestSrc: ["'self'"],
baseUri: ["'self'"],
workerSrc: ["'self'"],
childSrc: ["'self'"],
upgradeInsecureRequests: []
},
reportOnly: false
}));

// 其他安全头
app.use(helmet.dnsPrefetchControl());
app.use(helmet.expectCt());
app.use(helmet.frameguard({ action: 'deny' }));
app.use(helmet.hidePoweredBy());
app.use(helmet.hsts());
app.use(helmet.ieNoOpen());
app.use(helmet.noSniff());
app.use(helmet.permittedCrossDomainPolicies());
app.use(helmet.referrerPolicy());
app.use(helmet.xssFilter());

安全头配置示例

const securityHeaders = {
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'",
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': 'geolocation=(), microphone=(), camera=()'
};

// 在响应头中设置
Object.entries(securityHeaders).forEach(([key, value]) => {
res.setHeader(key, value);
});

实战安全防护方案

1. 输入验证和输出编码

完整的输入处理流程

import validator from 'validator';
import DOMPurify from 'dompurify';

class InputSanitizer {
constructor() {
this.whitelist = {
tags: ['a', 'p', 'br', 'strong', 'em', 'ul', 'ol', 'li'],
attributes: {
'a': ['href', 'title', 'target'],
'img': ['src', 'alt']
}
};
}

// 验证输入
validate(input, type = 'string') {
if (!input || typeof input !== 'string') {
throw new Error('输入无效');
}

// 根据类型进行不同验证
switch (type) {
case 'email':
if (!validator.isEmail(input)) {
throw new Error('邮箱格式无效');
}
break;
case 'url':
if (!validator.isURL(input)) {
throw new Error('URL格式无效');
}
break;
case 'text':
if (input.length > 1000) {
throw new Error('输入过长');
}
break;
}

return input;
}

// 清理HTML
sanitizeHTML(html) {
return DOMPurify.sanitize(html, this.whitelist);
}

// 转义HTML
escapeHTML(html) {
return html
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}

// 清理用户名
sanitizeUsername(username) {
return validator.escape(username)
.replace(/[^a-zA-Z0-9_]/g, '');
}

// 清理文件名
sanitizeFilename(filename) {
return validator.escape(filename)
.replace(/[<>:"/\\|?*]/g, '')
.replace(/\s+/g, '_');
}
}

// 使用示例
const sanitizer = new InputSanitizer();

function processUserInput(input) {
try {
const cleaned = sanitizer.validate(input, 'text');
const escaped = sanitizer.escapeHTML(cleaned);
return escaped;
} catch (error) {
console.error('输入验证失败:', error);
return '';
}
}

React组件中的安全处理

import React, { useState } from 'react';
import DOMPurify from 'dompurify';

function SafeCommentBox() {
const [comment, setComment] = useState('');
const [error, setError] = useState('');

const handleSubmit = (e) => {
e.preventDefault();

try {
// 验证输入
if (comment.length < 5) {
throw new Error('评论至少需要5个字符');
}

if (comment.length > 500) {
throw new Error('评论不能超过500个字符');
}

// 清理HTML
const sanitizedComment = DOMPurify.sanitize(comment, {
ALLOWED_TAGS: ['p', 'strong', 'em', 'a'],
ALLOWED_ATTR: ['href', 'title', 'target']
});

// 提交评论
submitComment(sanitizedComment);
setComment('');
setError('');
} catch (err) {
setError(err.message);
}
};

return (
<div className="comment-box">
<form onSubmit={handleSubmit}>
<textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="写下你的评论..."
rows={4}
maxLength={500}
/>
<div className="char-count">{comment.length}/500</div>
{error && <div className="error">{error}</div>}
<button type="submit">发布评论</button>
</form>
</div>
);
}

// 使用textContent替代innerHTML
function SafeDisplay({ content }) {
return <div className="content">{content}</div>;
}

// 安全的组件渲染
function renderSafeComponents(components) {
return components.map(component => {
// 避免使用dangerouslySetInnerHTML
return (
<div key={component.id}>
{component.title}
<SafeDisplay content={component.content} />
</div>
);
});
}

2. 安全的API通信

HTTPS和证书验证

// 使用fetch的安全配置
async function secureFetch(url, options = {}) {
// 强制使用HTTPS
if (!url.startsWith('https://')) {
throw new Error('只允许HTTPS请求');
}

// 配置请求选项
const secureOptions = {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
},
mode: 'cors',
credentials: 'same-origin',
redirect: 'follow'
};

try {
const response = await fetch(url, secureOptions);

if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}

return await response.json();
} catch (error) {
console.error('API请求失败:', error);
throw error;
}
}

// 使用axios的安全配置
const secureAxios = require('axios');

const apiClient = secureAxios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
withCredentials: true,
validateStatus: function(status) {
return status >= 200 && status < 300;
}
});

// 请求拦截器
apiClient.interceptors.request.use(config => {
// 添加安全头
config.headers['X-Security-Token'] = getSecurityToken();
return config;
}, error => {
return Promise.reject(error);
});

// 响应拦截器
apiClient.interceptors.response.use(response => {
return response.data;
}, error => {
if (error.response) {
// 处理HTTP错误
switch (error.response.status) {
case 401:
// 重新登录
redirectToLogin();
break;
case 403:
// 无权限
showPermissionError();
break;
case 429:
// 请求过于频繁
showRateLimitError();
break;
}
}
return Promise.reject(error);
});

API认证和授权

// JWT Token管理
class AuthManager {
constructor() {
this.token = localStorage.getItem('auth_token');
this.refreshToken = localStorage.getItem('refresh_token');
}

// 登录
async login(credentials) {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
});

const data = await response.json();

if (response.ok) {
this.setTokens(data.token, data.refreshToken);
return true;
}

throw new Error(data.message);
} catch (error) {
throw error;
}
}

// 设置Token
setTokens(token, refreshToken) {
this.token = token;
this.refreshToken = refreshToken;

// 设置过期时间(假设token有效期1小时)
const expirationTime = Date.now() + 3600000;
localStorage.setItem('auth_token', token);
localStorage.setItem('auth_token_expires', expirationTime);
localStorage.setItem('refresh_token', refreshToken);
}

// 获取授权头
getAuthHeader() {
if (!this.token || this.isTokenExpired()) {
throw new Error('Token已过期');
}

return {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
};
}

// 检查Token是否过期
isTokenExpired() {
const expirationTime = localStorage.getItem('auth_token_expires');
return !expirationTime || Date.now() > parseInt(expirationTime);
}

// 刷新Token
async refreshToken() {
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
refreshToken: this.refreshToken
})
});

const data = await response.json();

if (response.ok) {
this.setTokens(data.token, data.refreshToken);
return true;
}

throw new Error('Token刷新失败');
} catch (error) {
this.logout();
throw error;
}
}

// 登出
logout() {
this.token = null;
this.refreshToken = null;
localStorage.removeItem('auth_token');
localStorage.removeItem('auth_token_expires');
localStorage.removeItem('refresh_token');
}
}

// 使用示例
const authManager = new AuthManager();

// 安全的API调用
async function secureAPICall(url, options = {}) {
try {
// 添加认证头
const authHeader = authManager.getAuthHeader();
const allHeaders = {
...authHeader,
...options.headers
};

// 发起请求
const response = await fetch(url, {
...options,
headers: allHeaders
});

// 如果Token过期,尝试刷新
if (response.status === 401) {
const refreshed = await authManager.refreshToken();
if (refreshed) {
// 重新发起请求
return fetch(url, {
...options,
headers: {
...authManager.getAuthHeader(),
...options.headers
}
});
}
}

return response;
} catch (error) {
console.error('API调用失败:', error);
throw error;
}
}

3. 安全的第三方资源加载

内容安全策略 (CSP)

// 完整的CSP配置
const cspConfig = {
'default-src': ["'self'"],
'script-src': [
"'self'",
"'unsafe-inline'",
"'unsafe-eval'",
"https://cdn.jsdelivr.net",
"https://cdnjs.cloudflare.com"
],
'style-src': [
"'self'",
"'unsafe-inline'",
"https://fonts.googleapis.com"
],
'img-src': [
"'self'",
"data:",
"https:",
"https://picsum.photos"
],
'font-src': [
"'self'",
"https://fonts.gstatic.com"
],
'connect-src': [
"'self'",
"https://api.example.com",
"https://stats.example.com"
],
'frame-src': ["'none'"],
'frame-ancestors': ["'none'"],
'object-src': ["'none'"],
'base-uri': ["'self'"],
'form-action': ["'self'"],
'report-uri': '/csp-violation-report',
'report-to': 'csp-endpoint'
};

// 生成CSP字符串
function generateCSPHeader(config) {
return Object.entries(config)
.map(([directive, sources]) => {
if (Array.isArray(sources)) {
return `${directive} ${sources.join(' ')}`;
}
return `${directive} ${sources}`;
})
.join('; ');
}

// 使用示例
const cspHeader = generateCSPHeader(cspConfig);
res.setHeader('Content-Security-Policy', cspHeader);

Subresource Integrity (SRI)

<!-- 为第三方资源添加SRI -->
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"
integrity="sha384-HjW8j5rEkZuJbD6T/h/h4gLKP+5n5D0I5pCiJPPdXyJt4kKCLTrZlcG/CiKvGs7"
crossorigin="anonymous"></script>

<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
integrity="sha384-GLhlTQ8iRABdZJilZpsuSvgNmjM3WM57PWhB4bMJpaL5F1jAVtHMtDo5+CPGvV"
crossorigin="anonymous">

<!-- 计算SRI哈希 -->
function generateSRI(url) {
return fetch(url)
.then(response => response.text())
.then(text => {
const encoder = new TextEncoder();
const data = encoder.encode(text);
return crypto.subtle.digest('SHA-384', data);
})
.then(hash => {
const base64 = btoa(String.fromCharCode(...new Uint8Array(hash)));
return `sha384-${base64}`;
});
}

// 使用示例
generateSRI('https://cdn.example.com/script.js')
.then(sri => {
console.log('SRI:', sri);
// 添加到script标签
});

第三方库安全检查

// 检查第三方库的版本安全性
const vulnerablePackages = {
'lodash': '<4.17.15',
'moment': '<2.29.1',
'jquery': '<3.5.0'
};

function checkPackageSecurity(packageName, version) {
if (vulnerablePackages[packageName]) {
const vulnerableVersion = vulnerablePackages[packageName];
if (semver.lt(version, vulnerableVersion)) {
console.warn(`⚠️ ${packageName}@${version} 存在安全漏洞,请升级到${vulnerableVersion}以上版本`);
return false;
}
}
return true;
}

// 扫描依赖
function scanDependencies() {
const packageJson = require('./package.json');

Object.entries(packageJson.dependencies).forEach(([name, version]) => {
const isSafe = checkPackageSecurity(name, version);
if (!isSafe) {
console.log(`🔴 ${name}@${version} - 需要升级`);
} else {
console.log(`🟢 ${name}@${version} - 安全`);
}
});
}

4. 安全的用户界面组件

安全的表单组件

import React, { useState, useEffect } from 'react';
import validator from 'validator';

function SecureForm({ onSubmit }) {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
confirmPassword: ''
});

const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);

const validateField = (name, value) => {
const fieldErrors = {};

switch (name) {
case 'username':
if (!value) {
fieldErrors.username = '用户名不能为空';
} else if (!validator.isAlphanumeric(value)) {
fieldErrors.username = '用户名只能包含字母和数字';
} else if (value.length < 3 || value.length > 20) {
fieldErrors.username = '用户名长度必须在3-20个字符之间';
}
break;

case 'email':
if (!value) {
fieldErrors.email = '邮箱不能为空';
} else if (!validator.isEmail(value)) {
fieldErrors.email = '邮箱格式不正确';
}
break;

case 'password':
if (!value) {
fieldErrors.password = '密码不能为空';
} else if (value.length < 8) {
fieldErrors.password = '密码长度至少8个字符';
} else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
fieldErrors.password = '密码必须包含大小写字母和数字';
}
break;

case 'confirmPassword':
if (!value) {
fieldErrors.confirmPassword = '请确认密码';
} else if (value !== formData.password) {
fieldErrors.confirmPassword = '两次输入的密码不一致';
}
break;
}

return fieldErrors;
};

const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));

// 实时验证
const fieldErrors = validateField(name, value);
setErrors(prev => ({
...prev,
[name]: fieldErrors[name] || null
}));
};

const handleSubmit = async (e) => {
e.preventDefault();

// 验证所有字段
const newErrors = {};
Object.keys(formData).forEach(key => {
const fieldErrors = validateField(key, formData[key]);
Object.assign(newErrors, fieldErrors);
});

setErrors(newErrors);

if (Object.keys(newErrors).length === 0) {
setIsSubmitting(true);
try {
await onSubmit(formData);
} catch (error) {
setErrors({ submit: error.message });
} finally {
setIsSubmitting(false);
}
}
};

return (
<form onSubmit={handleSubmit} className="secure-form" noValidate>
<div className="form-group">
<label htmlFor="username">用户名</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
className={errors.username ? 'error' : ''}
maxLength={20}
/>
{errors.username && <span className="error-text">{errors.username}</span>}
</div>

<div className="form-group">
<label htmlFor="email">邮箱</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className={errors.email ? 'error' : ''}
/>
{errors.email && <span className="error-text">{errors.email}</span>}
</div>

<div className="form-group">
<label htmlFor="password">密码</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
className={errors.password ? 'error' : ''}
minLength={8}
/>
{errors.password && <span className="error-text">{errors.password}</span>}
</div>

<div className="form-group">
<label htmlFor="confirmPassword">确认密码</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
className={errors.confirmPassword ? 'error' : ''}
/>
{errors.confirmPassword && <span className="error-text">{errors.confirmPassword}</span>}
</div>

{errors.submit && (
<div className="form-error">{errors.submit}</div>
)}

<button
type="submit"
disabled={isSubmitting}
className="submit-button"
>
{isSubmitting ? '提交中...' : '注册'}
</button>
</form>
);
}

安全的文件上传组件

import React, { useState, useRef } from 'react';
import validator from 'validator';

function SecureFileUpload({ onUpload }) {
const [file, setFile] = useState(null);
const [uploadProgress, setUploadProgress] = useState(0);
const [error, setError] = useState('');
const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef(null);

const allowedTypes = [
'image/jpeg',
'image/png',
'image/gif',
'application/pdf',
'text/plain'
];

const maxSize = 5 * 1024 * 1024; // 5MB

const validateFile = (file) => {
// 检查文件类型
if (!allowedTypes.includes(file.type)) {
throw new Error(`不支持的文件类型: ${file.type}`);
}

// 检查文件大小
if (file.size > maxSize) {
throw new Error(`文件大小不能超过5MB`);
}

// 检查文件名
const filename = file.name;
if (filename.length > 100) {
throw new Error('文件名过长');
}

// 检查文件名中的危险字符
const dangerousChars = /[<>:"/\\|?*]/;
if (dangerousChars.test(filename)) {
throw new Error('文件名包含非法字符');
}

return true;
};

const handleFileSelect = (e) => {
const selectedFile = e.target.files[0];
setError('');

if (!selectedFile) {
setFile(null);
return;
}

try {
validateFile(selectedFile);
setFile(selectedFile);
} catch (err) {
setError(err.message);
setFile(null);
// 清空文件输入
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
};

const handleUpload = async () => {
if (!file) {
setError('请选择文件');
return;
}

setIsUploading(true);
setError('');

try {
const formData = new FormData();
formData.append('file', file);

const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
onUploadProgress: (progressEvent) => {
const progress = Math.round(
(progressEvent.loaded / progressEvent.total) * 100
);
setUploadProgress(progress);
}
});

if (!response.ok) {
throw new Error('文件上传失败');
}

const result = await response.json();
onUpload(result);
setFile(null);
setUploadProgress(0);

// 清空文件输入
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
} catch (err) {
setError(err.message);
} finally {
setIsUploading(false);
}
};

return (
<div className="secure-file-upload">
<div className="upload-area">
<input
type="file"
ref={fileInputRef}
onChange={handleFileSelect}
accept={allowedTypes.join(',')}
disabled={isUploading}
/>

{file ? (
<div className="file-info">
<div className="file-name">{file.name}</div>
<div className="file-size">{(file.size / 1024 / 1024).toFixed(2)} MB</div>
</div>
) : (
<div className="placeholder">
<i className="fas fa-cloud-upload-alt"></i>
<p>点击选择文件或拖拽文件到此处</p>
<p className="hint">支持JPG、PNG、GIF、PDF格式,最大5MB</p>
</div>
)}
</div>

{error && <div className="error-message">{error}</div>}

{isUploading && (
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${uploadProgress}%` }}
></div>
<span className="progress-text">{uploadProgress}%</span>
</div>
)}

<button
onClick={handleUpload}
disabled={!file || isUploading}
className="upload-button"
>
{isUploading ? '上传中...' : '上传'}
</button>
</div>
);
}

安全监控和响应

1. 安全日志记录

// 安全事件日志
class SecurityLogger {
constructor() {
this.logs = [];
}

logSecurityEvent(eventType, details = {}) {
const logEntry = {
timestamp: new Date().toISOString(),
eventType,
details,
userAgent: navigator.userAgent,
url: window.location.href,
userId: getCurrentUserId()
};

this.logs.push(logEntry);

// 发送到服务器
this.sendToServer(logEntry);

// 本地存储(用于调试)
this.saveToLocalStorage(logEntry);
}

async sendToServer(logEntry) {
try {
await fetch('/api/security/log', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Security-Token': getSecurityToken()
},
body: JSON.stringify(logEntry)
});
} catch (error) {
console.error('安全日志发送失败:', error);
}
}

saveToLocalStorage(logEntry) {
try {
const logs = JSON.parse(localStorage.getItem('security_logs') || '[]');
logs.push(logEntry);

// 只保留最近的100条日志
if (logs.length > 100) {
logs.shift();
}

localStorage.setItem('security_logs', JSON.stringify(logs));
} catch (error) {
console.error('本地日志保存失败:', error);
}
}

getLogs() {
return this.logs;
}

// 检测可疑行为
detectSuspiciousActivity() {
const recentLogs = this.logs.slice(-10);
const failedAttempts = recentLogs.filter(log =>
log.eventType === 'login_failed' ||
log.eventType === 'access_denied'
).length;

if (failedAttempts >= 5) {
this.logSecurityEvent('suspicious_activity', {
reason: '多次登录失败',
failedAttempts
});
return true;
}

return false;
}
}

// 使用示例
const securityLogger = new SecurityLogger();

// 记录XSS尝试
function detectXSSAttempt() {
const suspiciousPatterns = [
/<script/i,
/javascript:/i,
/on\w+\s*=/i,
/eval\(/i
];

const bodyText = document.body.innerHTML;

for (const pattern of suspiciousPatterns) {
if (pattern.test(bodyText)) {
securityLogger.logSecurityEvent('xss_attempt', {
pattern: pattern.toString(),
bodyLength: bodyText.length
});

// 清理页面
document.body.textContent = '检测到安全威胁,页面已被清理';
location.reload();

return true;
}
}

return false;
}

// 监控键盘记录器
function detectKeylogger() {
const keyEvents = [];
let lastInputTime = Date.now();

document.addEventListener('keypress', (e) => {
keyEvents.push({
key: e.key,
time: Date.now(),
element: e.target.tagName
});

// 检查快速连续输入
const timeDiff = Date.now() - lastInputTime;
if (timeDiff < 10) {
securityLogger.logSecurityEvent('potential_keylogger', {
timeDiff,
element: e.target.tagName
});
}

lastInputTime = Date.now();

// 限制事件数量
if (keyEvents.length > 1000) {
keyEvents.shift();
}
});
}

2. 安全响应机制

class SecurityResponse {
constructor() {
this.blockedIPs = new Set();
this.userSuspicious = false;
}

// 检测异常请求
detectAnomaly(request) {
const anomalies = [];

// 检查请求频率
const requestCount = this.getRequestCount(request.ip, 1);
if (requestCount > 100) {
anomalies.push({
type: 'high_frequency',
ip: request.ip,
count: requestCount
});
}

// 检查异常User-Agent
const suspiciousUserAgents = [
/bot/i,
/crawler/i,
/spider/i,
/scanner/i
];

if (suspiciousUserAgents.some(pattern => pattern.test(request.userAgent))) {
anomalies.push({
type: 'suspicious_ua',
userAgent: request.userAgent
});
}

// 检查地理位置异常
if (this.isGeoAnomaly(request.ip, request.userLocation)) {
anomalies.push({
type: 'geo_anomaly',
ip: request.ip,
expectedLocation: this.getUserLocation(),
actualLocation: request.userLocation
});
}

return anomalies;
}

// 处理安全事件
async handleSecurityEvent(event) {
switch (event.type) {
case 'xss_attempt':
await this.handleXSSAttempt(event);
break;

case 'csrf_attempt':
await this.handleCSRFAttempt(event);
break;

case 'brute_force':
await this.handleBruteForce(event);
break;

case 'suspicious_activity':
await this.handleSuspiciousActivity(event);
break;
}
}

// 处理XSS尝试
async handleXSSAttempt(event) {
// 记录事件
await this.logSecurityEvent('XSS_ATTEMPT', event);

// 如果是重复攻击,增加防护级别
if (this.getXSSAttemptCount(event.ip) > 5) {
await this.blockIP(event.ip, 'repeated_xss');
}

// 通知管理员
this.notifyAdmin({
type: 'security_alert',
severity: 'high',
message: '检测到XSS攻击尝试',
details: event
});
}

// 处理暴力破解
async handleBruteForce(event) {
const ip = event.ip;
const attempts = this.getBruteForceAttempts(ip);

// 记录失败尝试
this.incrementBruteForceAttempts(ip);

// 检查是否需要封禁
if (attempts > 10) {
await this.blockIP(ip, 'brute_force');
this.notifyAdmin({
type: 'security_alert',
severity: 'critical',
message: '检测到暴力破解攻击',
details: { ip, attempts }
});
}
}

// 阻止IP访问
async blockIP(ip, reason) {
this.blockedIPs.add({ ip, reason, timestamp: Date.now() });

// 设置定时解除封禁
setTimeout(() => {
this.blockedIPs.delete(ip);
}, 24 * 60 * 60 * 1000); // 24小时后解除

// 保存到服务器
await this.saveBlockedIPs();
}

// 检查IP是否被阻止
isIPBlocked(ip) {
const blocked = Array.from(this.blockedIPs).find(
item => item.ip === ip &&
Date.now() - item.timestamp < 24 * 60 * 60 * 1000
);
return !!blocked;
}

// 通知管理员
async notifyAdmin(alert) {
try {
await fetch('/api/admin/security-alert', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(alert)
});
} catch (error) {
console.error('安全通知发送失败:', error);
}
}

// 安全检查中间件
securityMiddleware(req, res, next) {
const ip = req.ip;

// 检查IP是否被阻止
if (this.isIPBlocked(ip)) {
return res.status(403).json({
error: '访问被拒绝',
message: '您的IP地址已被临时封禁'
});
}

// 检查请求异常
const anomaly = this.detectAnomaly({
ip,
userAgent: req.headers['user-agent'],
userLocation: req.headers['x-forwarded-for']
});

if (anomaly.length > 0) {
this.handleSecurityEvent({
type: 'anomaly_detected',
ip,
anomalies: anomaly
});
}

// 为响应添加安全头
res.set({
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block'
});

next();
}
}

安全测试策略

1. 自动化安全测试

// XSS测试用例
const XSS_TESTS = [
'<script>alert("XSS")</script>',
'"><script>alert("XSS")</script>',
'<img src="x" onerror="alert(\'XSS\')">',
'<svg onload="alert(\'XSS\')">',
'javascript:alert("XSS")',
'<a href="javascript:alert(\'XSS\')">点击</a>',
'<iframe src="javascript:alert(\'XSS\')"></iframe>'
];

// CSRF测试用例
const CSRF_TESTS = [
`<form action="/transfer" method="POST">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="1000">
<input type="submit" value="点击转账">
</form>`,
`<img src="https://bank.com/transfer?to=attacker&amount=1000" style="display:none;">`
];

// 安全测试工具类
class SecurityTester {
constructor() {
this.results = [];
}

// 测试XSS防护
async testXSSProtection(inputElement, submitFunction) {
const results = [];

for (const test of XSS_TESTS) {
try {
// 重置输入
inputElement.value = '';

// 注入测试载荷
inputElement.value = test;

// 提交表单
await submitFunction();

// 检查是否执行了脚本
const scripts = document.querySelectorAll('script');
const executed = Array.from(scripts).some(script =>
script.textContent.includes('alert')
);

results.push({
test,
passed: !executed,
executed
});

// 恢复输入
inputElement.value = '';

} catch (error) {
results.push({
test,
passed: false,
error: error.message
});
}
}

this.results.push({
type: 'xss_protection',
tests: results,
summary: {
total: results.length,
passed: results.filter(r => r.passed).length,
failed: results.filter(r => !r.passed).length
}
});

return results;
}

// 测试CSRF防护
async testCSRFProtection(form) {
const results = [];

for (const test of CSRF_TESTS) {
try {
// 创建测试iframe
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);

iframe.contentDocument.write(test);
iframe.contentDocument.close();

// 等待一段时间检查是否发生了请求
await new Promise(resolve => setTimeout(resolve, 2000));

results.push({
test,
passed: true // 假设防护有效
});

document.body.removeChild(iframe);

} catch (error) {
results.push({
test,
passed: false,
error: error.message
});
}
}

this.results.push({
type: 'csrf_protection',
tests: results,
summary: {
total: results.length,
passed: results.filter(r => r.passed).length,
failed: results.filter(r => !r.passed).length
}
});

return results;
}

// 安全配置测试
testSecurityConfig() {
const results = [];

// 检查安全头
const securityHeaders = [
'X-Content-Type-Options',
'X-Frame-Options',
'X-XSS-Protection',
'Content-Security-Policy',
'Strict-Transport-Security'
];

securityHeaders.forEach(header => {
const value = document.defaultView.getComputedStyle(document.documentElement).getPropertyValue(`-x-${header}`);
results.push({
header,
present: !!value,
value
});
});

// 检查Cookie安全属性
const cookies = document.cookie.split(';');
const secureCookies = cookies.filter(cookie =>
cookie.trim().includes('Secure') ||
cookie.trim().includes('HttpOnly')
);

results.push({
type: 'cookie_security',
totalCookies: cookies.length,
secureCookies: secureCookies.length
});

this.results.push({
type: 'security_config',
tests: results
});

return results;
}

// 生成测试报告
generateReport() {
const report = {
timestamp: new Date().toISOString(),
results: this.results,
summary: {
totalTests: this.results.reduce((sum, result) => sum + result.tests.length, 0),
passedTests: this.results.reduce((sum, result) =>
sum + (result.summary ? result.summary.passed : 0), 0
),
failedTests: this.results.reduce((sum, result) =>
sum + (result.summary ? result.summary.failed : 0), 0
)
}
};

// 生成HTML报告
return this.generateHTMLReport(report);
}

// 生成HTML报告
generateHTMLReport(report) {
const html = `
<html>
<head>
<title>安全测试报告</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.passed { color: green; }
.failed { color: red; }
.test-item { margin: 10px 0; padding: 10px; border: 1px solid #ddd; }
.summary { background: #f0f0f0; padding: 15px; margin: 20px 0; }
</style>
</head>
<body>
<h1>安全测试报告</h1>
<p>测试时间: ${new Date(report.timestamp).toLocaleString()}</p>

<div class="summary">
<h2>测试摘要</h2>
<p>总测试数: ${report.summary.totalTests}</p>
<p>通过测试: <span class="passed">${report.summary.passedTests}</span></p>
<p>失败测试: <span class="failed">${report.summary.failedTests}</span></p>
</div>

<div class="details">
${report.results.map(result => `
<div class="test-item">
<h3>${result.type}</h3>
${result.summary ? `
<p>通过: ${result.summary.passed}, 失败: ${result.summary.failed}</p>
` : ''}
</div>
`).join('')}
</div>
</body>
</html>
`;

return html;
}
}

// 使用示例
const securityTester = new SecurityTester();

// 运行安全测试
async function runSecurityTests() {
// 测试XSS防护
const inputElement = document.querySelector('input[name="comment"]');
const submitFunction = () => {
document.querySelector('form').submit();
};
await securityTester.testXSSProtection(inputElement, submitFunction);

// 测试CSRF防护
const form = document.querySelector('form');
await securityTester.testCSRFProtection(form);

// 测试安全配置
securityTester.testSecurityConfig();

// 生成报告
const report = securityTester.generateReport();
console.log('安全测试完成', report);

// 保存报告
const blob = new Blob([report], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'security-report.html';
a.click();
}

总结

前端安全防护是一个系统工程,需要从多个层面进行防护:

核心防护策略

  1. 输入验证:严格验证所有用户输入
  2. 输出编码:对输出内容进行适当的编码
  3. 安全头配置:正确配置HTTP安全头
  4. 访问控制:实施合理的访问控制策略
  5. 安全监控:持续监控安全事件

最佳实践

  • 纵深防御:采用多层防护措施
  • 最小权限:遵循最小权限原则
  • 安全开发生命周期:将安全融入开发过程
  • 定期测试:定期进行安全测试和评估
  • 持续更新:及时更新依赖库和安全补丁

工具推荐

  • 扫描工具:OWASP ZAP、Burp Suite
  • 代码分析:SonarQube、CodeQL
  • 依赖检查:npm audit、Snyk
  • 监控工具:ELK Stack、Splunk

记住,安全是一个持续的过程,而不是一次性的任务。定期评估和更新安全措施,才能有效应对不断变化的安全威胁。


本文档详细介绍了前端安全防护的各个方面,从基础概念到高级防护策略,帮助构建更安全的前端应用。