Skip to content

HTTP / HTTPS

HTTP 协议基础

HTTP(HyperText Transfer Protocol)是无状态的应用层协议,基于请求-响应模型:

客户端(浏览器)                               服务器
     |                                          |
     |  ── HTTP 请求 ──────────────────────→   |
     |     GET /api/users HTTP/1.1              |
     |     Host: example.com                    |
     |     Accept: application/json             |
     |                                          |
     |  ←────────────────── HTTP 响应 ──        |
     |     HTTP/1.1 200 OK                      |
     |     Content-Type: application/json       |
     |     { "users": [...] }                   |

请求方法语义

方法语义幂等安全请求体典型用途
GET获取资源查询数据
POST创建资源/提交数据表单提交、创建记录
PUT全量替换资源更新完整记录
PATCH部分更新资源更新部分字段
DELETE删除资源⚠️删除记录
HEAD获取响应头(无 body)检查资源是否存在
OPTIONS获取支持的方法CORS 预检请求
幂等(Idempotent):多次执行和一次执行效果相同
安全(Safe):不修改服务器状态

HTTP 状态码

分类

1xx  信息性    请求已接收,继续处理
2xx  成功      请求已成功处理
3xx  重定向    需要进一步操作才能完成
4xx  客户端错误  请求有问题
5xx  服务器错误  服务器处理出错

高频状态码详解

200 OK                    请求成功,返回数据
201 Created               资源创建成功(POST 响应)
204 No Content            成功但无响应体(DELETE 响应)

301 Moved Permanently     永久重定向(搜索引擎更新 URL)
302 Found                 临时重定向(保持原始方法不变——但实际浏览器会改为 GET)
303 See Other             临时重定向(明确要求用 GET)
304 Not Modified          资源未修改,使用缓存
307 Temporary Redirect    临时重定向(严格保持原始方法)
308 Permanent Redirect    永久重定向(严格保持原始方法)

400 Bad Request           请求格式错误
401 Unauthorized          未认证(需要登录)
403 Forbidden             已认证但无权限
404 Not Found             资源不存在
405 Method Not Allowed    请求方法不被允许
409 Conflict              资源冲突(如并发更新)
422 Unprocessable Entity  请求格式正确但语义错误
429 Too Many Requests     请求频率超限

500 Internal Server Error 服务器内部错误
502 Bad Gateway           网关/代理收到无效响应
503 Service Unavailable   服务不可用(维护/过载)
504 Gateway Timeout       网关/代理等待上游超时

301 vs 302 vs 307 vs 308

                    永久重定向          临时重定向
                    ───────────         ──────────
方法可能改变        301                 302
(POST → GET)    

方法严格不变        308                 307

实践建议:
- 域名迁移 → 301
- HTTPS 强制跳转 → 301
- 临时维护跳转 → 307
- 表单提交后跳转 → 303(明确用 GET)

HTTP 缓存

HTTP 缓存分为两层:强缓存协商缓存

浏览器发起请求

检查强缓存(Cache-Control / Expires)
  ├─ 命中 → 直接使用缓存(200 from cache),不发请求
  └─ 未命中 ↓
     发起请求,携带协商字段

     服务器检查资源是否变化
       ├─ 未变化 → 304 Not Modified(使用缓存)
       └─ 已变化 → 200 OK(返回新资源)

强缓存

浏览器不与服务器通信,直接使用本地缓存:

Cache-Control 头(HTTP/1.1,优先级高):
────────────────────────────────────────
Cache-Control: max-age=31536000    一年内直接用缓存
Cache-Control: no-cache            每次都要协商验证(不是不缓存!)
Cache-Control: no-store            完全不缓存(敏感数据用这个)
Cache-Control: private             只有浏览器可以缓存(不允许 CDN)
Cache-Control: public              允许 CDN 等中间缓存
Cache-Control: s-maxage=600        CDN 缓存时间(覆盖 max-age)
Cache-Control: immutable           资源永不变(即使用户刷新也不验证)
Cache-Control: stale-while-revalidate=60  
                                   缓存过期后 60s 内先返回旧缓存,后台重新验证

Expires 头(HTTP/1.0,优先级低):
────────────────────────────────
Expires: Thu, 01 Jan 2026 00:00:00 GMT
缺点:依赖客户端时间,可能不准确

协商缓存

强缓存过期后,浏览器携带条件请求头询问服务器资源是否变化:

