目录
- 开头:
- 一.HTTP协议基础
-
- 1.认识HTTP
- 2.URL
-
- (1)结构解析
- [(2)DNS 域名 - IP 映射](#(2)DNS 域名 - IP 映射)
- [(3)urlencode 与 urldecode](#(3)urlencode 与 urldecode)
- 3.HTTP协议请求与响应格式
- 二.代码实战
- 三.Http请求方法
-
- [1. GET:获取资源](#1. GET:获取资源)
- 2.POST:提交数据
- 四.长连接与短链接
- [五.cookie 和 session](#五.cookie 和 session)
-
- [1. Cookie](#1. Cookie)
-
- (1)基本定义
- [(2)Set-Cookie 响应头完整属性详解](#(2)Set-Cookie 响应头完整属性详解)
- [(3)Cookie 请求头](#(3)Cookie 请求头)
- (4)Cookie安全问题
- 2.Session
- 六.HTTPS
-
- 1.先导知识
- 2.HTTPS工作过程探究
-
- [(1)方案 1:只使用对称加密](#(1)方案 1:只使用对称加密)
- [(2)方案 2:只使用非对称加密(服务器有公钥私钥)](#(2)方案 2:只使用非对称加密(服务器有公钥私钥))
- [(3)方案 3:双方都用非对称加密](#(3)方案 3:双方都用非对称加密)
- [(4)方案 4:非对称加密 + 对称加密(混合加密)](#(4)方案 4:非对称加密 + 对称加密(混合加密))
- [(5)非对称加密 + 对称加密 + CA 证书认证](#(5)非对称加密 + 对称加密 + CA 证书认证)
- [3.数字证书与 CA 认证体系](#3.数字证书与 CA 认证体系)
- 结尾
- 往期回顾
开头:
ok了,今天这一期我们来学习Linux网络部分中十分重要的应用层协议HTTP,并且来尝试写一个网页出来,废话不多说,我们直接开始。
一.HTTP协议基础
1.认识HTTP
HTTP(HyperText Transfer Protocol,超文本传输协议)是互联网中客户端(浏览器)与服务器之间通信的基石。它定义了双方如何交换超文本(HTML、图片、视频等资源)
HTTP协议有两个核心特点:

正是因为是无状态的,才衍生出了Cookie,Session等会话保持技术,后面会讲
2.URL
(1)结构解析
平时说的 "网址" 本质上就是 URL(Uniform Resource Locator,统一资源定位符)。一个完整的 URL 结构如下:

各部分的含义:

(2)DNS 域名 - IP 映射
HTTP 本身不做域名解析 ,HTTP 是应用层传输协议;DNS 专门负责域名 ↔ IP 映射,HTTP 依赖 DNS 拿到 IP 之后,才可以建立 TCP 连接发起请求
整体流程:(浏览器访问http://www.example.com)
- 用户输入域名
www.example.com,浏览器不知道该域名对应的 IP,无法直接建立 TCP 连接 - 浏览器发起DNS 查询,把域名翻译成服务器 IP 地址
- 拿到 IP 后,浏览器向这个 IP 的 80 端口(HTTP 默认) 发起 TCP 三次握手,建立连接
- 在 TCP 连接之上发送 HTTP 请求报文
- 服务器返回 HTTP 响应
关键点:DNS 解析发生在 HTTP 请求之前,HTTP 报文里面放的是域名(Host 头),而底层传输用的是 IP
简单来说,我们在浏览器上想要访问网页一般都是通过域名 ,但是网络连接需要的是IP地址 ,这时就要通过DNS进行 域名 -》IP地址的映射
(3)urlencode 与 urldecode
URL 中/、?、:、&、=等字符具有特殊含义,如果参数值中需要包含这些字符,就必须进行转义编码
编码规则 :将需要转码的字符转为十六进制,从右到左每 2 位为一组,前面加上%,格式为%XY

urldecode 就是 urlencode 的逆过程,将%XY还原为原始字符
3.HTTP协议请求与响应格式
请求格式


HTTP 请求由 请求行(首行)、请求头、空行、请求正文 四部分组成:
- 请求行
例如:

- 请求方法 :
POST,表示本次请求的动作类型,常见的还有 GET、PUT、DELETE 等 - 请求 URL :这里是完整的绝对 URL(通常出现在代理请求场景);普通浏览器请求一般只写路径(如
/companyLogin.do),域名由Host头补充 - HTTP 版本 :
HTTP/1.1,是目前应用最广泛的 HTTP 版本
- 请求报头
从第二行Host:开始,到Cookie行结束的所有键: 值格式的行,都是请求头,用来说明请求的附加属性、客户端信息、编码规则等。每行一个字段,冒号分隔名称与值
针对上述图片,相关对应的属性有:
Host: job.xjtu.edu.cn:目标服务器的域名,HTTP/1.1 强制必填,用于服务器区分同一 IP 下的多个网站Connection: keep-alive:请求完成后保持 TCP 连接复用,避免每次请求都重新握手Content-Length: 36:下方请求正文的字节长度,告诉服务器需要读取多少数据Content-Type: application/x-www-form-urlencoded:请求正文的格式,这里表示普通表单键值对格式Cookie: ...:客户端携带的身份凭证、状态信息,用于维持登录态User-Agent:客户端(浏览器)的版本、系统信息Referer:标识本次请求是从哪个页面跳转而来
-
空行
请求头全部结束后,必须跟一个只有换行符的空行 ,作用是标记请求头结束,下方开始是请求体,是协议规定的强制分隔符
-
请求正文
空行之后的内容就是请求体,是本次请求要发送给服务器的真实业务数据
响应格式


HTTP 响应的结构和请求高度对称,同样由 状态行(响应行)、响应报头、空行、响应正文 四部分组成
- 状态行
例如:

- HTTP版本
- 状态码 :3 位数字,标识请求的处理结果,是核心判断依据:

- 状态描述:是状态码的简短文本说明,和状态码一一对应
- 响应报头
和请求头格式完全一致,为键: 值逐行排列,用来说明响应的附加属性
常见字段:
Content-Type: text/html; charset=utf-8:响应正文的类型与编码,告诉浏览器如何解析内容Content-Length: 1024:响应正文的字节长度Set-Cookie: ...:服务器向客户端写入 CookieServer: nginx:服务器使用的软件信息Date: ...:响应生成的时间
- 空行
和请求一致,响应头和响应体之间必须有一个空行作为分隔标记 - 响应正文
空行之后的内容就是响应体,是服务器返回的真实业务数据,也是客户端最终需要的资源:
- 访问网页时:响应体是 HTML 源码
- 调用接口时:响应体通常是 JSON 数据
- 下载图片时:响应体是图片的二进制内容
核心总结:

二.代码实战
下面我们要基于应用层HTTP协议自己写一个网页出来

如上图,这是我们要创建的所有文件,下面我们直接开始
首先就是 Socket.hpp,这个和上一期我们封装的 socket 没有区别,我们直接拿来用:
cpp
#pragma once
//模板方法模式
#include"Log.hpp"
#include"Common.hpp"
#include<memory>
#include<sys/socket.h>
#include"InetAddr.hpp"
class Socket
{
static const int defaultbacklog = 8;
public:
virtual void SocketOrDie() = 0;
virtual void BindOrDie(uint16_t port) = 0;
virtual void ListenOrDie(int backlog) = 0;
virtual std::shared_ptr<Socket> AcceptOrDie(InetAddr* Client) = 0;
virtual void Close() =0;
virtual int Recv(std::string* out) =0;
virtual int Send(const std::string& message) =0;
virtual int Connect(std::string ip ,uint16_t port) =0;
public:
void BuildTcpSockfd(uint16_t port,int backlog = defaultbacklog)
{
SocketOrDie();
BindOrDie(port);
ListenOrDie(backlog);
}
void BuildClientSockfd(std::string ip ,uint16_t port)
{
SocketOrDie();
}
};
class TcpSockfd : public Socket
{
public:
TcpSockfd(): _sockfd(-1)
{}
TcpSockfd(int fd): _sockfd(fd)
{
}
void SocketOrDie() override
{
_sockfd = socket(AF_INET,SOCK_STREAM,0);
if(_sockfd<0)
{
LOG(LogLevel::FATAL)<<"socket err";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO)<<"socket success";
}
void BindOrDie(uint16_t port) override
{
InetAddr peer(port);
int n=bind(_sockfd,peer.Inet(),peer.Len());
if(n<0)
{
LOG(LogLevel::FATAL)<<"bind err";
exit(BIND_ERR);
}
LOG(LogLevel::INFO)<<"bind success";
}
void ListenOrDie(int backlog) override
{
int n = listen(_sockfd,backlog);
if(n<0)
{
LOG(LogLevel::FATAL)<<"Listen err";
exit(LISTEN_ERR);
}
LOG(LogLevel::INFO)<<"Listen success";
}
std::shared_ptr<Socket> AcceptOrDie(InetAddr* Client) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = accept(_sockfd,CONV(peer),&len);
if(fd<0)
{
LOG(LogLevel::FATAL)<<"accept err";
return nullptr;
}
Client->SetAddr(peer);
return std::make_shared<TcpSockfd>(fd);
}
int Recv(std::string* out) override
{
char buffer[4096*4];
ssize_t n = recv(_sockfd,buffer,sizeof(buffer)-1,0);
if(n>0)
{
buffer[n]=0;
*out += buffer;
}
return n;
}
int Send(const std::string& message) override
{
return send(_sockfd,message.c_str(),message.size(),0);
}
int Connect(std::string ip ,uint16_t port)
{
InetAddr peer(ip,port);
return connect(_sockfd,peer.Inet(),peer.Len());
}
void Close()
{
if(_sockfd>0) close(_sockfd);
}
private:
int _sockfd;
};
接着就是 TcpServer.hpp

如上图,基本的代码逻辑和之前我们写的差不多,关键是这里的回掉函数要从 Start()函数作为参数传入
下面我们来实现最重要的Http.hpp

如上图,对于请求和响应我们都封装成为一个类,成员变量就是上面我们在http请求与响应格式哪里介绍的相关元素,在 Http类中,我们直接调用 TcpServer.hpp中的 Start()函数,而HandlerHttpRequest()函数就是服务器要做的主要工作:
- 第一步:我们要接收来自浏览器来的请求
- 第二步:对请求进行反序列化,提取出相关的信息
- 第三步:拿到请求的目标html文件,并设置响应正文数据,以及相关报头
- 第四步:构建http响应,并由服务器发送回浏览器

接下来我们就要来实现这一个个函数
首先就是反序列化Deserialize()



以 \r\n为分隔符,提取出请求行,然后按照请求的结构提取出对应的元素,这里需要判断uri其中的/代表的是网页根目录,也就是当前工作目录中的一个目录,里面就是一些 html文件

接着就是在HttpResponse类中设置目标文件

然后就是SetTextData()设置响应正文的数据


下面是构建响应

这样我们就完成了基本的代码,运行起来看看
(这里html文件可以让AI帮忙写一份)

如上图,这样就完成了
现在我们点击登录,输入用户名以及密码:

点击 Login:

我们发现怎么网址多了这些??
这个login是什么??其实这里要讲一下 form表单
在登录文件login.html中,有一段这样的代码:

这就是 form表单 ,作用就是:收集页面用户输入的数据,交给浏览器,由浏览器封装成 HTTP 请求发送给服务器,其中method代表的是请求方法,此时是 GET方法
这样我们就可以改进我们的代码:

在Http类中添加:

在HttpRequest类中的反序列化中,要对从请求行得到的uri提取出参数


下面我们登录后就是这样的

Http.hpp
cpp
#pragma once
#include"TcpServer.hpp"
#include"Log.hpp"
#include<unordered_map>
#include"util.hpp"
const std::string gspace = " "; //空格
const std::string glinespace = "\r\n"; //空行
const std::string glinesep = ": ";
const std::string webroot = "./webroot";
const std::string homeage = "/index.html";
const std::string err_404 = "/404.html";
//请求
class HttpRequest
{
public:
void ParseReqLine(std::string& reqline)
{
std::stringstream ss(reqline);
ss>> _method >> _uri >> _version;
}
void Deserialize(std::string& req_str)
{
std::string reqline; //请求行
bool ret = Tool::GetLineMessage(req_str,reqline,glinespace); //提取第一行(请求行)
(void)ret;
//对收到的请求做反序列化
ParseReqLine(reqline);
if(_uri=="/")
_uri = webroot + homeage; //网站首页
else
_uri = webroot + _uri;
//uri = ./wwwroot/login.html/login?username=zhansan & password =123456
std::string sep = "?";
auto pos = _uri.find(sep);
if(pos == std::string::npos) return;
_is_interact = true;
_text = _uri.substr(pos+sep.size()); //提取参数
_uri = _uri.substr(0,pos);
}
std::string Uri() { return _uri; }
bool IsInteract() { return _is_interact; }
std::string Args() { return _text; }
private:
std::string _method; //请求方法
std::string _uri; //uri
std::string _version; //http版本
std::unordered_map<std::string,std::string> _headers; //请求报头
std::string _blankline = glinespace; //空行
std::string _text; //请求正文
bool _is_interact = false;
};
//响应
class HttpResponse
{
public:
void SetFilename(std::string filename)
{
_targetfile = filename;
}
//设置状态码
void SetCode(int code)
{
_code = code;
switch(_code)
{
case 100:
_desc = "err";
break;
case 404:
_desc ="Not Found";
_targetfile = webroot + err_404;
Tool:: ReadFileContent(_targetfile,_text);
break;
default:
LOG(LogLevel::WARNING)<< "未知状态码";
break;
}
}
//设置报头
void SetHeader(const std::string& key,const std::string& val)
{
if(!_headers.count(key))
{
_headers[key] = val;
}
}
void SetTextData(std::string& text)
{
_text = text;
}
void SetTextData()
{
_text="";
bool ret = Tool:: ReadFileContent(_targetfile,_text); //读取文件内容
if(!ret)
{
LOG(LogLevel::FATAL)<<"文件打开失败";
SetCode(404);
}
else
{
LOG(LogLevel::INFO)<< "成功读取文件:"<< _targetfile;
}
int filesize = Tool:: GetFileSize(_targetfile);
SetHeader("Content-Length",std::to_string(filesize));
std::string type = Tool:: GetFileType(_targetfile);
SetHeader("Content-Type",type);
}
//构建响应
std::string Serialize()
{
std::string resp_line = _version + gspace + std::to_string(_code) + gspace + _desc + glinespace; //状态行
std::string header_line;
for(auto& header: _headers)
{
header_line +=(header.first + glinesep + header.second + glinespace);
}
return resp_line + header_line + blankline + _text;
}
public:
std::string _version; //http版本
int _code; //状态码
std::string _desc; //状态码描述
std::unordered_map<std::string,std::string> _headers; //响应报头
std::string blankline = glinespace; //空行
std::string _text; //响应正文
std::string _targetfile;
};
using func_t = std::function<void(HttpRequest&,HttpResponse&)>;
class Http
{
public:
Http(uint16_t port)
:_ts(std::make_unique<TcpServer>(port))
{}
void HandlerHttpRequest(std::shared_ptr<Socket>& sock, InetAddr& peer)
{
//服务器要来接收来自浏览器的请求
std::string HttpReq_buffer; //定义缓冲区
int n = sock->Recv(&HttpReq_buffer);
if(n>0)
{
//读取成功 ,但目前不能保证是否读到完整的请求
HttpRequest req;
HttpResponse resp;
//1.反序列化
req.Deserialize(HttpReq_buffer);
//判断是否需要交互
if(req.IsInteract())
{
//需要交互
if(!_server.count(req.Uri()))
{
LOG(LogLevel::WARNING)<< req.Uri() <<"不存在";
resp.SetCode(404);
}
else
{
_server[req.Uri()](req,resp);
}
}
else
{
resp._code = 200;
resp._desc = "OK";
resp.SetFilename(req.Uri());
resp.SetTextData();
}
//构建响应
resp._version = "HTTP/1.1";
std::string resp_message = resp.Serialize();
//发送
sock->Send(resp_message);
sock->Close();
}
}
void Start()
{
_ts->Start([this](std::shared_ptr<Socket>& sock, InetAddr& peer){
this->HandlerHttpRequest(sock,peer);
}
);
}
void BuildServerMethod(const std::string& method,const func_t& func)
{
std::string key = webroot + method;
if(_server.count(key))
{
LOG(LogLevel::WARNING)<< key <<"已经存在";
return ;
}
_server[key] = func;
LOG(LogLevel::INFO)<< "成功添加方法:"<< key;
}
private:
std::unique_ptr<TcpServer> _ts;
std::unordered_map<std::string,func_t> _server;
};
util.hpp
cpp
#pragma once
#include<string>
#include<iostream>
#include<fstream>
class Tool
{
public:
static bool GetLineMessage(std::string& buffer,std::string& message,std::string sep)
{
auto pos = buffer.find(sep);
if(pos == std::string::npos) return false;
message = buffer.substr(0,pos);
buffer.erase(pos);
return true;
}
static bool ReadFileContent(std::string& filename,std::string& text)
{
//用二进制读取
int filesize = GetFileSize(filename);
if(filesize > 0)
{
std::ifstream in(filename);
if(!in.is_open()) return false;
text.resize(filesize);
in.read((char*)text.c_str(),filesize);
in.close();
}
return true;
}
static int GetFileSize(std::string& filename)
{
std::ifstream in(filename,std::ios::binary); //以二进制形式打开,防止\r\n影响结果
if(!in.is_open()) return -1;
in.seekg(0,in.end);
int filesize = in.tellg();
in.seekg(0,in.beg);
in.close();
return filesize;
}
static std::string GetFileType(std::string& filename)
{
// .wwwroot/a/b/xxx.html
auto pos = filename.rfind(".");
if(pos == std::string::npos) return "text/html";
std::string type = filename.substr(pos);
if(type == ".html" || type == ".htm") return "text/html";
else if(type == ".jpg") return "image/jpeg";
else if(type == "png") return "image/png";
else return "";
}
};
三.Http请求方法
HTTP 请求方法(也叫「请求动词」)是客户端向服务器发起请求时,用来声明本次请求的意图和操作类型的核心标识。服务器会根据不同的方法,对同一个 URL 执行完全不同的操作

补充说明:LINK 和 UNLINK 在 HTTP/1.1 中已被正式废弃,生产环境几乎不会使用;TRACE 和 CONNECT 属于网络运维 / 代理层面的方法,业务开发很少直接编写。日常开发、面试、手写服务器,核心掌握前 6 种即可,重中之重是 GET 和 POST
1. GET:获取资源
GET 是 HTTP 协议中最基础、使用频率最高 的方法,语义非常纯粹:向服务器请求获取 URL 指定的资源。服务器收到 GET 请求后,解析对应的资源,将内容放在响应体中返回
标准请求示例:

带参数的 GET 请求示例(参数拼接在 URL 后):

核心特性
- 语义:读操作:理论上只获取数据,不修改服务器上的任何资源,是「安全」的方法
- 幂等性:同一个 GET 请求执行 1 次和执行 100 次,服务器的状态和返回结果完全一致
- 参数传递方式 :所有参数以**查询字符串(Query String)**的形式拼接在 URL 末尾,格式为
?key1=value1&key2=value2 - 长度限制:因为参数在 URL 中,而浏览器、服务器、代理都会对 URL 长度做限制(通常 2KB~8KB),因此 GET 不适合传输大量数据
在我们手写的 HTTP 服务器中,静态资源访问就是标准的 GET 场景:
- 解析请求行拿到 URL 路径
- 将 URL 映射到
wwwroot目录下的本地文件 - 读取文件内容,构造响应返回
- 当 URL 中检测到
?查询参数时,判定为交互型请求,走路由分发逻辑
2.POST:提交数据
POST 的核心语义是向服务器提交实体主体(Entity Body) ,通常用来让服务器处理提交的数据并产生结果。数据放在请求体中传输,不会暴露在 URL 里
标准请求示例:

核心特性
- 语义:写操作:通常会新增、修改服务器上的数据,改变服务端状态
- 非幂等:同一个 POST 请求提交多次,可能会产生多条数据、多次扣款等不同结果(比如重复提交订单)
- 参数传递方式:数据全部放在**请求体(Body)**中,URL 上不可见
- 数据大小:理论上没有长度限制,可以传输海量数据(比如上传 GB 级文件)
两者的对比总结:

四.长连接与短链接
HTTP 是应用层协议,它的底层传输完全依赖 TCP 协议。而每建立一个 TCP 连接,都会有对用的握手开销
如果每发一个 HTTP 请求都要新建一个 TCP 连接,握手挥手的开销会非常大。长连接和短连接,本质上就是「一个 TCP 连接上能承载多少个 HTTP 请求」的复用策略:
短链接:
一个 TCP 连接仅处理一次HTTP 请求 - 响应交互,服务器返回响应后立即关闭 TCP 连接。下一次请求必须重新经历三次握手建立新连接
Connection: close:请求 / 响应完成后,关闭 TCP 连接

长连接:
TCP 连接建立后保持打开状态,多个 HTTP 请求 - 响应可以复用同一个 TCP 连接,直到满足关闭条件(空闲超时、达到最大请求数、某一方主动要求关闭)才断开连接
Connection: keep-alive:请求 / 响应完成后,保持TCP 连接,继续复用

五.cookie 和 session
HTTP 协议天生是无状态、无连接 的 ------ 每个请求之间完全独立,服务器默认无法知道「连续两次请求是不是同一个用户发的」。而我们日常上网的登录状态、购物车、个性化推荐,都依赖服务器识别用户身份。Cookie 和 Session 就是为了解决 HTTP 无状态问题、实现会话保持而生的两大核心机制
为什么需要 Cookie 和 Session?
HTTP 协议的设计初衷只是传输超文本文档,本身不保存任何状态信息:
- 你第一次请求 B 站首页,第二次请求 B 站登录接口,服务器默认认为这是两个完全无关的请求
- 没有额外机制的话,服务器永远不知道你是谁、有没有登录过
会话保持的核心需求:在用户连续访问网站的一段时间(一个会话)内,服务器能够持续识别用户身份,保存用户的相关状态
为了解决这个问题,先后诞生了两种主流方案:
- Cookie :把状态数据存在客户端(浏览器),每次请求自动带给服务器
- Session :把状态数据存在服务器端,只给客户端发一个唯一标识(Session ID),通过标识匹配用户状态
1. Cookie
(1)基本定义
Cookie(也叫 Web Cookie、浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据。浏览器在之后向同一服务器发起请求时,会自动将 Cookie 携带在请求头中发送给服务器
核心作用:告知服务器「多个请求是否来自同一个浏览器」,从而实现保持登录状态、记录用户偏好等功能

第一次用户在浏览器上填写登录信息后,浏览器向服务器发送相对应的登录信息,然后服务器进行处理认证,通过后向浏览器返回,携带 Set-Cookie报头,随后在浏览器上创建Cookie,后续的所有请求都会自动携带Cookie
关键点:Cookie 是服务器下发、浏览器存储、自动携带。整个过程浏览器自动完成,前端代码不需要手动拼接
(2)Set-Cookie 响应头完整属性详解
服务器通过Set-Cookie响应头向浏览器写入 Cookie,这是 Cookie 的核心配置入口。完整格式:

每个属性的作用逐一详解:
① name=value(必填)
Cookie 的名称和值,是 Cookie 的核心内容
- 示例:
username=zhangsan - 如果名称或值包含空格、分号、逗号等特殊字符,需要进行 URL 编码
- 一个响应头可以写多个
Set-Cookie,同时写入多个 Cookie
② expires /max-age(过期时间)
设置 Cookie 的失效时间,不写就是会话 Cookie
expires:指定一个具体的过期时间点,必须遵循RFC 1123 标准格式
示例:expires=Thu, 18 Dec 2024 12:00:00 UTCmax-age:指定多少秒后过期,相对时间,优先级高于 expires
示例:max-age=3600表示 1 小时后过期
RFC 1123 时间格式细节 :
格式:星期几, 两位日期 月份 四位年份 时:分:秒 UTC
示例:Tue, 01 Jan 2030 12:34:56 GMT
③ path(路径作用域)
限制 Cookie 只能在指定路径下的请求中被发送,默认是设置 Cookie 时的当前路径
- 示例:
path=/a/b- 访问
/a/b/page.html→ 浏览器会自动携带这个 Cookie - 访问
/a/x.html→ 不会携带 - 访问
/index.html→ 不会携带
- 访问
- 通常设置为
path=/,表示网站根路径下所有页面都能使用这个 Cookie
④ domain(域名作用域)
指定哪些域名可以接收这个 Cookie,默认是设置 Cookie 的当前主机名
- 示例:
domain=.example.com前面带点,表示所有子域名都能用www.example.com能用到api.example.com也能用到
- 不加点的话,只能精确匹配当前域名
- 安全限制:不能设置其他域名的 Cookie,浏览器会拒绝
⑤ secure(安全传输)
标记后,这个 Cookie只能通过 HTTPS 协议发送,HTTP 明文连接下不会携带。
- 作用:防止 Cookie 在网络传输中被中间人截获
- 生产环境涉及登录的 Cookie 建议都加上
⑥ HttpOnly(禁止脚本访问)
标记后,这个 Cookie不能被客户端 JavaScript(document.cookie)读取,只能由浏览器自动在 HTTP 请求中携带。
- 核心作用:**防止跨站脚本攻击(XSS)**窃取 Cookie
- 这是保护会话 Cookie 最重要的安全属性之一,登录 Session ID 强烈建议开启
(3)Cookie 请求头
浏览器向服务器发送请求时,会自动把符合作用域的 Cookie 放到Cookie请求头中:

- 多个 Cookie 之间用
;(分号 + 空格)分隔 - 只有 name 和 value,不会带 expires、path 等属性(那些是浏览器本地管理用的)
(4)Cookie安全问题
Cookie 存储在客户端,天生存在安全风险:
- 可篡改:用户可以在浏览器开发者工具里手动修改 Cookie 值
- 可窃取:XSS 攻击可以盗取未设置 HttpOnly 的 Cookie
- 可伪造:攻击者可以构造假 Cookie 发送给服务器
- 隐私问题:第三方 Cookie 可以跨网站跟踪用户浏览行为
所以绝对不能把敏感信息(密码、身份证号)直接存在 Cookie 里。敏感状态要存在服务端,这就引出了 Session
2.Session
Session(会话)是服务器端用来跟踪用户状态的机制 。服务器为每个浏览器(用户)创建一个独立的 Session 对象,保存用户的状态数据,同时生成一个唯一的Session ID,通过 Cookie 下发给客户端
后续请求中,客户端只携带 Session ID,服务器通过 ID 找到对应的 Session 对象,就能识别用户身份和状态

核心思想:真实数据存在服务器,客户端只存一个无意义的唯一标识。哪怕标识被窃取,用户的真实敏感信息也不会直接泄露
六.HTTPS
HTTPS(Hyper Text Transfer Protocol Secure,超文本传输安全协议)并不是一个全新的应用层协议,而是在 HTTP 协议的基础上增加了一层 SSL/TLS 加密层
HTTP 本身是明文传输,数据在网络中裸奔,而 HTTPS 通过加密、认证、完整性校验三大机制,解决了 HTTP 传输中的安全问题
1.先导知识
(1)为什么需要HTTPS
HTTP 协议所有内容都是明文文本传输,数据从客户端到服务器的过程中,会经过路由器、WiFi 热点、运营商基站、代理服务器等无数个物理节点。任何一个中间节点都可以读取、篡改、劫持传输的内容,而通信双方完全无法察觉
典型风险:运营商劫持
最常见的例子就是下载劫持:

- 用户点击「天天动听」的下载按钮,本质是发送 HTTP 请求获取下载链接
- 运营商的网络设备解析出请求内容,发现是下载 APP 的响应
- 直接把响应里的下载链接篡改成「QQ 浏览器」的下载地址
- 用户收到的页面看起来一切正常,但下载的东西已经被掉包了

这就是典型的中间人攻击(Man-in-the-Middle, MITM):中间人不中断通信,只是悄悄篡改、窃听数据,收发双方都感知不到
更严重的风险
- 隐私泄露:登录账号密码、银行卡信息、聊天记录被中间人窃取
- 内容篡改:网页被植入广告、恶意代码、钓鱼链接
- 身份冒充:劫持会话 Cookie,冒充用户操作
- 流量劫持:强制跳转到其他网站,注入推广内容
正是因为 HTTP 明文传输的这些天然缺陷,才诞生了 HTTPS------ 核心目标就是:让传输的内容只有通信双方能看懂,中间人截获了也解不开、改不了、冒充不了
(2)密码学基础概念
在讲 HTTPS 之前,必须先搞懂四类核心密码学概念,这是 HTTPS 的构建基石
a.对称加密
加密和解密使用同一个密钥 的加密方式,也叫单密钥加密
- 明文 + 密钥(加密) → 密文
- 密文 + 密钥(解密) → 明文
常见算法
DES、3DES、AES、Blowfish、RC2 等,其中 AES 是现在的主流标准
特点
- ✅ 算法公开、计算量小、加密速度极快、效率高,适合加密大量数据
- ❌ 密钥分发困难:通信前双方怎么安全地交换密钥?密钥本身如果明文传,会被中间人截获
b.非对称加密
需要一对密钥:公开密钥(公钥)和私有密钥(私钥)。公钥和私钥是配对生成的,用公钥加密的内容,只能用对应的私钥解密;反过来用私钥加密的内容,只能用对应的公钥解密
常见算法
RSA、DSA、ECDSA 等
特点
- ✅ 解决了密钥分发问题:公钥可以公开随便发,私钥自己留着,不用传输
- ❌ 算法复杂、计算量大、加密解密速度非常慢,比对称加密慢几个数量级
c.数据摘要(哈希 / 数字指纹)
通过单向散列函数(Hash 函数),对任意长度的信息运算,生成一串固定长度的摘要字符串
三大核心特性
- **定长:**无论输入多长,输出长度固定(比如 SHA256 永远是 256 位)
- **分散:**输入哪怕只改一个字符,输出摘要都会天差地别
- 不可逆:从摘要几乎不可能反推出原始内容
作用
不是加密,而是用来校验数据有没有被篡改------ 如果两份数据的摘要相同,就认为数据完全一致
d.数字签名
「数据摘要 + 私钥加密」就得到了数字签名。简单说就是:用私钥给数据的摘要加密,附在数据后面一起发出去。
验证过程
接收方收到「数据 + 数字签名」后:
- 用发送方的公钥解密签名,得到原始摘要 hash1
- 对收到的数据重新计算摘要,得到 hash2
- 对比 hash1 和 hash2,一致则说明数据完整、未被篡改,且确实是持有私钥的人发的
作用
- 防篡改:改了数据,摘要就对不上
- 防冒充:没有私钥的人造不出合法的签名
数字签名是 HTTPS 证书体系的核心原理
2.HTTPS工作过程探究
(1)方案 1:只使用对称加密
客户端和服务器提前约定好同一个对称密钥,所有请求响应都用这个密钥加密

问题:
- 密钥无法安全分发:如果密钥明文传输,中间人直接截获密钥,后续加密形同虚设
- 密钥管理灾难:服务器要服务海量客户端,每个客户端密钥都不一样,管理成本极高
- 鸡生蛋问题:要加密传输密钥,又需要先有一个加密密钥,陷入死循环
结论:只靠对称加密,解决不了密钥传输的安全问题
(2)方案 2:只使用非对称加密(服务器有公钥私钥)
服务器保留私钥,把公钥明文发给客户端;客户端发数据用公钥加密,服务器用私钥解密

问题
- 单向安全:只有客户端→服务器方向是安全的。服务器回数据如果用私钥加密,中间人也能用公钥解密(因为公钥是公开的)
- 速度极慢:非对称加密效率很低,所有数据都用非对称加密,网页加载会慢到无法接受
结论:只用一套非对称加密,反向传输不安全,且性能不达标
(3)方案 3:双方都用非对称加密
客户端和服务器各有一套公钥私钥,双方交换公钥;
- 客户端发数据:用服务器公钥加密,只有服务器能解
- 服务器发数据:用客户端公钥加密,只有客户端能解

问题
- 效率极低:两次非对称加密,速度更慢
- 还是没解决身份问题:中间人依然可以在交换公钥的阶段,把两边的公钥都换成自己的,实现中间人攻击
结论:非对称加密越多越慢,而且根上解决不了「公钥被掉包」的身份认证问题
(4)方案 4:非对称加密 + 对称加密(混合加密)
非对称加密慢,但安全;对称加密快,但密钥难传。那就用非对称加密来传对称密钥,后续所有数据用对称加密传输------ 兼顾安全和效率

优点
- 只有密钥协商阶段用非对称加密,数据传输用对称加密,性能大幅提升
- 中间人截获了加密后的密钥,因为没有私钥解不开,拿不到对称密钥
致命漏洞:中间人攻击(MITM)
这个方案有一个根上的问题:客户端无法确认收到的公钥,真的是目标服务器的,而不是中间人的。
中间人完整攻击流程:
- 服务器有公钥 S、私钥 S';中间人有自己的公钥 M、私钥 M'
- 客户端发起请求,服务器返回公钥 S
- 中间人劫持报文,把公钥 S 替换成自己的公钥 M,发给客户端
- 客户端以为拿到了服务器的公钥,生成对称密钥 X,用公钥 M 加密后发出去
- 中间人截获,用自己的私钥 M' 解密,得到对称密钥 X;再用服务器公钥 S 加密 X,发给服务器
- 服务器用私钥 S' 解密,也得到对称密钥 X
最终结果:客户端和服务器都觉得连接正常,双方用 X 加密通信,但中间人也持有 X,所有数据都能解密窃听、篡改
问题本质:公钥传输阶段没有身份认证,客户端不知道公钥是谁的,中间人可以轻松掉包
(5)非对称加密 + 对称加密 + CA 证书认证
这就是 HTTPS 的最终方案。既然公钥容易被掉包,那就引入一个权威第三方机构(CA,证书认证机构),给服务器的公钥做担保,证明「这个公钥确实属于这个网站」。
客户端不再直接接收裸公钥,而是接收服务器的数字证书,证书里有公钥、域名、有效期,还有 CA 的数字签名。客户端通过验证证书的合法性,来确认公钥的真实性,从根源上解决中间人攻击
3.数字证书与 CA 认证体系
数字证书就相当于网站的「身份证」,是由权威 CA 机构颁发的、证明网站身份的电子文件

证书里主要包含:
- CA 的数字签名
- 证书颁发机构(CA)
- 证书有效期
- 扩展信息
- 网站域名
- 网站的公钥等
证书是怎么签发出来的?

- 服务器生成密钥对:网站运营方自己生成一对公钥和私钥,私钥自己严格保存
- 生成 CSR 请求:把公钥、域名、企业信息等打包成证书请求文件(CSR)
- 提交给 CA 审核:CA 验证申请者确实拥有这个域名
- CA 签发证书 :
- CA 对证书的明文信息做哈希,得到摘要
- 用 CA 自己的私钥对摘要加密,得到数字签名
- 把「证书明文 + 数字签名」拼在一起,就是完整的数字证书
- 服务器部署证书:把证书和私钥部署在服务器上
客户端怎么验证证书?
浏览器 / 操作系统里内置了所有受信任 CA 的公钥。收到服务器的证书后,会做四重校验:
- 校验有效期
检查证书是否在有效期内,过期则直接不信任 - 校验颁发机构是否受信任
查看证书是哪个 CA 发的,系统里有没有这个 CA 的根证书。如果是不知名的 CA 签发的,浏览器会弹出「不安全」警告 - 校验证书是否被篡改(核心:数字签名验证)
- 从系统中取出该 CA 的公钥
- 用 CA 公钥解密证书里的数字签名,得到 CA 计算的原始摘要 hash1
- 对证书的明文内容重新计算哈希,得到 hash2
- 对比 hash1 和 hash2:
- 一致:证书完整,没被篡改
- 不一致:证书被改过,无效
为什么中间人改不了证书?
中间人可以修改证书里的域名、公钥,但他没有 CA 的私钥,没法生成对应的数字签名。只要一改,客户端重新算摘要就会和签名解密后的结果对不上,立刻就能发现
- 校验域名是否匹配
检查证书里的域名和当前访问的域名是否一致。
为什么中间人不能整个掉包证书?
中间人可以自己去 CA 申请一个合法证书,但证书里的域名是他自己的,和用户访问的域名不匹配。浏览器检测到域名不一致,同样会弹出安全警告

结尾
ok了,今天这一期学习就到这里了,如果对你有帮助,感谢你的关注与点赞,我主页里有更好康的呦!
