计算机网络 : 应用层协议HTTP
计算机网络 : 应用层协议HTTP
目录
- 计算机网络 : 应用层协议HTTP
- 引言
- 1. HTTP协议相关知识点
- 1.1 HTTP协议
- 1.2 认识URL
- 1.3 `urlencode`和`urldecode`
- 2. HTTP协议请求与响应格式
- 2.1 HTTP请求
- 2.2 HTTP响应
- 3. HTTP的方法
- 3.1 GET方法(重点)
- 3.2 POST方法(重点)
- 3.3 PUT方法
- 3.4 HEAD方法
- 3.5 DELETE方法
- 3.6 OPTIONS方法
- 4. HTTP的状态码
- 5. HTTP常见的`Header`
- 6. HTTP协议模拟实现
- 6.1 最简单的HTTP服务器
- 6.2 HTTP协议模拟实现
- `Http.hpp`
- `Util.hpp`
- 7. 附录——HTTP历史及版本技术与时代背景
- 8. HTTP `cookie`与`session`
- 8.1 HTTP Cookie
- 8.2 HTTP Session
- 9. HTTPS协议原理——加密
- 9.1 HTTPS相关概念
- 9.1.1 加密的原因
- 9.1.2 常见的加密方式
- 9.1.3 数据摘要(数据指纹)
- 9.2 HTTPS的工作过程探究
- 9.2.1 方案一:只使用对称加密
- 9.2.2 方案二:只使用非对称加密
- 9.2.3 方案三:双方都使用非对称加密
- 9.2.4 方案四:非对称加密+对称加密
- 9.2.5 中间人攻击——针对上面的四个方案
- 9.2.6 CA证书与数据签名
- 9.2.7 方案五:非对称加密 + 对称加密 + 证书认证(HTTPS原理)
- 9.3 常见问题(重要)
- 9.3.1 为什么摘要内容在网络传输的时候一定要加密形成签名?
- 9.3.2 为什么签名不直接加密,而是要先hash形成摘要?
- 9.3.3 如何成为中间人
- 9.3.4 中间人有没有可能篡改该证书
- 9.3.5 中间人整个掉包证书
引言
HTTP(超文本传输协议)是互联网应用层通信的核心协议之一,定义了客户端与服务器之间交换数据的规则。作为Web通信的基础,HTTP协议支撑着网页浏览、API交互等现代互联网服务。本文将从HTTP的基本概念出发,深入解析其请求与响应格式、方法、状态码、头部字段等核心内容,并通过代码示例展示HTTP服务器的实现原理。此外,还将探讨HTTPS的安全机制、Cookie与Session的工作原理,以及HTTP协议的演进历史。
1. HTTP协议相关知识点
1.1 HTTP协议
- 在互联网世界中,HTTP(
HyperText Transfer Protocol
,超文本传输协议)是一个至关重要的协议。它定义了客户端(如浏览器)与服务器之间如何通信,以交换或传输超文本(如 HTML 文档)。 - HTTP 协议是客户端与服务器之间通信的基础,客户端通过 HTTP 协议向服务器发送请求,服务器收到请求后处理并返回响应。HTTP 协议是一个无连接、无状态的协议,即每次请求都需要建立新的连接,且服务器不会保存客户端的状态信息。
1.2 认识URL
我们平常所说的网址就是URL
http://
:获取“资源”采用的协议www.example.jp
:域名——ip
地址(具有唯一性)/dir/index.htm
:要访问的资源路径(目标服务器上特定路径的一个文件(具有唯一性));如果只有一个/
表示要访问web的根目录
。uid=1
:为了访问提交的参数
1.3 urlencode
和urldecode
-
在URL中,像
/
、?
、:
等字符已被赋予特殊含义,因此不能直接使用。若参数中需包含这些特殊字符,必须先进行转义。 -
转义规则如下:将需转码的字符转换为16进制,从右到左取4位(不足4位直接处理),每2位作一组,前面加上
%
,编码成%XY
格式。 -
例如,
+
被转义为%2B
。 -
-
URL解码(
urldecode
)则是URL编码(urlencode
)的逆过程。 -
编码工具
2. HTTP协议请求与响应格式
2.1 HTTP请求
- 首行:
[方法] + [url] + [版本]
;uri
表示自己要请求什么资源 - Header: 请求的属性,冒号分割的键值对,每组属性之间使用
\r\n
分隔,遇到空行表示Header
部分结束;用空行将报头和有效载荷进行分离。 - Body: 空行后面的内容都是
Body
,Body
允许为空字符串,如果Body
存在,则在Header
中会有一个Content-Length
属性来标识Body
的长度。 http
协议,序列化和反序列化用的是特殊字符进行子串拼接,且不依赖任何第三方库。http
请求的本质就是请求服务器中的./wwwroot
目录下的特定路径下的资源。而uri
中的路径就是我们要访问的。- 我们在进行
http
请求时,首页作为站点的入口,一个网站就是一颗多叉树,点击链接时,浏览器会形成新的访问地址,发起二次请求。
2.2 HTTP响应
- [版本号] + [状态码] + [状态码解释];
- Header:请求的属性,冒号分割的键值对;每组属性之间使用
\r\n
分隔;遇到空行表示Header
部分结束;用空行将报头和有效载荷进行分离。 - Body:空行后面的内容都是
Body
,Body
允许为空字符串;如果Body
存在,则在Header
中会有一个Content-Length
属性来标识Body
的长度;如果服务器返回了一个html
页面,那么html
页面内容就是在body
中。
3. HTTP的方法
下面是两个常见方法和一些不常用的方法(了解即可)
3.1 GET方法(重点)
- 用途:用于请求 URL 指定的资源(用户从远端获取内容)
- 示例:
GET /index.html HTTP/1.1
, - 特性:指定资源经服务器端解析后返回响应内容,
- 参数提交方式:通过
uri
进行提交,且会回显参数 - form 表单:https://www.runoob.com/html/html-forms.html。
3.2 POST方法(重点)
- 用途:用于传输实体的主体,通常用于提交表单数据(用户向远端进行参数提交)
- 示例:
POST /submit.cgi HTTP/1.1
, - 特性:可以发送大量的数据给服务器,并且数据包含在请求体中,
- 参数提交方式:通过
http request
正文提交,不会回显参数 - form 表单:https://www.runoob.com/html/html-forms.html。
3.3 PUT方法
- 用途:用于传输文件,将请求报文主体中的文件保存到请求 URL 指定的位置,
- 示例:
PUT /example.html HTTP/1.1
, - 特性:不太常用,但在某些情况下(如 RESTful API 中)用于更新资源。
3.4 HEAD方法
- 用途:与 GET 方法类似,但不返回报文主体部分,仅返回响应头,
- 示例:
HEAD /index.html HTTP/1.1
, - 特性:用于确认 URL 的有效性及资源更新的日期时间等。
3.5 DELETE方法
- 用途:用于删除文件,是 PUT 的相反方法,
- 示例:
DELETE /example.html HTTP/1.1
, - 特性:按请求 URL 删除指定的资源。
3.6 OPTIONS方法
- 用途:用于查询针对请求 URL 指定的资源支持的方法,
- 示例:
OPTIONS * HTTP/1.1
, - 特性:返回允许的方法(如 GET、POST 等)。
4. HTTP的状态码
最常见的状态码包括 200(OK)、404(Not Found)、403(Forbidden)、302(Redirect,重定向) 和 504(Bad Gateway)。
状态码 | 含义 | 应用场景 |
---|---|---|
100 | Continue | 上传大文件时,服务器告知客户端可以继续上传 |
200 | OK | 访问网站首页时,服务器成功返回网页内容 |
201 | Created | 发布新文章后,服务器返回创建成功的信息 |
204 | No Content | 删除文章后,服务器返回"无内容"表示操作成功 |
301 | Moved Permanently | 网站更换域名后自动跳转到新域名;搜索引擎更新链接时使用 |
302 | Found / See Other | 用户登录成功后重定向到用户首页 |
304 | Not Modified | 浏览器缓存机制,对未修改的资源返回此状态码 |
400 | Bad Request | 提交表单时格式不正确导致失败 |
401 | Unauthorized | 访问需要登录的页面时未登录或认证失败 |
403 | Forbidden | 尝试访问没有权限查看的页面 |
404 | Not Found | 访问不存在的网页链接 |
500 | Internal Server Error | 服务器崩溃或数据库错误导致页面无法加载 |
502 | Bad Gateway | 代理服务器无法从上游服务器获取有效响应 |
503 | Service Unavailable | 服务器维护或过载,暂时无法处理请求 |
以下是仅包含重定向相关状态码的表格:
状态码 | 含义 | 是否为临时重定向 | 应用样例 |
---|---|---|---|
301 | Moved Permanently | 否(永久重定向) | 网站换域名后,自动跳转到新域名;搜索引擎更新网站链接时使用 |
302 | Found / See Other | 是(临时重定向) | 用户登录成功后,重定向到用户首页 |
307 | Temporary Redirect | 是(临时重定向) | 临时重定向资源到新的位置(较少使用) |
308 | Permanent Redirect | 否(永久重定向) | 永久重定向资源到新的位置(较少使用) |
关于重定向的验证,以301为代表:HTTP状态码301(永久重定向)和302(临时重定向)都依赖Location选项。以下是关于两者依赖Location选项的详细说明:
HTTP状态码301(永久重定向):
- 当服务器返回HTTP 301状态码时,表示请求的资源已经被永久移动到新的位置。
- 在这种情况下,服务器会在响应中添加一个Location头部,用于指定资源的新位置。这个Location头部包含了新的URL地址,浏览器会自动重定向到该地址。
- 例如,在HTTP响应中,可能会看到类似于以下的头部信息:
HTTP/1.1 301 Moved Permanently\r\n Location: https://www.new-url.com\r\n
HTTP状态码302(临时重定向):
- 当服务器返回HTTP 302状态码时,表示请求的资源临时被移动到新的位置。
- 同样地,服务器也会在响应中添加一个Location头部来指定资源的新位置。浏览器会暂时使用新的URL进行后续的请求,但不会缓存这个重定向。
- 例如,在HTTP响应中,可能会看到类似于以下的头部信息:
HTTP/1.1 302 Found\r\n Location: https://www.new-url.com\r\n
总结:无论是HTTP 301还是HTTP 302重定向,都需要依赖Location选项来指定资源的新位置。这个Location选项是一个标准的HTTP响应头部,用于告诉浏览器应该将请求重定向到哪个新的URL地址。
5. HTTP常见的Header
HTTP 报头说明说明:
Content-Type
: 数据类型(如text/html
)。Content-Length
: Body 的长度。Host
: 客户端告知服务器,所请求资源是在哪个主机和端口。User-Agent
: 声明用户的操作系统和浏览器版本信息。Referer
: 表示当前页面是从哪个页面跳转而来。Location
: 搭配 3xx 状态码使用,指示客户端下一步去哪里访问。Cookie
: 用于在客户端存储少量信息,通常用于实现会话(session)的功能。
关于Connection
报头详解:
HTTP 的 Connection
是报文头的一部分,用于控制客户端与服务器的连接状态。其核心作用包括:
- 管理持久连接(长连接):持久连接允许客户端和服务器在请求/响应完成后不立即关闭TCP链接,允许在同一个 TCP 连接上复用多个请求/响应。
- HTTP/1.1:默认启用持久连接,除非显式指定
Connection: close
。 - HTTP/1.0:默认非持久,需通过
Connection: keep-alive
显式启用。
- HTTP/1.1:默认启用持久连接,除非显式指定
- 语法格式:
Connection: keep-alive
:请求保持连接复用。Connection: close
:请求完成后关闭连接。
下面附上一张HTTP常见的header的表格
字段名 | 含义 | 样例 |
---|---|---|
Accept | 客户端可接受的响应内容类型 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 |
Accept-Encoding | 客户端支持的数据压缩格式 | Accept-Encoding: gzip, deflate, br |
Accept-Language | 客户端可接受的语言类型 | Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 |
Host | 请求的主机名和端口号 | Host: www.example.com:8080 |
User-Agent | 客户端的软件环境信息 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 |
Cookie | 客户端发送给服务器的HTTP cookie信息 | Cookie: session_id=abcdefg12345; user_id=123 |
Referer | 请求的来源URL | Referer: http://www.example.com/previous_page.html |
Content-Type | 实体主体的媒体类型 | Content-Type: application/x-www-form-urlencoded (表单提交) Content-Type: application/json (JSON数据) |
Content-Length | 实体主体的字节大小 | Content-Length: 150 |
Authorization | 认证信息,如用户名和密码 | Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== (Base64编码) |
Cache-Control | 缓存控制指令 | 请求时:Cache-Control: no-cache 或Cache-Control: max-age=3600 响应时:Cache-Control: public, max-age=3600 |
Connection | 请求完成后是否关闭或保持连接 | Connection: keep-alive 或Connection: close |
Date | 请求或响应的日期和时间 | Date: Wed, 21 Oct 2023 07:28:00 GMT |
Location | 重定向的目标URL(与3xx状态码配合使用) | Location: http://www.example.com/new_location.html (配合302状态码) |
Server | 服务器类型 | Server: Apache/2.4.41 (Unix) |
Last-Modified | 资源的最后修改时间 | Last-Modified: Wed, 21 Oct 2023 07:20:00 GMT |
ETag | 资源的唯一标识符,用于缓存 | ETag: "3f80f-1b6-5f4e2512a4100" |
Expires | 响应过期的日期和时间 | Expires: Wed, 21 Oct 2023 08:28:00 GMT |
这里还有一张Content-Type
对照表
Content-Type | 用途说明 | 典型示例 |
---|---|---|
text/plain | 纯文本 | .txt 文件 |
text/html | HTML 文档 | .html , .htm 网页 |
text/css | CSS 样式表 | .css 文件 |
text/javascript | JavaScript 代码 | .js 文件 |
application/json | JSON 数据 | API 请求/响应 |
application/xml | XML 数据 | .xml 文件 |
application/x-www-form-urlencoded | 表单提交(默认编码) | HTML 表单 POST 请求 |
multipart/form-data | 表单提交(含文件上传) | 文件上传表单 |
application/octet-stream | 二进制数据 | 文件下载 |
image/jpeg | JPEG 图像 | .jpg , .jpeg 图片 |
image/png | PNG 图像 | .png 图片 |
image/gif | GIF 图像 | .gif 动图/图片 |
image/svg+xml | SVG 矢量图 | .svg 矢量图形 |
audio/mpeg | MP3 音频 | .mp3 音频文件 |
video/mp4 | MP4 视频 | .mp4 视频文件 |
application/pdf | PDF 文档 | .pdf 文件 |
application/zip | ZIP 压缩文件 | .zip 压缩包 |
application/msword | Word 文档 | .doc 文件 |
application/vnd.ms-excel | Excel 文档 | .xls 文件 |
application/vnd.openxmlformats-officedocument.wordprocessingml.document | Word (新版) | .docx 文件 |
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet | Excel (新版) | .xlsx 文件 |
6. HTTP协议模拟实现
6.1 最简单的HTTP服务器
实现一个最简单的 HTTP 服务器,只在网页上输出 “Hello World”;只要我们按照 HTTP 协议的要求构造数据,就很容易能做到。
#include <sys/socket.h> // 提供socket相关函数和数据结构
#include <netinet/in.h> // 提供IPv4套接字地址结构sockaddr_in定义
#include <arpa/inet.h> // 提供IP地址转换函数
#include <unistd.h> // 提供POSIX操作系统API(如read/write)
#include <stdio.h> // 标准输入输出
#include <string.h> // 字符串操作函数
#include <stdlib.h> // 标准库函数(如atoi)// 打印程序使用说明
void Usage() {printf("usage: ./server [ip] [port]\n");
}int main(int argc, char* argv[]) {// 检查参数数量是否正确(程序名 + IP + 端口)if (argc != 3) {Usage();return 1;}// 1. 创建套接字// AF_INET: IPv4协议族// SOCK_STREAM: 面向连接的TCP套接字// 0: 默认协议(TCP)int fd = socket(AF_INET, SOCK_STREAM, 0);if (fd < 0) {perror("socket"); // 打印错误信息return 1;}// 2. 绑定地址和端口struct sockaddr_in addr;addr.sin_family = AF_INET; // IPv4地址族addr.sin_addr.s_addr = inet_addr(argv[1]); // 将字符串IP转换为网络字节序addr.sin_port = htons(atoi(argv[2])); // 将字符串端口转换为网络字节序的短整型// 绑定套接字到指定IP和端口int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));if (ret < 0) {perror("bind");return 1;}// 3. 监听连接// 10: 等待连接队列的最大长度(backlog)ret = listen(fd, 10);if (ret < 0) {perror("listen");return 1;}// 4. 进入无限循环处理客户端连接for (;;) {// 准备客户端地址结构struct sockaddr_in client_addr;socklen_t len = sizeof(client_addr);// 接受客户端连接int client_fd = accept(fd, (struct sockaddr*)&client_addr, &len);if (client_fd < 0) {perror("accept");continue; // 接受失败继续尝试}// 5. 读取客户端请求数据char input_buf[1024 * 10] = {0}; // 10KB的输入缓冲区,初始化为0ssize_t read_size = read(client_fd, input_buf, sizeof(input_buf) - 1);if (read_size < 0) {close(client_fd); // 读取失败关闭客户端套接字continue;}// 打印收到的请求(用于调试)printf("[Request] %s", input_buf);// 6. 准备并发送HTTP响应char buf[1024] = {0}; // 响应缓冲区const char* hello = "<h1>hello world</h1>"; // 响应内容// 格式化HTTP响应// 注意:这个HTTP响应格式不标准,仅作示例sprintf(buf, "HTTP/1.0 200 OK\nContent-Length:%lu\n\n%s", strlen(hello), hello);// 发送响应给客户端write(client_fd, buf, strlen(buf));// 7. 关闭客户端连接close(client_fd);}// 8. 关闭服务器套接字(实际不会执行到这里)close(fd);return 0;
}
编译并启动服务后,在浏览器中输入 http://[ip]:[port]
,就能看到显示的结果 “Hello World”。
备注:此处我们使用 9090 端口号启动了 HTTP 服务器。虽然 HTTP 服务器一般使用 80 端口,但这只是一个通用的习惯,并不是说 HTTP 服务器就不能使用其他的端口号。
6.2 HTTP协议模拟实现
Http.hpp
#pragma once // 防止头文件重复包含// 包含必要的头文件
#include "Socket.hpp" // 套接字相关操作
#include "TcpServer.hpp" // TCP服务器实现
#include "Util.hpp" // 工具函数
#include "Log.hpp" // 日志模块
#include <iostream> // 标准输入输出
#include <string> // 字符串操作
#include <memory> // 智能指针
#include <sstream> // 字符串流
#include <functional> // 函数对象
#include <unordered_map> // 哈希表// 使用命名空间
using namespace SocketModule; // 套接字模块
using namespace LogModule; // 日志模块// 定义常量字符串
const std::string gspace = " "; // 空格
const std::string glinespace = "\r\n"; // HTTP换行符
const std::string glinesep = ": "; // HTTP头部键值分隔符// 定义Web根目录和默认页面
const std::string webroot = "./wwwroot"; // 网站根目录
const std::string homepage = "index.html"; // 默认首页
const std::string page_404 = "/404.html"; // 404错误页面/*** @class HttpRequest* @brief HTTP请求类,用于解析和存储HTTP请求信息*/
class HttpRequest
{
public:// 构造函数,初始化交互标志为falseHttpRequest() : _is_interact(false) {}// 序列化方法(当前为空实现)std::string Serialize(){return std::string();}/*** @brief 解析请求行* @param reqline 请求行字符串,格式如 "GET / HTTP/1.1"*/void ParseReqLine(std::string &reqline){std::stringstream ss(reqline); // 使用字符串流解析ss >> _method >> _uri >> _version; // 分别提取方法、URI和版本/*这是一个链式的流输入操作,从字符串流 ss 中依次提取三个数据:
第一个单词 → 存入 _method 变量(通常是 HTTP 方法,如 GET/POST)
第二个单词 → 存入 _uri 变量(请求的资源路径,如 /index.html)
第三个单词 → 存入 _version 变量(HTTP 版本,如 HTTP/1.1)*/}/*** @brief 反序列化HTTP请求* @param reqstr 完整的HTTP请求字符串* @return 是否解析成功*/bool Deserialize(std::string &reqstr){// 1. 提取请求行std::string reqline;bool res = Util::ReadOneLine(reqstr, &reqline, glinespace);LOG(LogLevel::DEBUG) << reqline;// 2. 对请求行进行反序列化ParseReqLine(reqline);// 处理URIif (_uri == "/")_uri = webroot + _uri + homepage; // 默认访问首页,如 ./wwwroot/index.htmlelse_uri = webroot + _uri; // 其他资源路径,如 ./wwwroot/a/b/c.html// 记录日志LOG(LogLevel::DEBUG) << "_method: " << _method;LOG(LogLevel::DEBUG) << "_uri: " << _uri;LOG(LogLevel::DEBUG) << "_version: " << _version;// 检查URI中是否包含查询参数const std::string temp = "?";auto pos = _uri.find(temp);if (pos == std::string::npos){return true; // 没有查询参数}// 提取查询参数_args = _uri.substr(pos + temp.size()); // 参数部分,如 username=zhangsan&password=123456_uri = _uri.substr(0, pos); // 实际URI部分,如 ./wwwroot/login_is_interact = true; // 标记为交互请求return true;}// 获取URIstd::string Uri(){return _uri;}// 判断是否是交互请求bool isInteract(){ return _is_interact;}// 获取查询参数std::string Args(){return _args;}// 析构函数~HttpRequest() {}private:std::string _method; // HTTP方法(GET/POST等)std::string _uri; // 请求URIstd::string _version; // HTTP版本std::unordered_map<std::string, std::string> _headers; // 请求头std::string _blankline; // 空行std::string _text; // 请求体std::string _args; // 查询参数bool _is_interact; // 是否是交互请求标志
};/*** @class HttpResponse* @brief HTTP响应类,用于构建和发送HTTP响应*/
class HttpResponse
{
public:// 构造函数,初始化空行和HTTP版本HttpResponse() : _blankline(glinespace), _version("HTTP/1.0") {}/*** @brief 序列化HTTP响应* @return 序列化后的HTTP响应字符串*/std::string Serialize(){// 构建状态行,如 "HTTP/1.0 200 OK\r\n"std::string status_line = _version + gspace + std::to_string(_code) + gspace + _desc + glinespace;// 构建响应头std::string resp_header;for (auto &header : _headers){std::string line = header.first + glinesep + header.second + glinespace;resp_header += line;}// 组合状态行、响应头、空行和响应体return status_line + resp_header + _blankline + _text;}// 设置目标文件路径void SetTargetFile(const std::string &target){_targetfile = target;}/*** @brief 设置响应状态码* @param code HTTP状态码*/void SetCode(int code){_code = code;switch (_code){case 200:_desc = "OK";break;case 404:_desc = "Not Found";break;case 301:_desc = "Moved Permanently";break;case 302:_desc = "See Other";break;default:break;}}/*** @brief 添加响应头* @param key 头部键* @param value 头部值*/void SetHeader(const std::string &key, const std::string &value){auto iter = _headers.find(key);if (iter != _headers.end())return;_headers.insert(std::make_pair(key, value));}/*** @brief 根据文件后缀确定Content-Type* @param targetfile 目标文件名* @return 对应的MIME类型*/std::string Uri2Suffix(const std::string &targetfile){// 查找最后一个点号auto pos = targetfile.rfind(".");if (pos == std::string::npos){return "text/html"; // 默认返回HTML类型}// 根据后缀返回对应MIME类型std::string suffix = targetfile.substr(pos);if (suffix == ".html" || suffix == ".htm")return "text/html";else if (suffix == ".jpg")return "image/jpeg";else if (suffix == "png")return "image/png";elsereturn "";}/*** @brief 构建HTTP响应*/bool MakeResponse(){// 特殊处理favicon.ico请求//favicon.ico 请求是浏览器自动发起的一个请求,目的是获取网站的Favicon(网站图标)。这个图标通常显示在浏览器标签页、书签栏或历史记录中,用于视觉识别网站。if (_targetfile == "./wwwroot/favicon.ico"){LOG(LogLevel::DEBUG) << "用户请求: " << _targetfile << "忽略它";return false;}// 重定向测试if (_targetfile == "./wwwroot/redir_test"){SetCode(301);SetHeader("Location", "https://www.qq.com/");return true;}// 读取文件内容int filesize = 0;bool res = Util::ReadFileContent(_targetfile, &_text);if (!res) // 文件不存在{_text = "";LOG(LogLevel::WARNING) << "client want get : " << _targetfile << " but not found";SetCode(404);// 返回404页面_targetfile = webroot + page_404;filesize = Util::FileSize(_targetfile);Util::ReadFileContent(_targetfile, &_text);// 设置响应头std::string suffix = Uri2Suffix(_targetfile);SetHeader("Content-Type", suffix);SetHeader("Content-Length", std::to_string(filesize));}else // 文件存在{LOG(LogLevel::DEBUG) << "读取文件: " << _targetfile;SetCode(200);filesize = Util::FileSize(_targetfile);std::string suffix = Uri2Suffix(_targetfile);SetHeader("Conent-Type", suffix);SetHeader("Content-Length", std::to_string(filesize));SetHeader("Set-Cookie", "username=zhangsan;");}return true;}// 设置响应体文本void SetText(const std::string &t){_text = t;}// 反序列化方法(当前为空实现)bool Deserialize(std::string &reqstr){return true;}// 析构函数~HttpResponse() {}public: // 注意:这里应该是private,但为了方便调试暂时设为publicstd::string _version; // HTTP版本int _code; // 状态码,如404std::string _desc; // 状态描述,如"Not Found"std::unordered_map<std::string, std::string> _headers; // 响应头std::vector<std::string> cookie; // Cookie(未使用)std::string _blankline; // 空行std::string _text; // 响应体std::string _targetfile; // 目标文件路径
};// HTTP处理函数类型定义
using http_func_t = std::function<void(HttpRequest &req, HttpResponse &resp)>;/*** @class Http* @brief HTTP服务器类,处理HTTP请求和响应*/
class Http
{
public:// 构造函数,初始化TCP服务器Http(uint16_t port) : tsvrp(std::make_unique<TcpServer>(port)) {}/*** @brief 处理HTTP请求* @param sock 客户端套接字* @param client 客户端地址信息*/void HandlerHttpRquest(std::shared_ptr<Socket> &sock, InetAddr &client){// 接收HTTP请求std::string httpreqstr;int n = sock->Recv(&httpreqstr); // 接收HTTP请求字符串if (n > 0) // 接收到数据{// 打印请求内容(调试用)std::cout << "##########################" << std::endl;std::cout << httpreqstr;std::cout << "##########################" << std::endl;// 解析请求和构建响应HttpRequest req;HttpResponse resp;req.Deserialize(httpreqstr);if (req.isInteract()) // 处理交互请求{if (_route.find(req.Uri()) == _route.end()){// 未找到路由(可添加302重定向处理)}else{// 通过哈希表_route调用注册的处理函数_route[req.Uri()](req, resp);//_route[req.Uri()] 会根据请求路径查找对应的处理函数。(req,resp)两个参数传入处理函数std::string response_str = resp.Serialize();sock->Send(response_str); // 发送响应}}else // 处理静态资源请求{resp.SetTargetFile(req.Uri());if (resp.MakeResponse()){std::string response_str = resp.Serialize();sock->Send(response_str);}}}// 调试模式下的处理(已注释掉)
#ifdef DEBUGstd::string httpreqstr;sock->Recv(&httpreqstr);std::cout << httpreqstr;HttpResponse resp;resp._version = "HTTP/1.1";resp._code = 200;resp._desc = "OK";std::string filename = webroot + homepage;bool res = Util::ReadFileContent(filename, &(resp._text));(void)res;std::string response_str = resp.Serialize();sock->Send(response_str);
#endif}// 启动HTTP服务器void Start(){tsvrp->Start([this](std::shared_ptr<Socket> &sock, InetAddr &client){ this->HandlerHttpRquest(sock, client); });}/*** @brief 注册服务处理函数* @param name 服务名称* @param h 处理函数*/void RegisterService(const std::string name, http_func_t h){std::string key = webroot + name; // 构建完整路径auto iter = _route.find(key);if (iter == _route.end()){_route.insert(std::make_pair(key, h)); // 添加到路由表}}// 析构函数~Http() {}private:std::unique_ptr<TcpServer> tsvrp; // TCP服务器实例std::unordered_map<std::string, http_func_t> _route; // 路由表
};
Util.hpp
#pragma once // 防止头文件被重复包含#include <iostream> // 标准输入输出流
#include <fstream> // 文件流操作
#include <string> // 字符串操作/*** @brief 工具类,提供文件读取和字符串处理等静态方法* * 这个类包含静态方法,不需要实例化即可使用*/
class Util
{
public:/*** @brief 读取文件内容到字符串中* @param filename 要读取的文件路径* @param out 输出参数,用于存储读取的文件内容*/static bool ReadFileContent(const std::string &filename, std::string *out){// 版本1:以文本方式逐行读取文件(不适合二进制文件如图片)// std::ifstream in(filename);// if (!in.is_open())// {// return false;// }// std::string line;// while(std::getline(in, line))// {// *out += line;// }// in.close();// 版本2:以二进制方式读取整个文件内容int filesize = FileSize(filename); // 先获取文件大小// 检查文件大小是否有效if(filesize > 0){// 打开文件std::ifstream in(filename);if(!in.is_open())return false; // 文件打开失败// 调整输出字符串大小以容纳文件内容out->resize(filesize);// 读取整个文件内容到字符串中// 注意:使用c_str()获取底层字符数组指针,并强制转换为char*进行读取in.read((char*)(out->c_str()), filesize);// 关闭文件in.close();}else{return false; // 文件大小无效或文件不存在}return true; // 读取成功}/*** @brief 从大字符串中读取一行(根据分隔符)* * @param bigstr 输入的大字符串(会被修改,读取后剩余部分保留)* @param out 输出参数,存储读取的一行内容* @param sep 行分隔符(如"\r\n")*/static bool ReadOneLine(std::string &bigstr, std::string *out, const std::string &sep){// 查找分隔符位置auto pos = bigstr.find(sep);// 如果没有找到分隔符,返回falseif(pos == std::string::npos)return false;// 提取分隔符之前的内容作为一行*out = bigstr.substr(0, pos);// 从原字符串中删除已读取的部分(包括分隔符)bigstr.erase(0, pos + sep.size());return true; // 读取成功}/*** @brief 获取文件大小(以字节为单位)* * @param filename 文件路径* @return int 文件大小(字节数),-1表示文件打开失败*/static int FileSize(const std::string &filename){// 以二进制模式打开文件std::ifstream in(filename, std::ios::binary);if(!in.is_open())return -1; // 文件打开失败//下面三行代码通过文件指针来获取文件大小// 将文件指针移动到文件末尾in.seekg(0, in.end);//0为偏移量,in.end为基准位置// 获取当前指针位置(即文件大小)int filesize = in.tellg();// 将文件指针移回文件开头in.seekg(0, in.beg);// 关闭文件in.close();return filesize; // 返回文件大小}
};
7. 附录——HTTP历史及版本技术与时代背景
-
HTTP/0.9 核心技术:
- 仅支持 GET 请求方法;
- 仅支持纯文本传输,主要是 HTML 格式;
- 无请求和响应头信息。
- 时代背景:1991 年,HTTP/0.9 版本作为 HTTP 协议的最初版本,用于传输基本的超文本 HTML 内容;
- 当时的互联网还处于起步阶段,网页内容相对简单,主要以文本为主。
-
HTTP/1.0 核心技术:
- 引入 POST 和 HEAD 请求方法;
- 请求和响应头信息,支持多种数据格式(MIME);
- 支持缓存(cache);
- 状态码(status code)、多字符集支持等。
- 时代背景:1996 年,随着互联网的快速发展,网页内容逐渐丰富,HTTP/1.0 版本应运而生;
- 为了满足日益增长的网络应用需求,HTTP/1.0 增加了更多的功能和灵活性;
- 然而,HTTP/1.0 的工作方式是每次 TCP 连接只能发送一个请求,性能上存在一定局限。
-
HTTP/1.1 核心技术:
- 引入持久连接(persistent connection),支持管道化(pipelining);
- 允许在单个 TCP 连接上进行多个请求和响应,提高了性能;
- 引入分块传输编码(chunked transfer encoding);
- 支持 Host 头,允许在一个 IP 地址上部署多个 Web 站点。
- 时代背景:1999 年,随着网页加载的外部资源越来越多,HTTP/1.0 的性能问题愈发突出;
- HTTP/1.1 通过引入持久连接和管道化等技术,有效提高了数据传输效率;
- 同时,互联网应用开始呈现出多元化、复杂化的趋势,HTTP/1.1 的出现满足了这些需求。
-
HTTP/2.0 核心技术:
- 多路复用(multiplexing),一个 TCP 连接允许多个 HTTP 请求;
- 二进制帧格式(binary framing),优化数据传输;
- 头部压缩(header compression),减少传输开销;
- 服务器推送(server push),提前发送资源到客户端。
- 时代背景:2015 年,随着移动互联网的兴起和云计算技术的发展,网络应用对性能的要求越来越高;
- HTTP/2.0 通过多路复用、二进制帧格式等技术,显著提高了数据传输效率和网络性能;
- 同时,HTTP/2.0 还支持加密传输(HTTPS),提高了数据传输的安全性。
-
HTTP/3.0 核心技术:
- 使用 QUIC 协议替代 TCP 协议,基于 UDP 构建的多路复用传输协议;
- 减少了 TCP 三次握手及 TLS 握手时间,提高了连接建立速度;
- 解决了 TCP 中的线头阻塞问题,提高了数据传输效率。
- 时代背景:2022 年,随着 5G、物联网等技术的快速发展,网络应用对实时性、可靠性的要求越来越高;
- HTTP/3.0 通过使用 QUIC 协议,提高了连接建立速度和数据传输效率,满足了这些需求;
- 同时,HTTP/3.0 还支持加密传输(HTTPS),保证了数据传输的安全性。
8. HTTP cookie
与session
8.1 HTTP Cookie
-
定义:
HTTP Cookie(也称为 Web Cookie、浏览器 Cookie 或简称 Cookie)是服务器发送到用户浏览器并保存在浏览器上的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态、记录用户偏好等。
-
工作原理:
- 当用户第一次访问网站时,服务器会在响应的 HTTP 头中设置 Set-Cookie 字段,用于发送 Cookie 到用户的浏览器;
- 浏览器在接收到 Cookie 后,会将其保存在本地(通常是按照域名进行存储);
- 在之后的请求中,浏览器会自动在 HTTP 请求头中携带 Cookie 字段,将之前保存的 Cookie 信息发送给服务器。
-
分类:
- 会话 Cookie(Session Cookie):在浏览器关闭时失效。
- 持久 Cookie(Persistent Cookie):带有明确的过期日期或持续时间,可以跨多个浏览器会话存在。
- 如果
Cookie
是一个持久性的Cookie
,那么它其实就是浏览器相关的特定目录下的一个文件,但直接查看这些文件可能会看到乱码或无法读取的内容,因为Cookie
文件通常以二进制或SQLite
格式存储。一般我们查看时直接在浏览器对应的选项中查看即可。
-
安全性:
由于
Cookie
是存储在客户端的,因此存在被篡改或窃取的风险。 -
用途:
- 用户认证和会话管理(最重要)
- 跟踪用户行为
- 缓存用户偏好等。
- 例如在 Chrome 浏览器下,可以直接访问:
chrome://settings/cookies
。
-
认识Cookie:
- HTTP 存在一个报头选项 Set-Cookie,可以用来给浏览器设置 Cookie 值。
- 在 HTTP 响应报头中添加 Set-Cookie 后,客户端(如浏览器)会获取该值并自行设置和保存 Cookie。
-
基本格式:
Set-Cookie: <name>=<value> 其中 <name> 是 Cookie 的名称,<value> 是 Cookie 的值。
-
Set-Cookie
头部字段:属性 值 描述 username peter 这是 Cookie 的名称和值,标识用户名为"peter"。 expires Thu, 18 Dec 2024 12:00:00 UTC 指定 Cookie 的过期时间。在这个例子中,Cookie 将在 2024 年 12 月 18 日 12:00:00 UTC 后过期。 path / 定义 Cookie 的作用范围。这里设置为根路径/,意味着 Cookie 对.example.com 域名下的所有路径都可用。 domain .example.com 指定哪些域名可以接收这个 Cookie。点前缀(.)表示包括所有子域名。 secure - 指示 Cookie 只能通过 HTTPS 协议发送,不能通过 HTTP 协议发送,增加安全性。 HttpOnly - 阻止客户端脚本(如 JavaScript)访问此 Cookie,有助于防止跨站脚本攻击(XSS)。 注意事项:
- 每个 Cookie 属性都以分号(
;
)和空格()分隔; - 名称和值之间使用等号(
=
)分隔; - 如果 Cookie 的名称或值包含特殊字符(如空格、分号、逗号等),则需要进行 URL 编码。
- 每个 Cookie 属性都以分号(
-
Cookie的生命周期:
- 如果设置了
expires
属性,则 Cookie 将在指定的日期/时间后过期; - 如果没有设置
expires
属性,则 Cookie 默认为会话 Cookie,即当浏览器关闭时过期。
- 如果设置了
-
安全性考虑:
- 使用 secure 标志可以确保 Cookie 仅在 HTTPS 连接上发送,从而提高安全性;
- 使用 HttpOnly 标志可以防止客户端脚本(如 JavaScript)访问 Cookie,从而防止 XSS 攻击;
- 通过合理设置 Set-Cookie 的格式和属性,可以确保 Cookie 的安全性、有效性和可访问性,从而满足 Web 应用程序的需求。
8.2 HTTP Session
用户单独使用Cookie时,其私密数据在浏览器(用户端)保存,非常容易被人盗取,更重要的是,除了被盗取之外,用户的私密数据也会因此泄漏。而Session
可以对此问题进行一些改善。
-
定义:
HTTP Session 是服务器用来跟踪用户与服务器交互期间用户状态的机制。由于 HTTP 协议是无状态的(每个请求都是独立的),因此服务器需要通过 Session 来记住用户的信息。
-
工作原理:
- 当用户首次访问网站时,服务器会为用户创建一个唯一的
Session ID
,并通过Cookie
将其发送到客户端; - 客户端在之后的请求中会携带这个
Session ID
,服务器通过Session ID
来识别用户,从而获取用户的会话信息; - 服务器通常会将
Session
信息存储在内存、数据库或缓存中。
- 当用户首次访问网站时,服务器会为用户创建一个唯一的
-
安全性:
- 与
Cookie
相似,由于Session ID
是在客户端和服务器之间传递的,因此也存在被窃取的风险。 - 但是一般情况下,即使
Cookie
被盗取,用户也仅泄漏了一个Session ID
,私密信息暂时不会有泄露的风险。 Session ID
便于服务端进行客户端有效性的管理,例如检测异地登录。- 此外,可以通过 HTTPS 和设置合适的
Cookie
属性(如HttpOnly
和Secure
)来增强安全性。
- 与
-
超时和失效:
- Session 可以设置超时时间,当超过这个时间后,
Session
会自动失效。 - 服务器也可以主动使
Session
失效,例如当用户登出时。
- Session 可以设置超时时间,当超过这个时间后,
-
用途:
- 用户认证和会话管理、
- 存储用户的临时数据(如购物车内容)、
- 实现分布式系统的会话共享(通过将会话数据存储在共享数据库或缓存中)。
9. HTTPS协议原理——加密
9.1 HTTPS相关概念
-
HTTPS 是一个应用层协议,在 HTTP 协议的基础上引入了加密层。由于 HTTP 协议的内容是明文传输的,可能导致传输过程中被篡改,而 HTTPS 通过加密机制增强了安全性。
-
什么是加密:加密就是把明文(要传输的信息)进行一系列变换,生成密文;解密就是把密文再进行一系列变换,还原成明文。在这个加密和解密的过程中,往往需要一个或多个中间数据辅助进行这个过程,这样的数据称为密钥。
9.1.1 加密的原因
-
运行商劫持:
由于我们通过网络传输的任何数据包都会经过运营商的网络设备(如路由器、交换机等),运营商的网络设备可以解析出传输的数据内容并进行篡改。
例如,当用户点击"下载按钮"时,实际上是向服务器发送了一个HTTP请求,而获取到的HTTP响应包含了该APP的下载链接。如果运营商进行劫持,就可能发现这个请求是要下载"千千动听",于是自动将返回给用户的响应篡改成"QQ浏览器"的下载地址。
由于 HTTP 的内容是明文传输的,数据会经过路由器、Wi-Fi 热点、通信服务运营商、代理服务器等多个物理节点。如果信息在传输过程中被劫持,传输的内容就会完全暴露。劫持者甚至可以篡改传输的信息而不被通信双方察觉,这就是中间人攻击。因此,我们必须对信息进行加密以确保安全性。
所以,在互联网上,明文传输是比较危险的事情!
9.1.2 常见的加密方式
-
对称加密:
- 对称加密采用单钥密码系统的加密方法,同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密,也称为单密钥加密,其特点是加密和解密所用的密钥是相同的。
- 常见对称加密算法包括:DES、3DES、AES、TDEA、Blowfish、RC2等。
- 对称加密的特点是算法公开、计算量小、加密速度快、加密效率高。
- 本质上,对称加密是通过同一个“密钥”将明文加密为密文,并能将密文解密回明文。
-
非对称加密:
- 非对称加密需要两个密钥来进行加密和解密,这两个密钥是公开密钥(public key,简称公钥)和私有密钥(private key,简称私钥)。
- 常见非对称加密算法包括 RSA、DSA、ECDSA(了解即可)。
- 其特点是算法强度复杂,安全性依赖于算法与密钥,但由于算法复杂,加密解密速度不如对称加密快。
- 非对称加密使用配对的公钥和私钥,最大的缺点是运算速度较慢。
- 具体使用方式包括:通过公钥对明文加密生成密文,再通过私钥解密还原为明文;也可以反向使用,即通过私钥加密明文生成密文,再通过公钥解密密文还原为明文。
9.1.3 数据摘要(数据指纹)
- 数据摘要(数据指纹) :利用单向散列函数(Hash 函数)对信息进行运算,生成一串固定长度的散列值,这个散列值就是数据摘要。数字指纹并不是一种加密机制,但可以用来判断数据有没有被篡改。
- 常见的摘要算法包括 MD5、SHA1、SHA256、SHA512 等,这些算法将无限的数据映射成有限的摘要,因此可能存在碰撞(即两个不同的信息计算出相同的摘要,但概率极低)。
- 摘要与加密算法的区别在于,摘要严格来说不属于加密,因为它不可逆(无法从摘要反推原始信息),通常用于数据完整性校验和对比。
9.2 HTTPS的工作过程探究
接下来讨论几种加密方案,并展示其问题,在通过完善来帮助我们理解最后的https
协议。
9.2.1 方案一:只使用对称加密
如果通信双方都各自持有同一个密钥X,且没有别人知道,这两方的通信安全当然可以被保证的(除非密钥被破解)。
引入对称加密之后,即使数据被截获,由于黑客不知道密钥是啥,因此就无法进行解密,也就不知道请求的真实内容是啥了。
但事情没这么简单。服务器同一时刻其实是给很多客户端提供服务的,这么多客户端,每个人用的密钥都必须是不同的(如果是相同那密钥就太容易扩散了,黑客就也能拿到了),因此服务器就需要维护每个客户端和每个密钥之间的关联关系,这也是个很麻烦的事情。
比较理想的做法,就是能在客户端和服务器建立连接的时候,双方协商确定这次的密钥是啥。
但是如果直接把密钥明文传输,那么黑客也就能获得密钥了,此时后续的加密操作就形同虚设了。
因此密钥的传输也必须加密传输!
但是要想对密钥进行对称加密,就仍然需要先协商确定一个“密钥的密钥”,这就成了“先有鸡还是先有蛋”的问题了。此时密钥的传输再用对称加密就行不通了。
9.2.2 方案二:只使用非对称加密
鉴于非对称加密的机制,如果服务器先把公钥以明文方式传输给浏览器,之后浏览器向服务器传输数据前都先用这个公钥加密再传输,从客户端到服务器的信道看似安全(实际仍有风险),因为只有服务器持有对应的私钥能解密公钥加密的数据。
然而,服务器到浏览器的通信如何保障安全?如果服务器使用私钥加密数据并传输给浏览器,浏览器可以用公钥解密,但该公钥最初是通过明文传输的,若被中间人劫持,攻击者同样能用此公钥解密服务器传来的信息,导致安全漏洞。
9.2.3 方案三:双方都使用非对称加密
服务端拥有公钥 S 与对应的私钥 S’,客户端拥有公钥 C 与对应的私钥 C’。客户和服务端交换公钥后,通信流程如下:
- 客户端→服务端:先用 S 加密数据再发送,仅服务端能用 S’ 解密。
- 服务端→客户端:先用 C 加密数据再发送,仅客户端能用 C’ 解密。
这种方案虽然可行,但存在两个问题:
- 效率太低(非对称加密计算开销大);
- 仍有安全隐患(如无法防御中间人攻击或重放攻击)。
9.2.4 方案四:非对称加密+对称加密
- 服务端具有非对称公钥S和私钥S’。
- 客户端发起HTTPS请求,获取服务端公钥S;
- 客户端在本地生成对称密钥C,通过公钥S加密后发送给服务器。
- 由于中间的网络设备没有私钥,即使截获了数据也无法还原出原文(理论上无法获取对称密钥)。
- 服务器通过私钥S’解密,还原出对称密钥C,并使用该密钥加密返回的响应数据。
- 后续通信仅使用对称加密,因密钥仅客户端和服务端知晓,即使数据被截获也无意义。
- 由于对称加密效率远高于非对称加密,因此仅在初始密钥协商阶段使用非对称加密。
- 安全问题:虽然上述流程看似安全,但仍存在潜在风险。
9.2.5 中间人攻击——针对上面的四个方案
-
确实,在方案2/3/4中,客户端获取到公钥S之后,对客户端形成的对称秘钥X用服务端给客户端的公钥S进行加密,中间人即使窃取到了数据,此时中间人确实无法解出客户端形成的密钥X,因为只有服务器有私钥S’。
-
但是中间人的攻击,如果在最开始握手协商的时候就进行了,那就不一定了。
-
假设hacker已经成功成为中间人:
- 服务器具有非对称加密算法的公钥S,私钥S’;
- 中间人具有非对称加密算法的公钥M,私钥M’;
- 客户端向服务器发起请求,服务器明文传送公钥S给客户端;
- 中间人劫持数据报文,提取公钥S并保存好,然后将被劫持报文中的公钥S替换成为自己的公钥M,并将伪造报文发给客户端;
- 客户端收到报文,提取公钥M(自己当然不知道公钥被更换过了),自己形成对称秘钥X,用公钥M加密X,形成报文发送给服务器;
- 中间人劫持后,直接用自己的私钥M’进行解密,得到通信秘钥X,再用曾经保存的服务端公钥S加密后,将报文推送给服务器;
- 服务器拿到报文,用自己的私钥S’解密,得到通信秘钥X;
- 双方开始采用X进行对称加密,进行通信。但是一切都在中间人的掌握中,劫持数据,进行窃听甚至修改,都是可以的。
-
上面的攻击方案,同样适用于方案2、方案3。问题本质出在哪里了呢?客户端无法确定收到的含有公钥的数据报文,就是目标服务器发送过来的!
9.2.6 CA证书与数据签名
-
CA证书
-
服务端在使用 HTTPS 前,需要向 CA 机构申领一份数字证书,数字证书里含有证书申请者信息、公钥信息等。服务器把证书传输给浏览器,浏览器从证书里获取公钥就行了,证书就如身份证,证明服务端公钥的权威性。
-
-
这个证书可以理解成是一个结构化的字符串,里面包含了以下信息:
- 证书发布机构
- 证书有效期
- 公钥
- 证书所有者
- 签名
- …
-
需要注意的是:申请证书的时候,需要在特定平台生成,会同时生成一对密钥对,即公钥和私钥。这对密钥对就是用来在网络通信中进行明文加密以及数字签名的。其中公钥会随着 CSR 文件,一起发给 CA 进行权威认证,私钥服务端自己保留,用来后续进行通信(其实主要就是用来交换对称秘钥)。
-
-
数据签名
-
签名的形成是基于非对称加密算法的(注意:目前暂时和 HTTPS 没有关系,不要和 HTTPS 中的公钥私钥搞混)。
-
-
当服务端申请 CA 证书的时候,CA 机构会对该服务端进行审核,并专门为该网站形成数字签名,过程如下:
- CA 机构拥有非对称加密的私钥 A 和公钥 A’;
- CA 机构对服务端申请的证书明文数据进行
hash
,形成数据摘要; - 然后对数据摘要用 CA 私钥 A’加密,得到数字签名 S。
-
服务端申请的证书明文和数字签名 S 共同组成了数字证书,这样一份数字证书就可以颁发给服务端了。
-
当客户端获取到这个证书之后,会对证书进行校验(防止证书是伪造的),过程如下:
- 判定证书的有效期是否过期
- 判定证书的发布机构是否受信任(操作系统中已内置的受信任的证书发布机构)
- 验证证书是否被篡改——从系统中拿到该证书发布机构的公钥,对签名解密,得到一个
hash
值(称为数据摘要),设为hash1
,然后计算整个证书的hash
值,设为hash2
,对比hash1
和hash2
是否相等,如果相等则说明证书是没有被篡改过的。
-
9.2.7 方案五:非对称加密 + 对称加密 + 证书认证(HTTPS原理)
在客户端和服务器刚一建立连接的时候,服务器给客户端返回一个证书,证书包含了之前服务端的公钥,也包含了网站的身份信息。
HTTPS工作过程中涉及到的密钥有三组:
- 第一组(非对称加密)用于校验证书是否被篡改。服务器持有服务端私钥(服务器私钥在形成CSR文件与申请证书时获得),客户端持有CA公钥(操作系统包含了可信任的CA认证机构有哪些,同时持有对应的CA公钥)。服务器在客户端请求时返回携带签名(签名是通过CA私钥得来的)的证书,客户端通过这个CA公钥进行证书验证,保证证书的合法性,进一步保证证书中携带的服务端公钥权威性。
- 第二组(非对称加密)用于协商生成对称加密的密钥,客户端用收到的CA证书中的服务端公钥(是可被信任的)给随机生成的对称加密的密钥加密并传输给服务器,服务器通过服务端私钥解密获取到对称加密密钥。
- **第三组(对称加密)是客户端和服务器后续传输的数据都通过这个对称密钥加密解密。**其实一切的关键都是围绕这个对称加密的密钥,其他机制都是辅助这个密钥工作的——第二组非对称加密的密钥是为了让客户端把这个对称密钥安全的传给服务器,第一组非对称加密的密钥是为了让客户端拿到第二组非对称加密的公钥。
9.3 常见问题(重要)
9.3.1 为什么摘要内容在网络传输的时候一定要加密形成签名?
-
常见的摘要算法有 MD5 和 SHA 系列。以 MD5 为例:
我们不需要研究具体的计算签名的过程,只需要了解 MD5 的特点:定长(无论多长的字符串,计算出来的 MD5 值都是固定长度,如 16 字节或 32 字节版本)、分散(源字符串只要改变一点点,最终得到的 MD5 值都会差别很大)、不可逆(通过源字符串生成 MD5 很容易,但通过 MD5 还原原串理论上不可能)。正因为 MD5 有这样的特性,我们可以认为如果两个字符串的 MD5 值相同,则这两个字符串相同。
-
理解判定证书篡改的过程(类比判定身份证是否伪造):
假设证书是一个简单字符串 “hello”,其 MD5 哈希值为 “BC4B2A76B9719D91”。
如果 “hello” 被篡改为 “hella”,其 MD5 值会变为 “BDBD6F9CF51F2FD8”。
客户端验证时只需重新计算 “hello” 的哈希值,对比是否与服务器提供的哈希值一致即可。
但问题在于,黑客可能同时篡改字符串和哈希值,使客户端无法分辨。
因此,哈希值必须加密传输(即签名)。
具体流程为:CA 对证书明文(如 “hello”)生成哈希摘要,并用 CA 私钥加密形成签名,将明文和加密签名组合成证书颁发给服务端。客户端请求时,服务端返回该证书。中间人因无 CA 私钥,无法篡改或替换证书。客户端通过预存的 CA 公钥解密签名,还原原始哈希值进行校验,从而确保证书合法性。
9.3.2 为什么签名不直接加密,而是要先hash形成摘要?
- 缩小签名密文的长度,加快数字签名的验证签名的运算速度
9.3.3 如何成为中间人
- ARP 欺骗:在局域网中,黑客通过收到 ARP Request 广播包,能够偷听到其他节点的 (IP, MAC) 地址。例如,黑客收到两个主机 A、B 的地址后,告诉 B(受害者)自己是 A,使得 B 发送给 A 的数据包都被黑客截取。
- ICMP 攻击:由于 ICMP 协议中有重定向报文类型,攻击者可以伪造一个 ICMP 信息并发送给局域网中的客户端,伪装成更好的路由通路,导致目标的上网流量被重定向到指定接口,达到与 ARP 欺骗类似的效果。
- 假 Wi-Fi 与假网站:攻击者通过伪造公共 Wi-Fi 或仿冒合法网站,诱导用户连接或输入敏感信息,从而窃取数据。
9.3.4 中间人有没有可能篡改该证书
- 中间人篡改了证书的明文
- 由于中间人没有CA机构的私钥,因此无法对内容进行hash后用私钥加密形成签名,也就无法为篡改后的证书生成匹配的签名。
- 如果强行篡改证书,客户端在收到证书后会检测到明文与签名解密后的值不一致,从而判定证书已被篡改并不可信,进而终止向服务器传输信息,防止信息泄露给中间人。
9.3.5 中间人整个掉包证书
- 由于中间人没有CA的私钥,因此无法伪造有效的假证书(为什么?)。
- 中间人只能向CA申请真实证书,然后用自己的证书进行替换。
- 虽然这种方法可以实现证书的整体替换,但需要注意证书明文中包含域名等服务器认证信息,客户端仍能识别出这种替换。
- 请始终牢记:中间人没有CA私钥,因此无法对任何证书(包括自己的证书)进行合法修改。