Web 安全:跨域、XSS 攻击与 CSRF 攻击
面试必备知识点 + 实战示例代码
目录
- [一、跨域 (CORS)](#一、跨域 (CORS) "#%E4%B8%80%E8%B7%A8%E5%9F%9F-cors")
- [二、XSS 攻击](#二、XSS 攻击 "#%E4%BA%8Cxss-%E6%94%BB%E5%87%BB")
- [三、CSRF 攻击](#三、CSRF 攻击 "#%E4%B8%89csrf-%E6%94%BB%E5%87%BB")
- 四、安全最佳实践
一、跨域 (CORS)
1.1 什么是跨域?
同源策略 是浏览器的一项核心安全机制,限制一个源的文档或脚本如何能与另一个源的资源进行交互。
同源判断: 三个条件全部相同才算同源
makefile
http://www.example.com:80/path
协议: http
域名: www.example.com
端口: 80
跨域示例:
| 当前页面 | 目标URL | 是否跨域 | 原因 |
|---|---|---|---|
http://a.com |
http://a.com/api |
否 | 同源 |
http://a.com |
https://a.com/api |
是 | 协议不同 |
http://a.com |
http://b.com/api |
是 | 域名不同 |
http://a.com:80 |
http://a.com:8080 |
是 | 端口不同 |
http://a.com |
http://sub.a.com |
是 | 子域名不同 |
1.2 跨域解决方案
方案一:CORS (跨域资源共享) - 推荐
服务端设置响应头:
javascript
// Node.js / Express
app.use((req, res, next) => {
// 允许所有源 (生产环境慎用)
res.header('Access-Control-Allow-Origin', '*')
// 允许指定源
// res.header('Access-Control-Allow-Origin', 'https://example.com')
// 允许的请求方法
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
// 允许的请求头
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
// 允许携带凭证
res.header('Access-Control-Allow-Credentials', 'true')
// 预检请求缓存时间 (秒)
res.header('Access-Control-Max-Age', '86400')
// 处理 OPTIONS 预检请求
if (req.method === 'OPTIONS') {
return res.sendStatus(200)
}
next()
})
简单请求 vs 预检请求:
diff
简单请求条件:
- 方法:GET、HEAD、POST
- 头部:Accept、Accept-Language、Content-Language、Content-Type
- Content-Type: application/x-www-form-urlencoded、multipart/form-data、text/plain
预检请求 (OPTIONS):
- 不满足简单请求条件时自动触发
- 先发 OPTIONS 请求询问是否允许
- 允许后才发送实际请求
方案二:JSONP (已过时)
html
<script>
// 定义回调函数
function handleResponse(data) {
console.log('收到数据:', data)
}
</script>
<!-- 通过 script 标签跨域请求 -->
<script src="http://api.example.com/data?callback=handleResponse"></script>
javascript
// 服务端返回
handleResponse({ name: "张三", age: 25 })
缺点: 只支持 GET 请求,存在安全风险,已不推荐使用。
方案三:代理服务器
javascript
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://backend-api.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}
// nginx 配置
location /api {
proxy_pass http://backend-api.com;
proxy_set_header Host $host;
}
方案四:WebSocket (不受同源策略限制)
javascript
const ws = new WebSocket('ws://another-domain.com/chat')
ws.onopen = () => console.log('连接已建立')
ws.onmessage = (event) => console.log('收到消息:', event.data)
二、XSS 攻击
2.1 什么是 XSS 攻击?
跨站脚本攻击 (Cross-Site Scripting) 是攻击者向网页注入恶意脚本,当用户访问时执行。
2.2 XSS 三种类型
类型一:反射型 XSS (Reflected XSS)
恶意脚本通过 URL 参数反射给用户。
xml
攻击链接:
http://example.com/search?q=<script>alert(document.cookie)</script>
页面直接渲染参数:
<div>搜索结果: <%= q %></div>
类型二:存储型 XSS (Stored XSS)
恶意脚本被存储到服务器,所有访问该页面的用户都会执行。
javascript
// 危险的评论功能
comment = req.body.comment
db.save(comment) // 直接存储,没有转义
// 渲染时直接输出
<div class="comment">${comment}</div>
类型三:DOM 型 XSS (DOM-based XSS)
恶意代码通过 DOM 操作修改页面执行。
javascript
// 危险代码
const hash = location.hash.substring(1)
document.getElementById('output').innerHTML = hash
// 攻击 URL:
// http://example.com/#<img src=x onerror=alert(1)>
2.3 XSS 攻击危害
- 窃取用户 Cookie (Session 劫持)
- 篡改网页内容
- 重定向到钓鱼网站
- 监听用户键盘操作
- 执行恶意操作
2.4 XSS 防御措施
措施一:输出转义 - 最重要
javascript
// HTML 转义函数
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'")
}
// 使用
div.innerHTML = escapeHtml(userInput)
措施二:使用 CSP (Content Security Policy)
html
<!-- 通过 HTTP 头或 meta 标签设置 -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' 'unsafe-inline' https://trusted.cdn.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' api.example.com;
">
nginx
# nginx 配置
add_header Content-Security-Policy "default-src 'self'; script-src 'self'";
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "DENY";
add_header X-XSS-Protection "1; mode=block";
措施三:HttpOnly Cookie
什么是 HttpOnly?
HttpOnly 是 Cookie 的一个属性,设置为 true 后,JavaScript 无法通过 document.cookie 读取该 Cookie,从而有效防止 XSS 攻击窃取 Session ID。
javascript
// 服务端设置 Cookie 时添加 HttpOnly
res.cookie('sessionId', sessionId, {
httpOnly: true, // JavaScript 无法读取
secure: true, // 仅 HTTPS 传输
sameSite: 'strict' // 防止 CSRF
})
HttpOnly 作用演示:
javascript
// 服务端设置
res.cookie('sessionId', 'abc123', { httpOnly: true })
res.cookie('userInfo', JSON.stringify({ name: 'Alice' })) // 没有 httpOnly
// 前端 JavaScript
console.log(document.cookie)
// 输出: userInfo={"name":"Alice"}
// sessionId 不会显示!
Cookie 属性完整说明:
| 属性 | 作用 | 推荐值 |
|---|---|---|
httpOnly |
防止 JS 读取 Cookie | true (敏感 Cookie) |
secure |
仅通过 HTTPS 传输 | true (生产环境) |
sameSite |
防止 CSRF 攻击 | 'strict' 或 'lax' |
domain |
指定 Cookie 有效域 | 不设置或当前域 |
path |
指定 Cookie 有效路径 | / |
maxAge |
Cookie 存活时间(毫秒) | 根据业务设置 |
expires |
Cookie 过期日期 | - |
不同后端框架设置示例:
javascript
// Express / Node.js
res.cookie('sessionId', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000
})
// koa
ctx.cookies.set('sessionId', token, {
httpOnly: true,
secure: true,
sameSite: 'strict'
})
// PHP
setcookie('sessionId', $token, [
'expires' => time() + 86400,
'path' => '/',
'domain' => 'example.com',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
])
// Java (Spring Boot)
ResponseCookie cookie = ResponseCookie.from("sessionId", token)
.httpOnly(true)
.secure(true)
.sameSite("Strict")
.maxAge(24 * 60 * 60)
.build();
设置响应头方式:
nginx
# nginx 设置 HttpOnly Cookie
add_header Set-Cookie "sessionId=abc123; HttpOnly; Secure; SameSite=Strict";
javascript
// 原生 HTTP 响应头
res.setHeader('Set-Cookie', [
'sessionId=abc123; HttpOnly; Secure; SameSite=Strict; Max-Age=86400; Path=/'
])
措施四:避免使用 innerHTML
javascript
// 危险
element.innerHTML = userInput
// 安全
element.textContent = userInput
element.innerText = userInput
// 动态创建元素
const div = document.createElement('div')
div.textContent = userInput
element.appendChild(div)
措施五:输入验证
javascript
// 白名单验证
function sanitizeInput(input) {
// 只允许字母、数字、空格
return input.replace(/[^a-zA-Z0-9\s]/g, '')
}
三、CSRF 攻击
3.1 什么是 CSRF 攻击?
跨站请求伪造 (Cross-Site Request Forgery) 是攻击者诱导用户在已登录的状态下,向目标网站发送非本意的请求。
3.2 CSRF 攻击原理
xml
用户已登录 bank.com (持有有效 Cookie)
1. 用户访问恶意网站 evil.com
2. evil.com 页面包含:
<form action="https://bank.com/transfer" method="POST">
<input name="to" value="attacker">
<input name="amount" value="10000">
</form>
<script>document.forms[0].submit()</script>
3. 浏览器自动携带 bank.com 的 Cookie
4. 银行服务器认为是用户本人操作
5. 转账成功!
3.3 XSS vs CSRF 对比
| 特性 | XSS | CSRF |
|---|---|---|
| 攻击者 | 在目标网站注入脚本 | 伪造用户请求 |
| 执行位置 | 用户浏览器 | 用户浏览器 |
| 需要 XSS | 是 | 否 |
| 需要 Cookie 获取 | 是 | 否 (自动携带) |
| 防御重点 | 输入转义、CSP | Token 验证 |
3.4 CSRF 防御措施
措施一:CSRF Token - 最有效
javascript
// 服务端生成 Token
const crypto = require('crypto')
function generateToken() {
return crypto.randomBytes(32).toString('hex')
}
// 存储到 Session
req.session.csrfToken = generateToken()
// 渲染表单时注入
<form method="POST" action="/transfer">
<input type="hidden" name="csrf_token" value="<%= csrfToken %>">
<input name="to" value="">
<input name="amount" value="">
</form>
// 验证 Token
app.post('/transfer', (req, res) => {
if (req.body.csrf_token !== req.session.csrfToken) {
return res.status(403).send('CSRF Token 验证失败')
}
// 处理转账...
})
措施二:SameSite Cookie 属性
javascript
// Strict: 完全禁止第三方 Cookie
res.cookie('sessionId', sessionId, {
sameSite: 'strict'
})
// Lax: 允许部分 GET 请求
res.cookie('sessionId', sessionId, {
sameSite: 'lax'
})
vbnet
SameSite 属性说明:
Strict: 最严格,任何跨站请求都不发送 Cookie
Lax: 允许顶级导航 GET 请求发送 Cookie(默认)
None: 必须配合 Secure,允许跨站发送
措施三:验证 Referer / Origin
javascript
app.post('/transfer', (req, res) => {
const origin = req.headers.origin
const referer = req.headers.referer
const allowedOrigins = ['https://bank.com', 'https://www.bank.com']
if (!allowedOrigins.includes(origin) && !referer?.startsWith('https://bank.com')) {
return res.status(403).send('非法请求来源')
}
// 处理业务...
})
措施四:双重 Cookie 验证
javascript
// 1. 设置 CSRF Cookie
res.cookie('csrfToken', generateToken(), {
httpOnly: false, // 需要能被 JS 读取
sameSite: 'strict'
})
// 2. 前端请求时从 Cookie 读取并放入 Header
// axios 拦截器
axios.interceptors.request.use(config => {
const csrfToken = getCookie('csrfToken')
config.headers['X-CSRF-Token'] = csrfToken
return config
})
// 3. 服务端验证
app.use((req, res, next) => {
const cookieToken = req.cookies.csrfToken
const headerToken = req.headers['x-csrf-token']
if (cookieToken && cookieToken !== headerToken) {
return res.status(403).send('CSRF 验证失败')
}
next()
})
措施五:重要操作二次确认
javascript
// 转账前要求输入密码或验证码
app.post('/transfer', (req, res) => {
const { password, amount, to } = req.body
// 验证支付密码
if (!verifyPaymentPassword(req.user.id, password)) {
return res.status(400).send('支付密码错误')
}
// 执行转账
executeTransfer(req.user.id, to, amount)
})
四、安全最佳实践
4.1 安全检查清单
- 所有用户输入都经过转义/验证
- 敏感 Cookie 设置 HttpOnly、Secure、SameSite
- 实施 CSP 策略
- 重要操作使用 CSRF Token
- 验证请求的 Referer/Origin
- HTTPS 强制跳转
- 实施速率限制防止暴力攻击
- 敏感操作二次确认
- 定期安全审计和依赖更新
4.2 安全 HTTP 响应头
javascript
// Express 中间件示例
app.use(helmet()) // npm install helmet
// 或手动设置
app.use((req, res, next) => {
// 防止点击劫持
res.setHeader('X-Frame-Options', 'DENY')
// 防止 MIME 类型嗅探
res.setHeader('X-Content-Type-Options', 'nosniff')
// 启用浏览器 XSS 过滤
res.setHeader('X-XSS-Protection', '1; mode=block')
// 限制引用来源
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin')
// 权限策略
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=()')
next()
})
4.3 面试常问问题
Q: 跨域是安全问题吗?
A: 跨域不是安全问题,它是浏览器的同源策略保护。CORS 是用来安全地放松这个限制的机制。
Q: XSS 和 CSRF 的区别?
A: XSS 是注入恶意代码,在用户浏览器执行;CSRF 是利用用户身份伪造请求。XSS 可以获取 Cookie,CSRF 自动携带 Cookie。
Q: 为什么 Token 可以防御 CSRF?
A: 攻击者无法获取用户页面中的 CSRF Token(同源策略保护),所以无法构造合法的请求。
Q: Cookie 的 SameSite 属性?
A: Strict 完全禁止跨站 Cookie,Lax 允许部分 GET 请求,None 必须配合 Secure 允许跨站。
Q: 什么是 HttpOnly Cookie?作用是什么?
A: HttpOnly 是 Cookie 的安全属性,设置为 true 后 JavaScript 无法通过 document.cookie 读取该 Cookie。主要作用是防止 XSS 攻击窃取 Session ID。
Q: HttpOnly 能完全防御 XSS 攻击吗?
A: 不能。HttpOnly 只能防止 Cookie 被窃取,但 XSS 攻击仍可执行其他恶意操作,如篡改页面内容、重定向、监听键盘等。需要配合 CSP、输出转义等综合防御。
Q: Cookie 中 httpOnly、secure、sameSite 的区别?
A:
httpOnly: 防止 JavaScript 读取 Cookie,防御 XSSsecure: 只在 HTTPS 下传输,防止中间人攻击sameSite: 防止跨站请求发送 Cookie,防御 CSRF
示例代码
文件夹结构
bash
examples/
├── package.json # 项目依赖配置
├── cors-demo.js # 跨域 (CORS) 演示
├── xss-demo.html # XSS 攻击交互演示
├── xss-defense.js # XSS 防御工具函数
├── csrf-demo.js # CSRF 攻击与防御演示
└── httponly-demo.js # HttpOnly Cookie 演示
1. package.json
json
{
"name": "web-security-examples",
"version": "1.0.0",
"description": "Web 安全示例:跨域、XSS 攻击、CSRF 攻击演示与防御",
"scripts": {
"cors": "node cors-demo.js",
"csrf": "node csrf-demo.js",
"defense": "node xss-defense.js",
"httponly": "node httponly-demo.js"
},
"dependencies": {
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"express": "^4.18.2",
"express-session": "^1.17.3",
"helmet": "^7.1.0"
}
}
2. cors-demo.js - 跨域演示
javascript
/**
* 跨域演示代码
*
* 运行方式:
* 1. npm install express cors
* 2. node cors-demo.js
* 3. 访问 http://localhost:3000
*/
const express = require('express')
const cors = require('cors')
const app = express()
// ============================================
// 方式一:使用 cors 中间件(推荐)
// ============================================
app.use(cors({
origin: ['http://localhost:3000', 'https://example.com'],
credentials: true,
optionsSuccessStatus: 200
}))
// ============================================
// 方式二:手动配置 CORS
// ============================================
/*
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Credentials', 'true')
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS')
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With')
res.header('Access-Control-Expose-Headers', 'X-Total-Count, X-Request-ID')
res.header('Access-Control-Max-Age', '86400')
if (req.method === 'OPTIONS') {
return res.sendStatus(204)
}
next()
})
*/
// API 路由
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head><title>CORS 测试</title></head>
<body>
<h1>CORS 跨域测试</h1>
<button onclick="testSimpleRequest()">简单请求 (GET)</button>
<button onclick="testComplexRequest()">复杂请求 (PUT)</button>
<script>
async function testSimpleRequest() {
const res = await fetch('/api/data');
const data = await res.json();
console.log(data);
}
async function testComplexRequest() {
const res = await fetch('/api/data', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'test' })
});
const data = await res.json();
console.log(data);
}
</script>
</body>
</html>
`);
});
app.get('/api/data', (req, res) => {
res.json({ message: 'GET 请求成功', timestamp: new Date().toISOString() });
});
app.listen(3000, () => console.log('服务器运行在 http://localhost:3000'))
3. xss-demo.html - XSS 攻击演示
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>XSS 攻击演示与防御</title>
<style>
body { font-family: Arial; max-width: 1000px; margin: 20px auto; padding: 20px; }
.vulnerable { border: 2px solid #f44336; background: #ffebee; padding: 15px; margin: 10px 0; }
.safe { border: 2px solid #4CAF50; background: #e8f5e9; padding: 15px; margin: 10px 0; }
button { padding: 10px 20px; margin: 5px; cursor: pointer; }
.btn-attack { background: #f44336; color: white; }
.btn-safe { background: #4CAF50; color: white; }
</style>
</head>
<body>
<h1>XSS 攻击演示与防御</h1>
<!-- 反射型 XSS -->
<div class="vulnerable">
<h3>危险示例 - innerHTML (可触发 XSS)</h3>
<input type="text" id="dangerous-input" placeholder="输入: <img src=x onerror=alert(1)>">
<button class="btn-attack" onclick="testDangerous()">测试攻击</button>
<div id="dangerous-output">结果将显示在这里...</div>
</div>
<div class="safe">
<h3>安全示例 - textContent</h3>
<input type="text" id="safe-input" placeholder="输入相同内容测试">
<button class="btn-safe" onclick="testSafe()">测试安全</button>
<div id="safe-output">结果将显示在这里...</div>
</div>
<script>
// 危险:直接使用 innerHTML
function testDangerous() {
const input = document.getElementById('dangerous-input').value;
document.getElementById('dangerous-output').innerHTML = input;
}
// 安全:使用 textContent
function testSafe() {
const input = document.getElementById('safe-input').value;
document.getElementById('safe-output').textContent = input;
}
// HTML 转义函数
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
</script>
</body>
</html>
4. xss-defense.js - XSS 防御工具
javascript
/**
* XSS 防御工具函数
*/
// HTML 转义
function escapeHtml(unsafe) {
if (typeof unsafe !== 'string') return unsafe
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'")
.replace(/\//g, "/")
}
// JavaScript 转义
function escapeJs(unsafe) {
if (typeof unsafe !== 'string') return unsafe
return unsafe
.replace(/\\/g, "\\\\")
.replace(/'/g, "\\'")
.replace(/"/g, '\\"')
.replace(/\n/g, "\\n")
.replace(/\r/g, "\\r")
}
// URL 转义
function escapeUrl(unsafe) {
return encodeURIComponent(unsafe)
}
// CSP Header 生成器
function generateCSP(options = {}) {
const {
defaultSrc = ["'self'"],
scriptSrc = ["'self'"],
styleSrc = ["'self'", "'unsafe-inline'"],
imgSrc = ["'self'", "data:", "https:"],
} = options
return [
`default-src ${defaultSrc.join(' ')}`,
`script-src ${scriptSrc.join(' ')}`,
`style-src ${styleSrc.join(' ')}`,
`img-src ${imgSrc.join(' ')}`,
].join('; ')
}
// 安全 Cookie 选项
function getSecureCookieOptions(options = {}) {
return {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/',
maxAge: 24 * 60 * 60 * 1000,
...options
}
}
// 安全响应头
function getSecurityHeaders() {
return {
'X-Frame-Options': 'DENY',
'X-Content-Type-Options': 'nosniff',
'X-XSS-Protection': '1; mode=block',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': 'geolocation=(), microphone=(), camera=()',
'Content-Security-Policy': generateCSP()
}
}
module.exports = {
escapeHtml,
escapeJs,
escapeUrl,
generateCSP,
getSecureCookieOptions,
getSecurityHeaders
}
5. csrf-demo.js - CSRF 攻击与防御演示
javascript
/**
* CSRF 攻击与防御演示
*
* 运行方式:
* 1. npm install express express-session cookie-parser
* 2. node csrf-demo.js
*/
const express = require('express')
const session = require('express-session')
const cookieParser = require('cookie-parser')
const crypto = require('crypto')
const app = express()
app.use(express.urlencoded({ extended: true }))
app.use(express.json())
app.use(cookieParser())
app.use(session({
secret: 'my-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: false,
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000
}
}))
// 模拟数据库
const db = {
users: [
{ id: 1, username: 'alice', balance: 10000 }
],
transfers: []
}
// 生成 CSRF Token
function generateCSRFToken() {
return crypto.randomBytes(32).toString('hex')
}
// CSRF 验证中间件
function csrfProtection(req, res, next) {
if (req.method === 'GET') {
if (!req.session.csrfToken) {
req.session.csrfToken = generateCSRFToken()
}
return next()
}
const token = req.body.csrf_token || req.headers['x-csrf-token']
const sessionToken = req.session.csrfToken
if (!token || token !== sessionToken) {
return res.status(403).send('CSRF Token 验证失败')
}
req.session.csrfToken = generateCSRFToken()
next()
}
// 主页
app.get('/', (req, res) => {
const user = req.session.user
res.send(`
<!DOCTYPE html>
<html>
<head><title>CSRF 演示</title></head>
<body>
<h1>CSRF 攻击与防御演示</h1>
${user ? `<p>已登录: ${user.username}, 余额: ¥${user.balance}</p>` : '<p>请先登录</p>'}
${!user ? `
<form method="POST" action="/login">
<input name="username" value="alice" required>
<input name="password" value="123456" required>
<button type="submit">登录</button>
</form>
` : `
<!-- 无保护的转账 -->
<h3>无保护的转账 (存在 CSRF 漏洞)</h3>
<form method="POST" action="/transfer/vulnerable">
<input name="to" value="attacker">
<input name="amount" value="100">
<button type="submit">转账 (无保护)</button>
</form>
<!-- 受保护的转账 -->
<h3>受保护的转账 (CSRF Token)</h3>
<form method="POST" action="/transfer/protected">
<input type="hidden" name="csrf_token" value="${req.session.csrfToken || ''}">
<input name="to" placeholder="收款人">
<input name="amount" placeholder="金额">
<button type="submit">转账 (受保护)</button>
</form>
<a href="/logout">退出登录</a>
`}
</body>
</html>
`)
})
// 登录
app.post('/login', (req, res) => {
const { username, password } = req.body
if (username === 'alice' && password === '123456') {
req.session.user = db.users[0]
req.session.csrfToken = generateCSRFToken()
res.redirect('/')
} else {
res.send('登录失败')
}
})
// 退出
app.get('/logout', (req, res) => {
req.session.destroy()
res.redirect('/')
})
// 无保护的转账
app.post('/transfer/vulnerable', (req, res) => {
const user = req.session.user
if (!user) return res.status(401).send('请先登录')
const { to, amount } = req.body
user.balance -= parseInt(amount)
res.send(`<p>向 ${to} 转账 ¥${amount} 成功!余额: ¥${user.balance}</p>`)
})
// 受保护的转账
app.post('/transfer/protected', csrfProtection, (req, res) => {
const user = req.session.user
if (!user) return res.status(401).send('请先登录')
const { to, amount } = req.body
user.balance -= parseInt(amount)
res.send(`<p>向 ${to} 转账 ¥${amount} 成功!余额: ¥${user.balance}</p>`)
})
app.listen(3000, () => console.log('CSRF 演示服务器运行在 http://localhost:3000'))
6. httponly-demo.js - HttpOnly Cookie 演示
javascript
/**
* HttpOnly Cookie 演示
*
* 运行方式:
* 1. npm install express cookie-parser
* 2. node httponly-demo.js
*/
const express = require('express')
const cookieParser = require('cookie-parser')
const app = express()
app.use(cookieParser())
app.use(express.urlencoded({ extended: true }))
const users = [
{ id: 1, username: 'alice', password: '123456' }
]
const sessions = new Map()
// Session 中间件
app.use((req, res, next) => {
const sessionId = req.cookies.session_id
if (sessionId && sessions.has(sessionId)) {
req.user = users.find(u => u.id === sessions.get(sessionId).userId)
}
next()
})
// 主页
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head><title>HttpOnly Cookie 演示</title></head>
<body>
<h1>HttpOnly Cookie 安全演示</h1>
${req.user ? `<p>已登录: ${req.user.username}</p>` : '<p>请先登录</p>'}
${!req.user ? `
<form method="POST" action="/login">
<input name="username" value="alice" required>
<input name="password" value="123456" required>
<button type="submit">登录</button>
</form>
` : `
<h3>JavaScript 读取 Cookie 测试</h3>
<button onclick="readCookie()">读取 Cookie</button>
<pre id="cookie-result"></pre>
<a href="/logout">退出登录</a>
`}
<script>
function readCookie() {
const cookies = document.cookie;
document.getElementById('cookie-result').textContent =
cookies || '(空 - HttpOnly Cookie 无法被 JS 读取!)';
}
</script>
</body>
</html>
`)
})
// 登录 - 设置 HttpOnly Cookie
app.post('/login', (req, res) => {
const { username, password } = req.body
const user = users.find(u => u.username === username && u.password === password)
if (!user) return res.send('登录失败')
const sessionId = Buffer.from(`${user.id}-${Date.now()}`).toString('base64')
sessions.set(sessionId, { userId: user.id, createdAt: Date.now() })
// HttpOnly Cookie - JS 无法读取
res.cookie('session_id', sessionId, {
httpOnly: true,
secure: false,
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000
})
// 普通 Cookie - JS 可以读取
res.cookie('user_info', JSON.stringify({ username: user.username }), {
maxAge: 24 * 60 * 60 * 1000
})
res.redirect('/')
})
app.get('/logout', (req, res) => {
res.clearCookie('session_id')
res.clearCookie('user_info')
res.redirect('/')
})
app.listen(3002, () => console.log('HttpOnly 演示服务器运行在 http://localhost:3002'))
运行方式
bash
cd examples
npm install
# 运行各个演示
npm run cors # http://localhost:3000
npm run defense # http://localhost:3001
npm run csrf # http://localhost:3000
npm run httponly # http://localhost:3002
💡 提示:安全是持续的过程,不是一次性配置。保持警惕,定期审计!