方案一:Last-Modified / If-Modified-Since(基于修改时间)

首次请求:
  响应头:Last-Modified: Tue, 15 Oct 2024 10:30:00 GMT

再次请求:
  请求头:If-Modified-Since: Tue, 15 Oct 2024 10:30:00 GMT
  
服务器比较修改时间:
  未变化 → 304 Not Modified
  已变化 → 200 OK + 新资源

缺点:
  - 精度只到秒(1 秒内多次修改检测不到)
  - 文件内容没变但修改时间变了(如重新部署)也会失效


方案二:ETag / If-None-Match(基于内容哈希,优先级更高)

首次请求:
  响应头:ETag: "abc123def456"

再次请求:
  请求头:If-None-Match: "abc123def456"
  
服务器比较 ETag:
  匹配 → 304 Not Modified
  不匹配 → 200 OK + 新资源 + 新 ETag

优点:精确到内容级别
缺点:服务器需要计算哈希,有一定开销

实际缓存策略

不同资源的缓存策略:

HTML 文件(入口文件):
  Cache-Control: no-cache
  ETag: "..."
  → 每次都协商验证,确保获取最新版本

带 hash 的静态资源(JS/CSS/图片):
  Cache-Control: max-age=31536000, immutable
  → 文件名包含内容哈希(如 app.a1b2c3.js)
  → 内容变化 = 文件名变化 = 新 URL
  → 可以长期缓存

API 响应:
  Cache-Control: no-store
  → 动态数据不缓存


  Cache-Control: max-age=60, stale-while-revalidate=300
  → 对实时性要求不高的 API 可短期缓存
缓存更新流程:

index.html  ← no-cache(每次验证)
  引用
    ├─ app.a1b2c3.js   ← immutable(长期缓存)
    ├─ style.d4e5f6.css ← immutable(长期缓存)
    └─ logo.g7h8i9.svg  ← immutable(长期缓存)

代码更新后:
index.html  ← 服务器返回新版本
  引用
    ├─ app.x1y2z3.js   ← 新 hash = 新 URL = 重新下载
    ├─ style.d4e5f6.css ← hash 没变 = 继续用缓存
    └─ logo.g7h8i9.svg  ← hash 没变 = 继续用缓存

CORS(跨源资源共享)

同源策略

浏览器的**同源策略(Same-Origin Policy)**限制不同源的文档或脚本之间的交互:

同源的定义:协议 + 域名 + 端口 完全相同

https://example.com/page1
https://example.com/page2      ✅ 同源
http://example.com/page1       ❌ 协议不同
https://api.example.com/data   ❌ 域名不同(子域名也不同源)
https://example.com:3000/api   ❌ 端口不同

同源策略限制的行为:
  ✅ 允许:<script>、<img>、<link>、<iframe> 加载跨域资源
  ❌ 禁止:JS 读取跨域请求的响应
  ❌ 禁止:JS 读取跨域 iframe 的 DOM
  ❌ 禁止:JS 读取跨域 Cookie / Storage

CORS 机制

CORS 是浏览器和服务器之间的协议——服务器通过响应头告诉浏览器"允许哪些源访问"

简单请求(Simple Request):

条件(同时满足):
  - 方法为 GET / HEAD / POST
  - Header 只有 Accept / Accept-Language / Content-Language / 
    Content-Type(仅 text/plain / multipart/form-data / application/x-www-form-urlencoded)
  - 没有自定义 Header

流程:
  浏览器 ─→ 服务器
    GET /api/data
    Origin: https://app.example.com
    
  服务器 ─→ 浏览器
    HTTP/1.1 200 OK
    Access-Control-Allow-Origin: https://app.example.com
    (浏览器检查 Origin 是否在允许列表中)
预检请求(Preflight Request):

条件(不满足简单请求条件时触发):
  - 使用 PUT / DELETE / PATCH 等方法
  - 发送 Content-Type: application/json
  - 携带自定义 Header(如 Authorization)

流程:
  步骤 1:浏览器先发 OPTIONS 预检
    OPTIONS /api/users
    Origin: https://app.example.com
    Access-Control-Request-Method: POST
    Access-Control-Request-Headers: Content-Type, Authorization
    
  步骤 2:服务器响应允许的操作
    HTTP/1.1 204 No Content
    Access-Control-Allow-Origin: https://app.example.com
    Access-Control-Allow-Methods: GET, POST, PUT, DELETE
    Access-Control-Allow-Headers: Content-Type, Authorization
    Access-Control-Max-Age: 86400   ← 预检结果缓存 24 小时
    
  步骤 3:预检通过后,发送实际请求
    POST /api/users
    Origin: https://app.example.com
    Content-Type: application/json
    Authorization: Bearer xxx

