安全威胁概览
┌──────────────────────────────────────────────────────────────┐
│ OWASP Top 10 (前端相关) │
├──────────────────────────────────────────────────────────────┤
│ │
│ 1. Broken Access Control 访问控制失效 │
│ 2. Cryptographic Failures 加密失败 │
│ 3. Injection 注入 │
│ 4. Insecure Design 不安全设计 │
│ 5. Security Misconfiguration 安全配置错误 │
│ 6. Vulnerable Components 脆弱组件 │
│ 7. Authentication Failures 认证失效 │
│ 8. Integrity Failures 完整性失效 │
│ 9. Logging & Monitoring 日志与监控 │
│ 10. SSRF 服务端请求伪造 │
│ │
└──────────────────────────────────────────────────────────────┘
XSS(跨站脚本)
XSS 原理
┌──────────────────────────────────────────────────────────────┐
│ XSS 攻击原理 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 攻击者 │
│ │ │
│ │ 在评论区提交: │
│ │ <script>stealCookies()</script> │
│ ▼ │
│ ┌─────────┐ │
│ │ 服务器 │ ← 存储型 XSS │
│ └────┬────┘ │
│ │ │
│ │ 其他用户访问页面 │
│ ▼ │
│ ┌─────────┐ │
│ │ 浏览器 │ ← 脚本被执行,窃取敏感信息 │
│ └─────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
XSS 类型
| 类型 |
说明 |
例子 |
| 存储型 |
恶意脚本永久存储在服务器 |
评论区注入脚本 |
| 反射型 |
恶意脚本通过 URL 参数注入 |
?search=<script> |
| DOM 型 |
仅客户端执行,不涉及服务器 |
URL hash 注入 |
XSS 防护
function escapeHtml(str) {
const escapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return str.replace(/[&<>"']/g, char => escapeMap[char]);
}
const userInput = '<script>alert(1)</script>';
return <div>{userInput}</div>;
return <div dangerouslySetInnerHTML={{ __html: userHtml }} />;
import DOMPurify from 'dompurify';
const sanitized = DOMPurify.sanitize(userHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong'],
ALLOWED_ATTR: ['class']
});
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
Vue 防护
<template>
<div>{{ userInput }}</div>
<div v-html="sanitizedHtml"></div>
</template>
CSRF(跨站请求伪造)
CSRF 原理
┌──────────────────────────────────────────────────────────────┐
│ CSRF 攻击原理 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 用户已登录 site-a.com │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ site-a.com 页面 │ │
│ │ <img src="site-b.com/transfer?to=hacker&amt=1000"> │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 浏览器自动携带 cookie,发起请求 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ site-b.com 服务器 │ │
│ │ 认为请求来自用户,执行转账操作 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
CSRF 防护
const csrfToken = generateToken();
session.csrfToken = csrfToken;
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ to: 'hacker', amount: 1000 })
});
function validateCsrfToken(req, res, next) {
const token = req.headers['x-csrf-token'];
if (token !== req.session.csrfToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
next();
}
Set-Cookie: sessionId=abc123; SameSite=Strict; Secure; HttpOnly
fetch('/api/action', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
框架集成
import axios from 'axios';
axios.interceptors.request.use(config => {
const csrfToken = document.querySelector('meta[name=csrf-token]')?.content;
if (csrfToken) {
config.headers['X-CSRF-Token'] = csrfToken;
}
return config;
});
CSP(内容安全策略)
CSP 配置
# Nginx 配置 CSP
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'nonce-random123';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
" always;
CSP 指令
| 指令 |
说明 |
示例 |
| default-src |
默认来源 |
'self' |
| script-src |
JS 来源 |
'self' 'nonce-random' |
| style-src |
CSS 来源 |
'self' 'unsafe-inline' |
| img-src |
图片来源 |
'self' data: https: |
| connect-src |
XHR/Fetch |
'self' https://api.example.com |
| font-src |
字体来源 |
'self' https://fonts.gstatic.com |
| frame-ancestors |
嵌入来源 |
'none' |
| base-uri |
来源 |
'self' |
| form-action |
表单提交 |
'self' |
nonce 策略
const nonce = crypto.randomBytes(16).toString('base64');
<script nonce="random123">
console.log('Script with nonce');
</script>
<script
nonce="random123"
dangerouslySetInnerHTML={{ __html: 'console.log("inline")' }}
/>
CORS(跨域资源共享)
CORS 原理
┌──────────────────────────────────────────────────────────────┐
│ CORS 原理 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 简单请求 (GET/POST, 标准头部) │
│ ┌────────┐ ┌────────┐ │
│ │ 浏览器 │ ──────→ │ 服务器 │ │
│ └────────┘ GET └────────┘ │
│ │ │ │
│ │ │ Access-Control-Allow-Origin │
│ │←─────────────────│ │
│ │ 响应 │ │
│ │
│ 预检请求 (PUT/DELETE, 自定义头) │
│ ┌────────┐ ┌────────┐ │
│ │ 浏览器 │ ──OPTIONS──→ │ 服务器 │ │
│ └────────┘ └────────┘ │
│ │ │ │
│ │ Access-Control-Allow-Origin │
│ │←─────────────────│ │
│ │ 预检响应 │ │
│ │ │ │
│ │ ────────────────→│ 实际请求 │
│ │ │ │
│ │←──────────────────│ 响应 │
│ │
└──────────────────────────────────────────────────────────────┘
服务器端配置
const cors = require('cors');
const corsOptions = {
origin: 'https://example.com',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
exposedHeaders: ['X-Total-Count'],
maxAge: 86400
};
app.use(cors(corsOptions));
前端配置
fetch('https://api.example.com/data', {
credentials: 'include'
});
axios.create({
withCredentials: true
});
输入验证与过滤
客户端验证
function isValidEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
}
function isValidUrl(url) {
try {
const parsed = new URL(url);
return ['http:', 'https:'].includes(parsed.protocol);
} catch {
return false;
}
}
function sanitizeInput(input) {
return input
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function validateLength(input, min, max) {
return input.length >= min && input.length <= max;
}
服务端验证(必须)
const { body, validationResult } = require('express-validator');
const registerValidation = [
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Invalid email'),
body('password')
.isLength({ min: 8 })
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.withMessage('Password must meet requirements'),
body('username')
.trim()
.isLength({ min: 3, max: 20 })
.matches(/^[a-zA-Z0-9_]+$/)
.withMessage('Username must be alphanumeric')
];
app.post('/register', registerValidation, (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
});
认证与授权
JWT 安全
const jwtSecret = process.env.JWT_SECRET;
const accessToken = jwt.sign(
{ userId: user.id, role: user.role },
jwtSecret,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId: user.id },
jwtSecret,
{ expiresIn: '7d' }
);
const tokenBlacklist = new Set();
app.post('/logout', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
if (token) {
tokenBlacklist.add(token);
}
res.json({ message: 'Logged out' });
});
function verifyToken(token) {
if (tokenBlacklist.has(token)) {
throw new Error('Token revoked');
}
return jwt.verify(token, jwtSecret);
}
OAuth 2.0 安全
const state = crypto.randomBytes(16).toString('hex');
session.oauthState = state;
app.get('/callback', (req, res) => {
const { code, state } = req.query;
if (state !== session.oauthState) {
return res.status(400).send('State mismatch');
}
});
const codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
HTTPS 与安全 Headers
安全响应头
# Nginx 配置安全头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
安全头说明
| Header |
作用 |
| X-Frame-Options |
防止点击劫持 |
| X-Content-Type-Options |
防止 MIME 类型嗅探 |
| X-XSS-Protection |
XSS 过滤器(现代浏览器已内置) |
| Referrer-Policy |
控制 Referer 头 |
| Permissions-Policy |
控制浏览器功能权限 |
| Strict-Transport-Security |
强制 HTTPS |
WebAuthn(无密码认证)
async function register() {
const credential = await navigator.credentials.create({
publicKey: {
challenge: new Uint8Array([...randomBytes]),
rp: {
name: 'My App',
id: 'example.com'
},
user: {
id: new Uint8Array([...userIdBytes]),
name: '[email protected]',
displayName: 'User Name'
},
pubKeyCredParams: [
{ alg: -7, type: 'public-key' },
{ alg: -257, type: 'public-key' }
],
authenticatorSelection: {
authenticatorAttachment: 'platform',
userVerification: 'required'
}
}
});
return credential;
}
async function authenticate() {
const assertion = await navigator.credentials.get({
publicKey: {
challenge: new Uint8Array([...randomBytes]),
allowCredentials: [{
id: storedCredentialId,
type: 'public-key'
}],
userVerification: 'required'
}
});
return assertion;
}
依赖安全
npm audit
npm audit fix
npx snyk test
依赖安全策略
{
"scripts": {
"security:audit": "npm audit --audit-level=high",
"security:check": "npx snyk test"
}
}
这一章想说的
前端安全是必须重视的领域:
- XSS:永远不要信任用户输入,使用转义或白名单
- CSRF:使用 Token、SameSite Cookie
- CSP:内容安全策略限制脚本执行
- CORS:正确配置跨域请求
- 输入验证:前后端双重验证
- 安全 Headers:配置完善的安全响应头
安全不是事后补救,而是设计时就要考虑。
延展阅读