HTTP/HTTPS 协议详解
问题
HTTP 和 HTTPS 协议的核心概念是什么?请求方法、状态码、HTTPS 加密原理是怎样的?
答案
HTTP(HyperText Transfer Protocol)是互联网上应用最广泛的应用层协议,用于客户端与服务器之间的通信。
HTTP 协议概览
HTTP 请求方法
常用方法
| 方法 | 语义 | 幂等 | 安全 | 请求体 | 响应体 |
|---|---|---|---|---|---|
| GET | 获取资源 | ✅ | ✅ | ❌ | ✅ |
| POST | 创建资源 | ❌ | ❌ | ✅ | ✅ |
| PUT | 替换资源 | ✅ | ❌ | ✅ | ✅ |
| PATCH | 部分更新 | ❌ | ❌ | ✅ | ✅ |
| DELETE | 删除资源 | ✅ | ❌ | ❌ | ✅ |
| HEAD | 获取头信息 | ✅ | ✅ | ❌ | ❌ |
| OPTIONS | 查询支持方法 | ✅ | ✅ | ❌ | ✅ |
- 幂等:多次请求结果相同(GET、PUT、DELETE)
- 安全:不修改服务器状态(GET、HEAD、OPTIONS)
GET vs POST
// GET 请求 - 参数在 URL 中
fetch('/api/users?page=1&size=10')
.then(res => res.json());
// POST 请求 - 参数在请求体中
fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'John', email: 'john@example.com' })
});
| 特性 | GET | POST |
|---|---|---|
| 参数位置 | URL 查询字符串 | 请求体 |
| 参数长度 | 受 URL 长度限制(约 2KB) | 无限制 |
| 缓存 | 可被缓存 | 默认不缓存 |
| 书签 | 可收藏为书签 | 不能 |
| 历史记录 | 参数保留在历史 | 参数不保留 |
| 安全性 | 参数暴露在 URL | 相对安全 |
HTTP 状态码
状态码分类
| 分类 | 范围 | 含义 |
|---|---|---|
| 1xx | 100-199 | 信息性响应 |
| 2xx | 200-299 | 成功 |
| 3xx | 300-399 | 重定向 |
| 4xx | 400-499 | 客户端错误 |
| 5xx | 500-599 | 服务器错误 |
常见状态码
// 2xx 成功
200 OK // 请求成功
201 Created // 资源创建成功
204 No Content // 成功但无返回内容
// 3xx 重定向
301 Moved Permanently // 永久重定向(SEO 权重转移)
302 Found // 临时重定向
304 Not Modified // 资源未修改(协商缓存)
307 Temporary Redirect // 临时重定向(保持方法)
308 Permanent Redirect // 永久重定向(保持方法)
// 4xx 客户端错误
400 Bad Request // 请求语法错误
401 Unauthorized // 未认证
403 Forbidden // 无权限
404 Not Found // 资源不存在
405 Method Not Allowed // 方法不允许
429 Too Many Requests // 请求过多(限流)
// 5xx 服务器错误
500 Internal Server Error // 服务器内部错误
502 Bad Gateway // 网关错误
503 Service Unavailable // 服务不可用
504 Gateway Timeout // 网关超时
301 vs 302 vs 307 vs 308
| 状态码 | 类型 | 请求方法 | 场景 |
|---|---|---|---|
| 301 | 永久 | 可能变为 GET | 域名迁移、URL 永久变更 |
| 302 | 临时 | 可能变为 GET | 临时跳转(登录后跳转) |
| 307 | 临时 | 保持原方法 | POST 请求临时重定向 |
| 308 | 永久 | 保持原方法 | POST 请求永久重定向 |
HTTP Headers
请求头
GET /api/users HTTP/1.1
Host: api.example.com
User-Agent: Mozilla/5.0 ...
Accept: application/json
Accept-Language: zh-CN,zh;q=0.9
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Cookie: session_id=abc123
Cache-Control: no-cache
| 头字段 | 说明 |
|---|---|
| Host | 目标主机(HTTP/1.1 必需) |
| User-Agent | 客户端信息 |
| Accept | 可接受的响应类型 |
| Authorization | 认证信息 |
| Cookie | 携带的 Cookie |
| Cache-Control | 缓存控制 |
| Content-Type | 请求体类型 |
响应头
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 1234
Content-Encoding: gzip
Cache-Control: max-age=3600
ETag: "abc123"
Set-Cookie: session_id=xyz789; HttpOnly; Secure
Access-Control-Allow-Origin: *
| 头字段 | 说明 |
|---|---|
| Content-Type | 响应体类型 |
| Content-Length | 响应体长度 |
| Cache-Control | 缓存策略 |
| ETag | 资源标识(协商缓存) |
| Set-Cookie | 设置 Cookie |
| Access-Control-* | CORS 相关 |
HTTPS
HTTPS = HTTP + TLS
HTTPS 通过 TLS(Transport Layer Security)协议提供:
- 加密:数据传输加密,防止窃听
- 完整性:数据校验,防止篡改
- 身份认证:证书验证,防止冒充
TLS 握手过程
TLS 1.3 改进
| 特性 | TLS 1.2 | TLS 1.3 |
|---|---|---|
| 握手延迟 | 2-RTT | 1-RTT |
| 0-RTT 恢复 | ❌ | ✅ |
| 加密套件 | 复杂 | 精简 |
| 前向安全 | 可选 | 强制 |
证书与 CA
// 查看证书信息(Node.js)
import https from 'https';
const req = https.get('https://example.com', (res) => {
const cert = res.socket.getPeerCertificate();
console.log('Subject:', cert.subject);
console.log('Issuer:', cert.issuer);
console.log('Valid from:', cert.valid_from);
console.log('Valid to:', cert.valid_to);
});
HTTP 版本演进
| 版本 | 年份 | 特点 |
|---|---|---|
| HTTP/0.9 | 1991 | 仅支持 GET,无头部 |
| HTTP/1.0 | 1996 | 头部、状态码、多种方法 |
| HTTP/1.1 | 1997 | 持久连接、管道化、Host |
| HTTP/2 | 2015 | 多路复用、头部压缩、服务器推送 |
| HTTP/3 | 2022 | 基于 QUIC,0-RTT |
常见面试问题
Q1: HTTP 和 HTTPS 的区别?
答案:
| 特性 | HTTP | HTTPS |
|---|---|---|
| 端口 | 80 | 443 |
| 安全性 | 明文传输 | 加密传输 |
| 证书 | 不需要 | 需要 CA 证书 |
| 性能 | 快 | 略慢(TLS 握手) |
| SEO | 不利 | 有优势 |
Q2: GET 和 POST 的区别?
答案:
| 维度 | GET | POST |
|---|---|---|
| 语义 | 获取数据 | 提交数据 |
| 参数 | URL 中 | 请求体中 |
| 长度 | 有限制 | 无限制 |
| 缓存 | 可缓存 | 不缓存 |
| 幂等 | 是 | 否 |
| 安全 | 是(不修改数据) | 否 |
"POST 比 GET 安全" 是误解。两者都是明文传输(HTTP),真正的安全需要 HTTPS。
Q3: 常见状态码及含义?
答案:
// 成功
200 // OK - 请求成功
201 // Created - 创建成功
204 // No Content - 成功但无内容
// 重定向
301 // 永久重定向
302 // 临时重定向
304 // 未修改(使用缓存)
// 客户端错误
400 // Bad Request - 请求错误
401 // Unauthorized - 未认证
403 // Forbidden - 无权限
404 // Not Found - 未找到
// 服务器错误
500 // Internal Server Error - 服务器错误
502 // Bad Gateway - 网关错误
503 // Service Unavailable - 服务不可用
Q4: HTTPS 是如何保证安全的?
答案:
HTTPS 通过 TLS 协议实现三重保护:
- 机密性:对称加密(AES)加密数据
- 完整性:MAC 校验防止篡改
- 身份认证:CA 证书验证服务器身份
Q5: 输入 URL 到页面展示经历了哪些过程?
答案:
- DNS 解析:域名 → IP 地址
- TCP 连接:三次握手建立连接
- TLS 握手:(HTTPS)建立安全连接
- 发送请求:构造 HTTP 请求
- 服务器处理:处理请求,返回响应
- 接收响应:解析响应内容
- 渲染页面:构建 DOM、CSSOM、渲染树
- TCP 断开:四次挥手关闭连接
Q6: HTTP 请求方法中 PUT 和 PATCH 的区别?幂等性是什么?
答案:
PUT vs PATCH
| 维度 | PUT | PATCH |
|---|---|---|
| 语义 | 整体替换资源 | 部分更新资源 |
| 幂等性 | 幂等 | 非幂等 |
| 请求体 | 完整的资源表示 | 仅包含需要修改的字段 |
| 缺失字段 | 会被置为默认值或删除 | 不受影响 |
interface User {
id: number;
name: string;
email: string;
age: number;
}
// PUT - 整体替换(必须传完整对象)
// 如果不传 age,age 会被置空或删除
const putUpdate = async (): Promise<void> => {
await fetch('/api/users/1', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: '张三',
email: 'zhangsan@example.com',
age: 25 // 必须传所有字段
})
});
};
// PATCH - 部分更新(只传需要修改的字段)
// 其他字段保持不变
const patchUpdate = async (): Promise<void> => {
await fetch('/api/users/1', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'new-email@example.com' // 只更新 email
})
});
};
什么是幂等性?
幂等性(Idempotency)指的是:同一个请求执行一次和执行多次的效果完全相同,服务器状态不会因为多次执行而产生不同的结果。
// GET - 幂等 ✅
// 多次获取同一资源,结果一致
// GET /api/users/1 → 总是返回同一个用户
// PUT - 幂等 ✅
// 多次用同样的数据替换,最终结果一致
// PUT /api/users/1 { name: "张三", age: 25 } → 执行多次结果相同
// DELETE - 幂等 ✅
// 删除一次和删除多次,最终效果一样(资源不存在)
// DELETE /api/users/1 → 无论执行多少次,用户 1 都不存在
// POST - 非幂等 ❌
// 每次执行都可能创建新资源
// POST /api/users → 每次调用都创建一个新用户
// PATCH - 非幂等 ❌(严格来说)
// 某些 PATCH 操作可能依赖当前状态
// PATCH /api/users/1 { age: "increment" } → 每次 age 加 1,结果不同
幂等性在 API 设计中非常重要——客户端在网络超时后可以安全地重试幂等请求(GET、PUT、DELETE),而不用担心产生副作用。对于非幂等的 POST 请求,通常需要借助幂等键(Idempotency Key)来避免重复提交。
幂等性在实际项目中的应用
// 使用幂等键防止 POST 请求重复创建
const createOrder = async (orderData: Record<string, unknown>): Promise<Response> => {
const idempotencyKey = crypto.randomUUID(); // 客户端生成唯一标识
return fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey // 服务端根据此 key 去重
},
body: JSON.stringify(orderData)
});
};
Q7: 常见的 HTTP 状态码及其含义(301 vs 302 vs 307 vs 308、401 vs 403、502 vs 504)
答案:
重定向状态码:301 vs 302 vs 307 vs 308
这四个重定向状态码的核心区别在于是否永久和是否保持请求方法:
| 状态码 | 名称 | 永久/临时 | 请求方法变化 | 典型场景 |
|---|---|---|---|---|
| 301 | Moved Permanently | 永久 | 可能变为 GET | 域名迁移、HTTP → HTTPS |
| 302 | Found | 临时 | 可能变为 GET | 登录跳转、短链接 |
| 307 | Temporary Redirect | 临时 | 保持原方法 | POST 表单临时重定向 |
| 308 | Permanent Redirect | 永久 | 保持原方法 | POST API 永久迁移 |
历史原因:早期浏览器实现 301/302 时,会将 POST 请求自动改为 GET 再重定向。虽然 RFC 规范并未要求改变方法,但浏览器的行为已成事实标准。因此后来引入了 307/308 来明确保证不会改变请求方法。
// 场景 1: 网站从 HTTP 迁移到 HTTPS(301 永久重定向)
// Nginx 配置:
// server {
// listen 80;
// return 301 https://$host$request_uri;
// }
// 场景 2: POST 请求的临时重定向(307)
// 支付表单提交后临时跳转到第三方支付页面
// 必须用 307,否则 POST 数据会丢失
const handlePayment = async (paymentData: Record<string, unknown>): Promise<void> => {
const response = await fetch('/api/payment', {
method: 'POST',
body: JSON.stringify(paymentData),
redirect: 'follow' // 浏览器自动跟随重定向
});
// 如果返回 307,浏览器会用 POST 方法请求新 URL
// 如果返回 302,浏览器可能会用 GET 方法(POST 数据丢失)
};
客户端错误:401 vs 403
| 状态码 | 名称 | 含义 | 解决方式 |
|---|---|---|---|
| 401 | Unauthorized | 未认证——没有提供有效的身份凭证 | 登录或刷新 Token |
| 403 | Forbidden | 无权限——身份已认证,但权限不足 | 联系管理员获取权限 |
// 模拟 401 和 403 的处理
const handleApiResponse = async (response: Response): Promise<void> => {
switch (response.status) {
case 401:
// 未登录或 Token 过期 → 跳转登录页
console.log('身份未认证,请重新登录');
// 尝试刷新 Token
const refreshed = await refreshToken();
if (!refreshed) {
window.location.href = '/login';
}
break;
case 403:
// 已登录但权限不够 → 提示无权限
console.log('您没有权限执行此操作');
// 比如普通用户访问管理员页面
break;
}
};
const refreshToken = async (): Promise<boolean> => {
try {
const res = await fetch('/api/auth/refresh', { method: 'POST' });
return res.ok;
} catch {
return false;
}
};
- 401:你是谁?(请先证明身份)
- 403:我知道你是谁,但你不能做这件事
服务器错误:502 vs 504
| 状态码 | 名称 | 含义 | 常见原因 |
|---|---|---|---|
| 502 | Bad Gateway | 网关/代理收到了上游服务器的无效响应 | 上游服务挂了、返回了错误格式的响应 |
| 504 | Gateway Timeout | 网关/代理等待上游服务器超时 | 上游服务处理太慢、网络不通 |
502 和 504 都涉及网关/代理,通常出现在有 Nginx 反向代理的架构中。排查时的关键区别:
- 502:先检查上游服务是否存活(
systemctl status、端口是否监听) - 504:先检查上游服务响应时间,考虑增加 Nginx 的
proxy_read_timeout
Q8: HTTPS 的 TLS 握手过程(简述四次握手、证书验证、对称密钥协商)
答案:
HTTPS 在 TCP 三次握手之后,还需要进行 TLS 握手来建立安全连接。以 TLS 1.2 为例,握手可简化为四个阶段:
四次握手概览
详细步骤解析
| 阶段 | 方向 | 关键内容 | 说明 |
|---|---|---|---|
| ClientHello | 客户端 → 服务器 | TLS 版本、加密套件、Client Random | 告诉服务器:我支持哪些加密方式 |
| ServerHello | 服务器 → 客户端 | 选定套件、Server Random、证书 | 告诉客户端:我们用这个加密方式,这是我的证书 |
| 密钥交换 | 客户端 → 服务器 | Pre-Master Secret | 客户端验证证书后,生成并加密预主密钥 |
| Finished | 双向 | 加密验证消息 | 双方确认密钥协商成功,开始加密通信 |
证书验证过程
客户端收到服务器证书后,需要验证证书的合法性:
// 证书验证的核心步骤(伪代码)
interface Certificate {
subject: string; // 证书持有者(域名)
issuer: string; // 颁发机构
publicKey: string; // 公钥
signature: string; // CA 的数字签名
validFrom: Date;
validTo: Date;
}
const verifyCertificate = (cert: Certificate, hostname: string): boolean => {
// 1. 验证证书链:逐级验证直到根证书(浏览器内置信任)
// 服务器证书 → 中间 CA → 根 CA(受信任)
const chainValid = verifyCertificateChain(cert);
// 2. 验证域名:证书中的域名必须匹配请求的域名
const domainValid = cert.subject === hostname
|| matchWildcard(cert.subject, hostname);
// 3. 验证有效期:证书是否在有效期内
const now = new Date();
const dateValid = now >= cert.validFrom && now <= cert.validTo;
// 4. 验证签名:用 CA 的公钥验证证书签名,确保未被篡改
const signatureValid = verifySignature(cert);
// 5. 检查吊销状态:通过 CRL 或 OCSP 检查证书是否被吊销
const notRevoked = checkRevocationStatus(cert);
return chainValid && domainValid && dateValid && signatureValid && notRevoked;
};
// 类型声明(辅助函数)
declare function verifyCertificateChain(cert: Certificate): boolean;
declare function matchWildcard(pattern: string, hostname: string): boolean;
declare function verifySignature(cert: Certificate): boolean;
declare function checkRevocationStatus(cert: Certificate): boolean;
对称密钥协商过程
TLS 握手的核心目标是让双方安全地协商出一个对称密钥用于后续通信:
// 密钥协商过程(简化描述)
// 1. 双方各自生成随机数
const clientRandom: string = generateRandom(); // 客户端随机数(明文传输)
const serverRandom: string = generateRandom(); // 服务器随机数(明文传输)
// 2. 客户端生成预主密钥,用服务器公钥加密后发送
// 只有服务器的私钥才能解密 → 保证安全
const preMasterSecret: string = generateRandom();
const encrypted: string = rsaEncrypt(preMasterSecret, serverPublicKey);
// → 发送给服务器
// 3. 双方用相同的算法,从三个随机数推导出主密钥
// Master Secret = PRF(Pre-Master Secret, Client Random, Server Random)
const masterSecret: string = PRF(preMasterSecret, clientRandom, serverRandom);
// 4. 从主密钥派生出实际使用的会话密钥
// 包括:加密密钥、MAC 密钥、IV 等
const sessionKeys = deriveKeys(masterSecret);
// 后续通信全部使用对称加密(AES),速度快
// 非对称加密(RSA)仅用于交换预主密钥,速度慢但安全
// 类型声明(辅助函数)
declare function generateRandom(): string;
declare function rsaEncrypt(data: string, publicKey: string): string;
declare function PRF(secret: string, ...randoms: string[]): string;
declare function deriveKeys(master: string): Record<string, string>;
declare const serverPublicKey: string;
非对称加密(RSA/ECDHE)计算量大,速度慢,约比对称加密慢 100-1000 倍。所以 TLS 采用混合加密策略:用非对称加密安全地交换对称密钥,然后用对称加密(AES)加密实际数据,兼顾了安全性和性能。
TLS 1.3 的改进
TLS 1.3 将握手从 2-RTT 优化到 1-RTT,并支持 0-RTT 恢复:
| 特性 | TLS 1.2 | TLS 1.3 |
|---|---|---|
| 握手延迟 | 2-RTT | 1-RTT |
| 0-RTT 恢复 | 不支持 | 支持(会话恢复场景) |
| 密钥交换 | RSA 或 ECDHE | 仅 ECDHE(前向安全) |
| 加密套件 | 多种(含不安全的) | 仅保留 5 种安全套件 |
| 前向安全 | 可选 | 强制 |
即使服务器的私钥在未来被泄露,也无法解密之前已经完成的通信。TLS 1.3 强制使用 ECDHE 密钥交换,每次连接都生成临时密钥对,确保前向安全。