CORS 响应头详解

Access-Control-Allow-Origin: https://app.example.com
  允许的源(不能同时设为 * 和使用 credentials)

Access-Control-Allow-Methods: GET, POST, PUT, DELETE
  允许的 HTTP 方法

Access-Control-Allow-Headers: Content-Type, Authorization
  允许的请求头

Access-Control-Expose-Headers: X-Total-Count, X-Request-Id
  允许 JS 读取的响应头(默认只能读取简单响应头)

Access-Control-Allow-Credentials: true
  允许携带 Cookie(前端 fetch 也要设 credentials: 'include')

Access-Control-Max-Age: 86400
  预检请求缓存时间(秒),减少 OPTIONS 请求次数
js
// 前端
fetch('https://api.example.com/data', {
  credentials: 'include',  // 必须
});

// 服务器响应头
Access-Control-Allow-Origin: https://app.example.com  // 不能是 *
Access-Control-Allow-Credentials: true                 // 必须

// 注意:Chrome 80+ 默认 SameSite=Lax
// 跨站请求不会自动携带 Cookie
// 需要 Cookie 设置 SameSite=None; Secure

Set-Cookie: session_id=abc123;
  Domain=.example.com;        可以被哪些域名访问
  Path=/;                     可以被哪些路径访问
  Expires=Thu, 01 Jan 2026;   过期时间(绝对时间)
  Max-Age=86400;              过期时间(相对秒数,优先级高于 Expires)
  HttpOnly;                   JS 不可读取(防 XSS 窃取)
  Secure;                     仅 HTTPS 传输
  SameSite=Lax;               跨站请求限制

SameSite 属性

SameSite=Strict
  完全禁止跨站携带 Cookie
  从 B 站跳转到 A 站 → 不带 A 站的 Cookie(用户需重新登录)

SameSite=Lax(默认值,Chrome 80+)
  允许顶级导航(链接点击、表单 GET)携带 Cookie
  禁止第三方请求(fetch、POST 表单、iframe)携带
  → 安全和体验的平衡

SameSite=None
  允许所有跨站请求携带(必须同时设置 Secure)
  → 第三方登录、支付回调等场景必须

对比:
场景                     Strict    Lax(默认)   None
───────────────────      ──────    ─────────   ────
顶级导航链接跳转          ❌         ✅          ✅
<img src="跨站">          ❌         ❌          ✅
fetch/XHR 跨站请求        ❌         ❌          ✅
<form method="GET">       ❌         ✅          ✅
<form method="POST">      ❌         ❌          ✅
<iframe>                  ❌         ❌          ✅

HTTPS

HTTP 的安全问题

HTTP 明文传输的三大风险:

1. 窃听(Eavesdropping)
   中间人可以看到所有传输内容(密码、Token)

2. 篡改(Tampering)
   中间人可以修改传输内容(注入广告、劫持页面)

3. 冒充(Impersonation)
   无法验证服务器身份(钓鱼网站)

TLS 握手流程

HTTPS = HTTP + TLS(Transport Layer Security)

TCP 三次握手完成后:

TLS 1.2 握手(2-RTT):
────────────────────
客户端                                    服务器
  |── ClientHello ──────────────────→     |  (1) 支持的加密套件列表 + 随机数A
  |                                       |
  |←── ServerHello ─────────────────     |  (2) 选定的加密套件 + 随机数B
  |←── Certificate ─────────────────     |  (3) 服务器证书(含公钥)
  |←── ServerHelloDone ────────────     |
  |                                       |
  |── ClientKeyExchange ───────────→     |  (4) 用公钥加密的 Pre-Master Secret
  |── ChangeCipherSpec ────────────→     |  (5) 后续使用协商的密钥加密
  |── Finished ────────────────────→     |
  |                                       |
  |←── ChangeCipherSpec ───────────     |  (6) 双方使用 随机数A + 随机数B + 
  |←── Finished ───────────────────     |      Pre-Master Secret 生成对称密钥
  |                                       |
  |════ 对称加密通信 ═══════════════     |


