本文将系统讲解前端安全的核心知识,包括XSS、CSRF、点击劫持等常见攻击方式及防御策略,帮助你构建安全的Web应用。
📋 目录
一、前端安全概述
1.1 常见安全威胁
前端安全威胁
├── XSS(跨站脚本攻击)
│ ├── 反射型XSS
│ ├── 存储型XSS
│ └── DOM型XSS
├── CSRF(跨站请求伪造)
├── 点击劫持(Clickjacking)
├── 中间人攻击(MITM)
├── SQL注入(前端参数)
└── 敏感信息泄露
1.2 安全防护原则
| 原则 | 说明 | 实践 |
|---|---|---|
| 最小权限 | 只授予必要权限 | 限制API权限范围 |
| 纵深防御 | 多层防护 | 前后端都要验证 |
| 默认安全 | 默认拒绝 | 白名单优于黑名单 |
| 输入验证 | 不信任任何输入 | 严格校验用户输入 |
| 输出编码 | 正确编码输出 | 防止XSS注入 |
二、XSS跨站脚本攻击
2.1 XSS攻击类型
反射型XSS
javascript
// 攻击URL
// https://example.com/search?q=<script>alert('XSS')</script>
// ❌ 危险代码:直接将URL参数插入页面
const query = new URLSearchParams(location.search).get('q');
document.getElementById('result').innerHTML = `搜索结果: ${query}`;
// 恶意脚本被执行!
存储型XSS
javascript
// 攻击者提交恶意评论
const comment = '<img src=x onerror="steal(document.cookie)">';
// ❌ 危险代码:直接渲染用户内容
function renderComments(comments) {
return comments.map(c => `<div class="comment">${c.content}</div>`).join('');
}
// 所有访问者都会执行恶意脚本!
DOM型XSS
javascript
// ❌ 危险代码:使用eval执行用户输入
const userInput = location.hash.slice(1);
eval(userInput); // 极度危险!
// ❌ 危险代码:innerHTML插入用户内容
element.innerHTML = userInput;
2.2 XSS防御策略
✅ 输入过滤与输出编码
javascript
// HTML实体编码
function escapeHtml(str) {
const escapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
};
return str.replace(/[&<>"'/]/g, char => escapeMap[char]);
}
// 安全渲染
function renderComment(comment) {
const safeContent = escapeHtml(comment.content);
return `<div class="comment">${safeContent}</div>`;
}
// URL参数编码
const safeQuery = encodeURIComponent(userInput);
✅ 使用安全的DOM API
javascript
// ❌ 危险
element.innerHTML = userContent;
// ✅ 安全:使用textContent
element.textContent = userContent;
// ✅ 安全:使用DOM API创建元素
const div = document.createElement('div');
div.textContent = userContent;
parent.appendChild(div);
✅ Vue/React自动转义
vue
<!-- Vue自动转义 -->
<template>
<!-- ✅ 安全:自动转义 -->
<div>{{ userContent }}</div>
<!-- ❌ 危险:v-html不转义 -->
<div v-html="userContent"></div>
</template>
jsx
// React自动转义
function Comment({ content }) {
// ✅ 安全:JSX自动转义
return <div>{content}</div>;
// ❌ 危险:dangerouslySetInnerHTML
// return <div dangerouslySetInnerHTML={{ __html: content }} />;
}
✅ 使用DOMPurify净化HTML
javascript
import DOMPurify from 'dompurify';
// 当必须渲染HTML时,使用DOMPurify净化
const dirtyHtml = '<img src=x onerror=alert(1)><b>Hello</b>';
const cleanHtml = DOMPurify.sanitize(dirtyHtml);
// 结果: <b>Hello</b>
// 配置允许的标签
const cleanHtml = DOMPurify.sanitize(dirtyHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href', 'title']
});
2.3 CSP内容安全策略
html
<!-- HTTP响应头 -->
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-abc123';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
<!-- 或使用meta标签 -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'">
javascript
// Nginx配置CSP
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'nonce-$request_id';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
" always;
三、CSRF跨站请求伪造
3.1 CSRF攻击原理
html
<!-- 攻击者网站 evil.com -->
<html>
<body>
<!-- 图片自动发起GET请求 -->
<img src="https://bank.com/transfer?to=hacker&amount=10000" />
<!-- 隐藏表单自动提交POST请求 -->
<form action="https://bank.com/transfer" method="POST" id="csrf-form">
<input type="hidden" name="to" value="hacker" />
<input type="hidden" name="amount" value="10000" />
</form>
<script>document.getElementById('csrf-form').submit();</script>
</body>
</html>
<!-- 用户访问evil.com时,如果已登录bank.com -->
<!-- 浏览器会自动携带bank.com的Cookie,请求成功! -->
3.2 CSRF防御策略
✅ CSRF Token
javascript
// 后端生成Token
app.get('/api/csrf-token', (req, res) => {
const token = crypto.randomBytes(32).toString('hex');
req.session.csrfToken = token;
res.json({ csrfToken: token });
});
// 前端获取并携带Token
async function fetchCsrfToken() {
const res = await fetch('/api/csrf-token');
const { csrfToken } = await res.json();
return csrfToken;
}
// 请求时携带Token
async function transferMoney(to, amount) {
const csrfToken = await fetchCsrfToken();
await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken // 自定义请求头
},
body: JSON.stringify({ to, amount })
});
}
✅ SameSite Cookie
javascript
// 后端设置Cookie
res.cookie('sessionId', sessionId, {
httpOnly: true, // 禁止JS访问
secure: true, // 仅HTTPS传输
sameSite: 'Strict', // 严格模式:跨站请求不携带
// sameSite: 'Lax', // 宽松模式:导航请求携带
maxAge: 3600000
});
SameSite属性对比:
┌─────────────┬──────────┬──────────┬──────────┐
│ 请求类型 │ Strict │ Lax │ None │
├─────────────┼──────────┼──────────┼──────────┤
│ 同站请求 │ ✅ 发送 │ ✅ 发送 │ ✅ 发送 │
│ 跨站链接 │ ❌ 不发送 │ ✅ 发送 │ ✅ 发送 │
│ 跨站表单GET │ ❌ 不发送 │ ✅ 发送 │ ✅ 发送 │
│ 跨站表单POST │ ❌ 不发送 │ ❌ 不发送 │ ✅ 发送 │
│ 跨站Ajax │ ❌ 不发送 │ ❌ 不发送 │ ✅ 发送 │
└─────────────┴──────────┴──────────┴──────────┘
✅ 验证Referer/Origin
javascript
// 后端验证请求来源
app.use((req, res, next) => {
const origin = req.get('Origin') || req.get('Referer');
const allowedOrigins = ['https://example.com', 'https://www.example.com'];
if (req.method !== 'GET') {
if (!origin || !allowedOrigins.some(o => origin.startsWith(o))) {
return res.status(403).json({ error: 'Invalid origin' });
}
}
next();
});
✅ 双重Cookie验证
javascript
// 前端:将Cookie值放入请求头
function getCSRFToken() {
return document.cookie
.split('; ')
.find(row => row.startsWith('csrf_token='))
?.split('=')[1];
}
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': getCSRFToken()
},
body: JSON.stringify(data)
});
// 后端:验证Cookie和Header中的Token是否一致
app.use((req, res, next) => {
if (req.method !== 'GET') {
const cookieToken = req.cookies.csrf_token;
const headerToken = req.get('X-CSRF-Token');
if (!cookieToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF validation failed' });
}
}
next();
});
四、点击劫持防御
4.1 点击劫持原理
html
<!-- 攻击者页面 -->
<style>
iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0; /* 透明iframe */
z-index: 2;
}
.fake-button {
position: absolute;
top: 100px;
left: 100px;
z-index: 1;
}
</style>
<!-- 用户看到的是"领取奖品"按钮 -->
<button class="fake-button">点击领取奖品</button>
<!-- 实际点击的是透明iframe中的"转账"按钮 -->
<iframe src="https://bank.com/transfer"></iframe>
4.2 防御策略
✅ X-Frame-Options响应头
javascript
// Nginx配置
add_header X-Frame-Options "DENY" always;
// 或
add_header X-Frame-Options "SAMEORIGIN" always;
// Express配置
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY');
next();
});
✅ CSP frame-ancestors
javascript
// 更现代的方式
add_header Content-Security-Policy "frame-ancestors 'none'" always;
// 或允许同源
add_header Content-Security-Policy "frame-ancestors 'self'" always;
✅ JavaScript防御
javascript
// 检测是否被iframe嵌入
if (window.top !== window.self) {
// 方式1:跳出iframe
window.top.location = window.self.location;
// 方式2:隐藏页面内容
document.body.style.display = 'none';
}
// 更安全的方式:使用sandbox
// 攻击者可能使用sandbox="allow-scripts"阻止跳转
if (window.top !== window.self) {
try {
// 尝试访问top,如果被sandbox限制会抛错
window.top.location.href;
} catch (e) {
document.body.innerHTML = '请在正常环境下访问';
}
}
五、其他安全威胁
5.1 敏感信息泄露
❌ 危险做法
javascript
// 前端存储敏感信息
localStorage.setItem('password', '123456'); // ❌
localStorage.setItem('token', 'jwt-token'); // ⚠️ 有风险
// 控制台打印敏感信息
console.log('用户密码:', password); // ❌
// URL中携带敏感信息
window.location.href = `/reset?token=${resetToken}`; // ⚠️
✅ 安全做法
javascript
// 使用HttpOnly Cookie存储Token
// 后端设置
res.cookie('token', jwtToken, {
httpOnly: true,
secure: true,
sameSite: 'Strict'
});
// 敏感操作使用POST
fetch('/api/reset-password', {
method: 'POST',
body: JSON.stringify({ token: resetToken })
});
// 生产环境移除console
// webpack配置
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true
}
}
})
]
}
5.2 第三方依赖安全
bash
# 检查依赖漏洞
npm audit
yarn audit
# 自动修复
npm audit fix
# 使用Snyk检查
npx snyk test
javascript
// package.json 锁定版本
{
"dependencies": {
"lodash": "4.17.21", // 精确版本
"axios": "^1.0.0" // 允许小版本更新
}
}
// 使用package-lock.json或yarn.lock锁定依赖树
5.3 HTTPS与传输安全
javascript
// 强制HTTPS
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
location.replace(`https:${location.href.substring(location.protocol.length)}`);
}
// HSTS响应头(后端配置)
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
5.4 安全的密码处理
javascript
// ❌ 明文传输密码
fetch('/api/login', {
body: JSON.stringify({ password: '123456' })
});
// ✅ 前端哈希(注意:后端仍需再次哈希)
async function hashPassword(password) {
const encoder = new TextEncoder();
const data = encoder.encode(password);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
// 使用
const hashedPassword = await hashPassword(password);
fetch('/api/login', {
body: JSON.stringify({ password: hashedPassword })
});
六、安全最佳实践
6.1 安全检查清单
markdown
## 前端安全检查清单
### XSS防护
- [ ] 用户输入进行转义/编码
- [ ] 使用textContent代替innerHTML
- [ ] 配置CSP策略
- [ ] 使用DOMPurify净化HTML
- [ ] 避免使用eval、new Function
### CSRF防护
- [ ] 实现CSRF Token机制
- [ ] 设置SameSite Cookie
- [ ] 验证Referer/Origin
- [ ] 敏感操作使用POST
### 点击劫持防护
- [ ] 设置X-Frame-Options
- [ ] 配置CSP frame-ancestors
- [ ] JavaScript检测iframe嵌入
### 传输安全
- [ ] 强制使用HTTPS
- [ ] 配置HSTS
- [ ] Cookie设置Secure标志
### 敏感信息保护
- [ ] 不在前端存储敏感信息
- [ ] 生产环境移除console
- [ ] 敏感数据不放URL参数
### 依赖安全
- [ ] 定期运行npm audit
- [ ] 锁定依赖版本
- [ ] 使用可信的CDN
6.2 安全响应头配置
nginx
# Nginx安全响应头配置
server {
# XSS防护
add_header X-XSS-Protection "1; mode=block" always;
# 禁止MIME类型嗅探
add_header X-Content-Type-Options "nosniff" always;
# 点击劫持防护
add_header X-Frame-Options "SAMEORIGIN" always;
# CSP策略
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
frame-ancestors 'self';
" always;
# HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# 引用策略
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# 权限策略
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
}
6.3 Vue安全配置
javascript
// Vue安全最佳实践
const app = createApp(App);
// 1. 避免使用v-html,如必须使用则净化
import DOMPurify from 'dompurify';
app.directive('safe-html', {
mounted(el, binding) {
el.innerHTML = DOMPurify.sanitize(binding.value);
},
updated(el, binding) {
el.innerHTML = DOMPurify.sanitize(binding.value);
}
});
// 2. 配置axios拦截器
axios.interceptors.request.use(config => {
// 添加CSRF Token
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
if (csrfToken) {
config.headers['X-CSRF-Token'] = csrfToken;
}
return config;
});
// 3. 路由守卫验证
router.beforeEach((to, from, next) => {
// 验证敏感路由
if (to.meta.requiresAuth && !isAuthenticated()) {
next('/login');
} else {
next();
}
});
6.4 React安全配置
jsx
// React安全最佳实践
// 1. 避免dangerouslySetInnerHTML
function SafeHtml({ html }) {
const cleanHtml = DOMPurify.sanitize(html);
return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;
}
// 2. URL验证
function SafeLink({ href, children }) {
const isSafe = href.startsWith('/') ||
href.startsWith('https://') ||
href.startsWith('mailto:');
if (!isSafe) {
console.warn('Potentially unsafe URL:', href);
return <span>{children}</span>;
}
return <a href={href}>{children}</a>;
}
// 3. 表单验证
function LoginForm() {
const [email, setEmail] = useState('');
const validateEmail = (value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value);
};
const handleSubmit = (e) => {
e.preventDefault();
if (!validateEmail(email)) {
alert('请输入有效的邮箱');
return;
}
// 提交表单
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
pattern="[^\s@]+@[^\s@]+\.[^\s@]+"
/>
</form>
);
}
📊 安全威胁与防御对照表
| 威胁类型 | 攻击方式 | 防御措施 |
|---|---|---|
| XSS | 注入恶意脚本 | 输出编码、CSP、DOMPurify |
| CSRF | 伪造用户请求 | Token、SameSite、验证Origin |
| 点击劫持 | 透明iframe覆盖 | X-Frame-Options、CSP |
| 中间人攻击 | 窃听/篡改数据 | HTTPS、HSTS |
| 敏感信息泄露 | 不当存储/传输 | HttpOnly Cookie、加密 |
| 依赖漏洞 | 利用第三方漏洞 | npm audit、版本锁定 |
💡 总结
前端安全防护的核心要点:
- ✅ 输入验证:永远不信任用户输入
- ✅ 输出编码:正确编码防止XSS
- ✅ CSRF防护:Token + SameSite Cookie
- ✅ 传输安全:强制HTTPS + HSTS
- ✅ 最小权限:只授予必要的权限
- ✅ 纵深防御:多层防护,前后端配合
安全是一个持续的过程,需要在开发的每个阶段都保持警惕!
💬 如果这篇文章对你有帮助,欢迎点赞收藏!有问题欢迎在评论区讨论~