Web 安全:跨域、XSS 攻击与 CSRF 攻击

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, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;")
}

// 使用
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?

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 验证失败')
  }
  // 处理转账...
})
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('非法请求来源')
  }

  // 处理业务...
})
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,防御 XSS
  • secure: 只在 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="输入: &lt;img src=x onerror=alert(1)&gt;">
    <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, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
    }
  </script>
</body>
</html>

4. xss-defense.js - XSS 防御工具

javascript 复制代码
/**
 * XSS 防御工具函数
 */

// HTML 转义
function escapeHtml(unsafe) {
  if (typeof unsafe !== 'string') return unsafe
  return unsafe
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;")
    .replace(/\//g, "&#x2F;")
}

// 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'))
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

💡 提示:安全是持续的过程,不是一次性配置。保持警惕,定期审计!

相关推荐
AI_56782 小时前
Webpack从“配置到提速”,4步解决“打包慢、体积大”问题
前端·javascript·vue.js
pinkQQx2 小时前
手把手搭建前端跨平台服务(IPlatform + iOS / Android / Web)
前端·javascript
江启年2 小时前
对useEffect和 useMemo的一些总结与感悟
前端
Aotman_2 小时前
Vue el-table 字段自定义排序
前端·javascript·vue.js·es6
LaiYoung_2 小时前
🛡️ 代码质量的“埃癸斯”:为什么你的项目需要这面更懂业务的 ESLint 神盾?
前端·代码规范·eslint
AAA阿giao2 小时前
qoder-cli:下一代命令行 AI 编程代理——全面解析与深度实践指南
开发语言·前端·人工智能·ai编程·mcp·context7·qoder-cli
我有一棵树2 小时前
Vite 7 中 dev 没样式、build 却正常:一次由 CSS import 位置引发的工程化问题
前端·javascript·vue.js
@Autowire2 小时前
CSS 中 px、%、vh、vw 这四种常用单位的区别
前端·css
@Autowire2 小时前
CSS 中「继承属性」的核心知识,包括哪些属性可继承、继承的规则、如何控制继承(继承/取消继承)
前端·css