TLS 1.3 握手(1-RTT,更快更安全):
────────────────────────────────
客户端                                    服务器
  |── ClientHello + KeyShare ──────→     |  (1) 一次性发送:支持的加密套件 + 
  |                                       |      DH 公钥 + 随机数
  |←── ServerHello + KeyShare ─────     |  (2) 选定的加密套件 + DH 公钥
  |←── Certificate ────────────────     |  (3) 证书
  |←── Finished ───────────────────     |
  |                                       |
  |── Finished ────────────────────→     |  (4) 完成,开始加密通信

证书验证

证书信任链:

Root CA(根证书,预装在操作系统/浏览器中)
  └─ 签发 → Intermediate CA(中间证书)
              └─ 签发 → example.com 的证书(叶子证书)

验证流程:
1. 浏览器收到 example.com 的证书
2. 检查证书是否在有效期内
3. 检查证书域名是否匹配
4. 用 Intermediate CA 的公钥验证叶子证书的签名
5. 用 Root CA 的公钥验证 Intermediate CA 的签名
6. Root CA 在本地信任列表中 → 验证通过

HTTP 头部详解

请求头

常用请求头:

Host: example.com                    目标主机(HTTP/1.1 必须)
Origin: https://app.example.com      请求源(CORS/CSRF 防护)
Referer: https://google.com/search   来源页面 URL
User-Agent: Mozilla/5.0 ...          客户端标识

Accept: application/json             期望的响应格式
Accept-Language: zh-CN,en;q=0.9      期望的语言
Accept-Encoding: gzip, br            支持的压缩算法

Authorization: Bearer <token>         认证凭证
Cookie: session_id=abc123             Cookie

If-Modified-Since: Tue, 15 Oct...    协商缓存(时间)
If-None-Match: "etag-value"          协商缓存(ETag)

Content-Type: application/json       请求体格式
Content-Length: 256                   请求体长度

响应头

常用响应头:

Content-Type: application/json; charset=utf-8  响应体格式
Content-Length: 1024                            响应体长度
Content-Encoding: gzip                          压缩算法

Cache-Control: max-age=31536000                 缓存控制
ETag: "abc123"                                  资源指纹
Last-Modified: Tue, 15 Oct 2024 10:30:00 GMT   修改时间

Set-Cookie: id=abc; HttpOnly; Secure            设置 Cookie

Access-Control-Allow-Origin: *                  CORS 允许源
Access-Control-Allow-Methods: GET,POST          CORS 允许方法

X-Content-Type-Options: nosniff                 禁止 MIME 嗅探
X-Frame-Options: DENY                           禁止被 iframe 嵌入
Strict-Transport-Security: max-age=31536000     强制 HTTPS(HSTS)
Content-Security-Policy: default-src 'self'     CSP 安全策略

内容协商

浏览器和服务器通过 Header 协商最合适的资源格式

请求:
Accept: text/html, application/json;q=0.9, */*;q=0.8
Accept-Language: zh-CN, en;q=0.9
Accept-Encoding: gzip, br

响应:
Content-Type: text/html; charset=utf-8
Content-Language: zh-CN
Content-Encoding: br

q 值(Quality Value):0-1 的权重,默认 1,越高越优先

压缩

常见压缩算法对比:

算法       压缩率    速度    浏览器支持
─────     ──────   ──────  ──────────
gzip       好       快      所有浏览器
br(Brotli) 更好     稍慢    所有现代浏览器
zstd       最好     快      Chrome 123+

配置示例(Nginx):
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1024;

brotli on;
brotli_types text/plain text/css application/json application/javascript;

TCP 连接管理

HTTP/1.1 的连接策略

Keep-Alive(HTTP/1.1 默认开启):

HTTP/1.0:每个请求一个 TCP 连接(开销大)
  请求1: TCP 握手 → 请求 → 响应 → 关闭
  请求2: TCP 握手 → 请求 → 响应 → 关闭

HTTP/1.1 Keep-Alive:复用 TCP 连接
  TCP 握手 → 请求1 → 响应1 → 请求2 → 响应2 → ... → 关闭

但仍有队头阻塞(Head-of-Line Blocking):
  同一连接上的请求必须排队,前一个响应完才能发下一个

浏览器的应对策略:
  对同一域名开 6 个并行 TCP 连接(Chrome)
  所以经典优化手段是"域名分片"(domain sharding)
    → 将资源分散到 cdn1.example.com、cdn2.example.com
    → HTTP/2 时代已不需要

面试高频题

1. 强缓存和协商缓存的区别?缓存的完整判断流程是什么?

强缓存不与服务器通信,直接使用本地缓存(状态码 200 from cache);协商缓存需要向服务器发请求验证资源是否变化(304 Not Modified 或 200 OK)。完整流程:①浏览器先检查 Cache-Control / Expires,如果未过期则命中强缓存直接使用;②如果过期,携带 If-None-Match(ETag)和/或 If-Modified-Since(Last-Modified)发请求到服务器;③服务器优先比较 ETag(精确到内容),其次比较 Last-Modified(精确到秒);④资源未变返回 304,已变返回 200 + 新资源。实际项目中,HTML 用 no-cache(每次协商),带 hash 的静态资源用 max-age=31536000, immutable(长期强缓存)。

2. CORS 是什么?简单请求和预检请求的区别?

CORS 是浏览器实现的跨域资源共享机制——允许服务器通过响应头声明"哪些源可以访问"。简单请求(GET/HEAD/POST + 简单 Header + 简单 Content-Type)浏览器直接发送并在响应中检查 Access-Control-Allow-Origin。非简单请求(如 PUT/DELETE、Content-Type: application/json、自定义 Header)浏览器会先发一个 OPTIONS 预检请求,询问服务器是否允许该跨域操作,通过后才发实际请求。预检结果可通过 Access-Control-Max-Age 缓存。携带 Cookie 时必须设 credentials: 'include'Allow-Origin 不能为 *、且需要 Allow-Credentials: true

三个值:①Strict:完全禁止跨站携带 Cookie,最安全但影响体验(从搜索引擎跳转过来需要重新登录);②Lax(Chrome 80+ 默认):只允许顶级导航(链接点击、GET 表单提交)携带 Cookie,禁止第三方请求(fetch/POST 表单/iframe)携带——在安全性和可用性间取平衡;③None:允许所有跨站请求携带,但必须同时设置 Secure(仅 HTTPS),用于第三方登录、支付回调等场景。Lax 是默认值,这直接影响了跨域 API 请求的 Cookie 携带行为。

4. HTTPS 相比 HTTP 做了什么?TLS 握手的大致流程?

HTTPS 在 HTTP 基础上增加了 TLS 层,解决三个问题:①加密——防止窃听(对称加密保护数据);②完整性——防止篡改(MAC 消息认证码);③身份认证——防止冒充(证书 + 数字签名)。TLS 1.2 握手流程:ClientHello(支持的加密套件 + 随机数A)→ ServerHello(选定套件 + 随机数B + 证书)→ 客户端验证证书后用公钥加密 Pre-Master Secret → 双方用三个随机数生成对称密钥 → 后续用对称密钥加密通信。TLS 1.3 优化为 1-RTT(Client 在 Hello 中直接发送 DH 公钥),减少握手延迟。

5. 实际项目中如何设计 HTTP 缓存策略?

核心原则是"入口文件协商缓存 + 静态资源长期强缓存"。具体:①HTML 文件设置 Cache-Control: no-cache,每次访问都向服务器验证是否有更新(配合 ETag);②JS/CSS/图片等静态资源,文件名包含内容哈希(如 app.a1b2c3.js),设置 Cache-Control: max-age=31536000, immutable;③当代码更新时,HTML 引用的资源 URL 自然变化,浏览器会重新下载新文件,旧文件缓存自动失效;④API 响应根据实时性要求选择:no-store(不缓存)或 max-age=60, stale-while-revalidate=300(短期缓存 + 后台重新验证)。这套方案的优势是无缓存失效问题(hash 变了就是新 URL)。


追问思考

  1. Cache-Control: no-cacheno-store 的区别是什么?为什么 HTML 文件用 no-cache 而不是 no-store
  2. CDN 在缓存体系中扮演什么角色?s-maxagemax-age 分别控制谁的缓存?
  3. 为什么 CORS 的预检请求使用 OPTIONS 方法?如果服务器不处理 OPTIONS 会怎样?
  4. Referer 头部在隐私方面有什么问题?Referrer-Policy 有哪些值?
  5. HTTP/1.1 的队头阻塞(Head-of-Line Blocking)具体是怎么发生的?浏览器开 6 个连接是怎么解决这个问题的?

用心学习,用代码说话 💻