本文继续深入HTTP协议,从HTTP/1.0的短连接机制入手,分析频繁建立和断开 TCP 连接所带来的性能开销,以及HTTP/1.1如何通过持久连接机制减少重复连接,提高网络通信效率。随后,进一步探讨HTTP的无连接与无状态特性,并由此引出Cookie和Session两种常见的状态管理机制。重点讲解Cookie的工作原理、常见分类及潜在的安全风险,再结合实际应用,分析Session如何与Cookie相互配合,实现用户身份识别与会话状态保持,帮助大家更深入地理解HTTP背后的通信机制。
目录
[2.1 HTTP/1.0与短连接:频繁建立连接的性能开销](#2.1 HTTP/1.0与短连接:频繁建立连接的性能开销)
[2.2 HTTP/1.1长连接机制:从连接复用到Connection首部](#2.2 HTTP/1.1长连接机制:从连接复用到Connection首部)
[2.2.1 为什么需要长连接:它是如何降低通信开销的?](#2.2.1 为什么需要长连接:它是如何降低通信开销的?)
[2.2.2 Connection首部详解:如何控制连接保持与关闭](#2.2.2 Connection首部详解:如何控制连接保持与关闭)
[2.2.3 长连接的实现关键:完整报文读取与多次请求处理](#2.2.3 长连接的实现关键:完整报文读取与多次请求处理)
[2.3 短连接与长连接对比:工作方式与适用场景](#2.3 短连接与长连接对比:工作方式与适用场景)
[3.1 如何理解HTTP的"无连接":与长连接是否矛盾?](#3.1 如何理解HTTP的“无连接”:与长连接是否矛盾?)
[3.2 如何理解HTTP的"无状态":服务器为什么不记得你?](#3.2 如何理解HTTP的“无状态”:服务器为什么不记得你?)
[4.1 Cookie为什么会出现:从HTTP无状态的局限说起](#4.1 Cookie为什么会出现:从HTTP无状态的局限说起)
[4.2 Cookie是什么:基本概念与作用](#4.2 Cookie是什么:基本概念与作用)
[4.3 Cookie的工作原理:从服务器下发到客户端携带](#4.3 Cookie的工作原理:从服务器下发到客户端携带)
[4.4 Cookie的分类:不同类型与生命周期](#4.4 Cookie的分类:不同类型与生命周期)
[4.5 Cookie的安全隐患:信息泄露与身份冒用风险](#4.5 Cookie的安全隐患:信息泄露与身份冒用风险)
[5.1 Session的基本原理:服务器如何识别用户身份](#5.1 Session的基本原理:服务器如何识别用户身份)
[5.2 Session就一定安全吗:会话标识泄露带来的风险](#5.2 Session就一定安全吗:会话标识泄露带来的风险)
[5.3 如何提高会话安全性:从会话保护到异常检测](#5.3 如何提高会话安全性:从会话保护到异常检测)
[5.4 Cookie与Session总结:会话管理与会话保持机制](#5.4 Cookie与Session总结:会话管理与会话保持机制)
一、HTTP请求方法补充:认识其他常见的请求方式
前面三篇文章中,我们已经陆续讲解了HTTP协议中一些比较常见的请求方法,比如GET和POST。不过,HTTP的请求方法可不止这两个,还有几个我们之前没有详细展开。
既然如此,这一篇就先把剩下的几个方法补充一下,也算是给前面的内容收个尾。
-
CONNECT方法: 主要用于建立网络隧道,常见于HTTP代理场景。比如,浏览器通过代理访问HTTPS网站时,可以使用CONNECT请求,让代理服务器与目标服务器建立连接,随后在这条隧道中传输数据。
-
PUT 方法: 主要用于创建或整体替换指定的资源。虽然我们平时接触POST比较多,但PUT在RESTful API中同样很常见,尤其适合资源更新等操作。至于文件上传,也可以通过PUT实现。
-
HEAD方法: 和GET非常相似,区别在于服务器只返回响应头,不返回响应体。比如,我们想查看某个资源的大小、类型或最后修改时间,却不想把整个文件下载下来,就可以考虑使用HEAD。
-
DELETE 方法: 顾名思义,就是请求服务器删除指定的资源。不过,这并不意味着客户端发送一个DELETE请求,就能随意删除服务器上的文件。实际开发中,服务器通常会结合身份认证、权限校验等机制,决定是否允许删除操作。
-
OPTIONS方法: 主要用于查询目标资源支持哪些通信选项,比如允许使用哪些 HTTP 请求方法。另外,在浏览器的跨域请求中,OPTIONS还经常用于CORS预检请求,提前确认服务器是否允许后续的跨域操作。
看到这里,可能有人会问了:既然OPTIONS可以用来查询服务器支持哪些请求方法,那我们该怎么测试呢?
其实很简单,可以直接使用curl工具向服务器发送OPTIONS请求。
例如:
bash
curl -i -X OPTIONS http://127.0.0.1:8080/
这里的几个参数分别表示:
-
-i**:** 显示服务器返回的HTTP响应头。
-
-X OPTIONS**:** 指定本次请求使用OPTIONS方法。
-
http://127.0.0.1:8080/****: 要测试的服务器地址,可以根据自己的实际情况进行修改。
如果服务器支持OPTIONS,并通过Allow响应头告知允许的方法,那么我们可能会看到类似下面的响应:
bash
HTTP/1.1 204 No Content
Allow: GET, HEAD, POST, OPTIONS
Content-Length: 0
其中,Allow字段表示目标资源允许使用的HTTP请求方法。
不过,这里需要注意:并不是所有服务器都会在OPTIONS响应中返回 Allow****字段,也不是所有服务器都开放了OPTIONS请求。
具体能否使用,还得看服务器的实现和配置。
另外,OPTIONS并不是一种默认就应该禁用的请求方法。比如,在需要处理跨域请求的 Web 服务中,它就可能发挥重要作用。
好了,到这里,HTTP中几种常见的请求方法就基本介绍完了。 虽然它们的用途各不相同,但本质上都是客户端向服务器表达操作意图的一种方式。至于服务器最终如何处理请求,还需要结合具体的业务逻辑来决定。

二、从短连接到长连接:HTTP连接管理机制详解
2.1 HTTP/1.0与短连接:频繁建立连接的性能开销
在HTTP/1.0时代,**短连接是HTTP通信中最常见的连接方式。**为什么呢?
因为早期的互联网还没有现在这么发达,网页内容相对简单,大多数页面主要以文字为主,图片、视频等资源也比较少。在这样的环境下,使用短连接基本能够满足当时的网络通信需求。
那么,什么是短连接呢?所谓的短连接,就是客户端与服务器建立TCP连接后,完成一次HTTP请求与响应交互,随后关闭这条连接。
比如,浏览器向服务器发送一个HTTP请求,服务器收到请求后,完成对应的业务处理,再将响应返回给浏览器。等这次交互结束,TCP连接也就关闭了。
这种每次交互都需要重新建立连接的通信方式,我们通常称为短连接模式。

不过,随着互联网不断发展,网页内容越来越丰富,短连接的弊端也逐渐暴露出来了。
举个简单的例子。假设我们现在要访问一个网页,这个网页包含1个HTML文件和3张图片。
浏览器想要把这个页面完整展示出来,就需要分别获取 HTML 文件和对应的图片资源。也就是说,至少需要发起4次HTTP请求。
如果使用短连接,那么这4次请求就需要分别建立和关闭TCP连接。
对于每一次新的连接,服务器都需要经历类似下面的处理过程:
-
建立TCP连接: 客户端与服务器通过三次握手建立连接。
-
接收客户端连接: 服务器通过accept()获取已经建立的客户端连接。
-
处理HTTP请求: 服务器读取请求报文,解析请求内容,并完成对应的业务处理。
-
返回响应并关闭连接: 服务器将处理结果返回给浏览器,随后关闭当前TCP连接。
这里需要注意,服务器并不一定要通过fork()创建子进程来处理请求。fork()只是早期服务器中比较常见的一种并发处理方式,实际开发中也可以使用多线程、线程池、事件驱动等不同模型。
不过,无论服务器采用哪种处理模型,只要每次HTTP请求都要重新建立TCP连接,就不可避免地会带来额外的连接管理开销。
你想想,明明只是加载一个网页,却需要反复进行TCP三次握手、建立连接、传输数据,再关闭连接。
如果只有三五个资源,问题可能还不算明显。
但如果一个网页中包含几十张图片、多个CSS样式文件以及JavaScript脚本呢?
比如,我们平时打开一个电商网站,页面中往往包含大量商品图片、广告素材以及各种静态资源。浏览器需要获取的内容可不止一个HTML文件那么简单。

如果这些资源全部采用短连接的方式获取,那么大量重复的TCP连接建立与关闭操作,就会带来额外的时间开销和服务器资源消耗。
这显然不是我们想要的结果。所以,随着网页资源越来越丰富,HTTP/1.0默认采用的短连接方式,已经逐渐难以满足高效通信的需求。
那么问题来了:既然每次请求都重新建立TCP连接这么麻烦,能不能建立一次连接后,让多个 HTTP 请求共用这条连接呢?
当然可以!这也就引出了我们接下来要讲的另一个重要概念------HTTP长连接(Persistent Connection)。
2.2 HTTP/1.1长连接机制:从连接复用到Connection首部
2.2.1 为什么需要长连接:它是如何降低通信开销的?
前面我们已经讲过,短连接最大的问题,就是每完成一次HTTP请求与响应交互,都需要关闭当前的TCP连接。等到下一次请求到来时,又得重新建立连接。
如果一个网页只有三五个资源,倒也没什么。但随着网页中的图片、CSS、JavaScript等资源越来越多,频繁建立和断开TCP连接所带来的性能开销,也就越来越明显了。
那么,有没有办法解决这个问题呢?
当然有!这就要说到HTTP/1.1中默认使用的长连接机制了。
所谓的长连接(Persistent Connection),核心思想其实很简单:客户端与服务器建立一条TCP连接后,不必每完成一次HTTP请求就立即关闭连接,而是可以继续复用这条连接,完成后续多次请求与响应交互。
还是拿前面加载网页的例子来说。假设一个网页包含1个HTML文件和3张图片。如果采用短连接方式,浏览器通常需要分别建立4条TCP连接,才能完成这些资源的获取。
但如果采用长连接,情况就不一样了。浏览器可以先建立一条TCP连接,通过这条连接请求HTML文件。等服务器返回响应后,连接不会立即关闭,而是继续保留下来。
接下来,浏览器就可以复用这条连接,继续请求页面中的图片等资源,而不必每次都重新进行TCP三次握手。
这样一来,原本需要反复建立和关闭的TCP连接,现在就可以被多次利用,大大减少了不必要的连接管理开销。

不过,这里有一个容易搞混的地方。
长连接并不等于浏览器可以随意一次性发送多个请求,服务器也能同时返回多个响应。
HTTP/1.1确实支持一种叫做**请求流水线(Pipelining)**的机制,允许客户端在尚未收到前一个响应时,继续发送后续请求。
但服务器必须按照请求的顺序返回对应的响应,不能随意打乱。这种机制也存在队头阻塞等问题,因此在实际浏览器中并没有得到广泛应用。
所以,我们这里主要理解长连接最核心的作用就可以了:
一条TCP连接可以被重复使用,承载多次HTTP请求与响应,而不是每次交互都重新建立连接。
至于如何让客户端和服务器知道这条连接是否需要继续保持,就涉及HTTP中一个非常重要的请求头字段Connection。接下来,我们就来看看它是如何工作的。
2.2.2 Connection首部详解:如何控制连接保持与关闭
前面我们已经知道,长连接可以让客户端和服务器复用同一条TCP连接,避免频繁建立和断开连接所带来的性能开销。
不过,**这里又有一个问题了:如果客户端支持HTTP/1.1,而服务器却比较老旧,只支持HTTP/1.0,那该怎么办呢?**毕竟,不是所有的服务器都支持相同的HTTP版本,也不是所有连接都能够一直保持下去。所以,客户端和服务器之间就需要按照HTTP协议的规则,判断当前连接是否可以继续复用。
那么,这个过程是怎么实现的呢?
这就要用到HTTP报文中一个非常重要的头部字段------Connection。
在HTTP/1.0中,默认采用的是短连接。如果双方希望保持连接,就可以通过Connection: keep-alive表达使用持久连接的意愿。
而到了HTTP/1.1,情况就不一样了。
**HTTP/1.1默认使用长连接,因此不需要每次都显式声明Connection: keep-alive。**只有某一方希望在当前请求与响应交互结束后关闭连接时,才需要通过Connection: close明确告知对方。
我们可以结合下面几种情况来理解:
-
**Connection: keep-alive:**表示希望保持当前TCP连接,以便后续继续复用。在HTTP/1.0中,通常需要通过这个字段显式表达长连接意愿;而在HTTP/1.1中,持久连接本身就是默认行为。
-
**Connection: close:**表示当前连接不再继续保持,完成本次请求与响应交互后,就应该关闭连接。
-
没有显式指定Connection字段: 对于HTTP/1.1,默认按照持久连接处理;而对于HTTP/1.0,通常按照短连接处理。

比如,客户端发送下面这样的HTTP/1.1请求:
html
GET /index.html HTTP/1.1
Host: www.example.com
Connection: keep-alive
如果服务器支持并决定继续保持连接,就可以返回:
html
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 13
Connection: keep-alive
Hello, World!
这样一来,双方完成本次交互后,就不必立即关闭TCP连接,而是可以继续通过这条连接发送后续请求。
反过来,如果服务器决定关闭连接,就可以返回:
html
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 13
Connection: close
Hello, World!
浏览器收到这个响应后,就知道服务器准备关闭当前连接,后续如果还需要请求资源,就得重新建立连接。
所以,我们马上就能得出一个结论:
除了正常交换HTTP请求和响应,客户端与服务器还可以通过Connection字段表达连接管理意图,从而决定当前TCP连接是否继续保持。
不过要注意,长连接并不是简单地看双方有没有写keep-alive,还需要结合HTTP协议版本、连接状态以及实际的服务器处理能力来判断。
所谓长连接,其实并没有想象中那么神秘。它的核心就是:完成一次HTTP请求与响应之后,是否继续保留当前TCP连接,供后续请求复用。
但是,讲到这里就结束了吗?当然没有!
虽然长连接让我们可以在同一条TCP连接上连续传输多个HTTP请求和响应,但这又引出了一个新的问题:
你能保证自己每次都能从TCP连接中,准确读取到一个完整的HTTP请求吗?
要知道,TCP可不是按照HTTP请求的边界来传输数据的。
至于为什么会这样,以及服务器应该如何正确读取和解析多个HTTP请求,我们接着往下看。
2.2.3 长连接的实现关键:完整报文读取与多次请求处理
前面我们已经知道,长连接允许客户端和服务器在同一条TCP连接上,连续完成多次HTTP请求与响应交互。
但这里有一个非常关键的前提:服务器必须能够准确识别每一个HTTP请求的边界,确保读取到的是一个完整的请求,而不是只有一部分数据。
为什么要强调这一点呢?
因为TCP是面向字节流的协议,它并不会帮我们区分HTTP请求的边界。客户端即使连续发送了多个HTTP请求,服务器也不一定能通过一次recv()调用,就刚好读取到一个完整的请求。
那么,我们该如何判断一个HTTP请求是否已经读取完整了呢?
其实,答案就在HTTP协议本身。
根据HTTP/1.1的报文格式,一个完整的HTTP请求通常由以下几个部分组成:
-
请求行(Request Line): 包含请求方法、请求目标和 HTTP 协议版本。
-
请求头(Request Headers): 携带请求的相关信息,比如Host、Content-Length、Connection等字段。
-
空行(CRLF): 用于分隔请求头和请求正文,表示请求头部分已经结束。
-
请求正文(Request Body): 用于携带需要提交的数据,并不是所有HTTP请求都包含正文。

了解了这些,我们就可以按照HTTP协议规定的格式,对接收到的数据进行解析。
首先,通过空行\r\n\r\n找到请求头的结束位置。如果请求没有正文,那么请求头解析完成后,通常就能确定整个请求的边界。
如果请求携带正文,就需要继续根据相应的报文定界规则进行读取。比如,通过Content-Length确定正文长度,或者按照Transfer-Encoding: chunked的分块编码规则判断正文是否结束。
只有确认请求的所有数据都已经接收完整,我们才能认为成功读取到了一个HTTP请求。
那么,问题又来了。既然我们已经能够准确识别一个HTTP请求的边界,是不是就可以连续读取多个请求了呢?当然可以!
我们只需要在同一条TCP连接上持续接收数据,将接收到的字节放入缓冲区,再按照HTTP协议格式逐个解析完整的请求。
如果缓冲区中的数据还不足以组成一个完整请求,那就继续接收;如果已经包含了多个请求,就依次解析,并保留尚未处理的数据。
这样一来,只要服务器能够正确处理请求边界,并且连接仍然保持有效,就可以在同一条TCP连接上连续处理多个HTTP请求,而不需要每次都重新建立连接。
其实,这种处理思路我们之前就已经接触过了。
还记得当时编写的网络版本计算机吗?
那个项目本质上采用的就是类似的长连接处理模式!
客户端和服务器建立TCP连接后,可以通过同一条连接持续进行多轮数据交互,而不是每完成一次请求就立即断开连接。
只不过,当时我们使用的是自己设计的应用层协议,而现在HTTP已经帮我们规定好了请求报文的组织格式和解析规则。
说白了,长连接解决的是TCP连接能不能重复使用的问题,而HTTP报文解析解决的则是如何从连续的字节流中,准确区分每一次请求。
只有把这两个问题都处理好,服务器才能真正实现稳定的HTTP长连接通信。
2.3 短连接与长连接对比:工作方式与适用场景
前面我们已经了解了短连接和长连接的工作原理。那么,如果站在服务器的角度,结合我们熟悉的文件描述符(File Descriptor),又该怎么理解这两种连接方式呢?
其实,两者的区别就更加直观了。
-
短连接: 每次建立新的TCP连接,服务器都需要通过accept()获取对应的连接文件描述符。完成一次HTTP请求与响应后,连接通常就会关闭。如果客户端还想继续发送请求,就得重新建立连接,服务器也需要重新执行accept()。
-
长连接: 客户端与服务器建立TCP连接后,服务器可以通过同一个连接文件描述符,连续接收并处理多个HTTP请求,再将对应的响应返回给客户端。只要连接没有关闭,就不需要为每次请求重新建立TCP连接。
说白了,短连接是"一次请求与响应,通常对应一条TCP连接",而长连接则是"一条TCP连接,可以完成多次请求与响应"。
不过,这里要注意,短连接并不意味着每次accept()返回的文件描述符数值都不同。因为文件描述符是可以被操作系统回收并重新分配的,真正不同的是每次新建立的TCP连接。
好了,到这里,关于HTTP长连接和短连接的内容,我们就先告一段落。
回过头来看,算上这一篇,我们已经连续讲了四篇HTTP协议相关的文章。从最开始的HTTP请求与响应,到常见的请求方法、状态码,再到各种HTTP报头字段(Header),基本上都已经梳理了一遍。当然,还有一个比较特殊的报头字段------Cookie,我们暂时还没有展开。
除此之外,我们还动手实现了一个迷你版HTTP服务器,把前面学习的协议知识真正运用到了代码中。
不过,讲了这么多,我们似乎还漏掉了一个比较基础的问题:
HTTP协议本身到底有哪些特性?为什么它要设计成现在这个样子?
所以,接下来我们先回归HTTP协议本身,聊聊它的几个重要特性。
而这些特性,又会引出我们这几篇HTTP文章中的最后一个重要话题:Cookie与Session。
它们究竟是为了解决什么问题而出现的?又是如何配合工作的?我们接着往下看。
三、HTTP的连接管理与无状态特性
我们在学习HTTP协议时,经常会听到这样一句话:
"HTTP是一种无连接、无状态的应用层协议,每次请求都需要重新建立连接,而且服务器不会保存客户端的状态信息。"
乍一听,好像没什么问题。
但仔细想想,不对啊!我们前面不是刚刚讲过,HTTP/1.1支持长连接吗?怎么到了这里,又变成无连接了呢?
这不是前后矛盾了吗?其实,要理解这个问题,我们首先得分清楚:HTTP和TCP到底分别负责什么。
3.1 如何理解HTTP的"无连接":与长连接是否矛盾?
我们前面讲过,HTTP是应用层协议,而TCP是传输层协议,两者负责的工作并不一样。
-
HTTP: 主要负责规定客户端和服务器之间如何组织、发送和解析请求与响应,也就是我们一直在讲的Request和Response。
-
TCP: 主要负责建立连接,并提供可靠的、面向字节流的数据传输服务。
所以,HTTP本身并不负责完成TCP三次握手,也不会直接管理底层TCP连接的建立过程,这些事情都是由传输层完成的。
而我们前面所说的长连接和短连接,本质上讨论的是:完成一次HTTP请求与响应后,底层的TCP连接是否继续保留,以供后续请求复用。
比如:
-
短连接: 建立TCP连接,完成一次HTTP请求与响应后,关闭连接。
-
长连接: 建立TCP连接后,不急着关闭,而是通过同一条连接继续完成多次HTTP请求与响应。
这样一看,是不是就好理解多了?不过,这里需要特别说明一下。
"HTTP无连接"更多是早期教材中对HTTP/1.0默认短连接行为的一种概括,并不能简单理解成 HTTP 永远不维护连接,也不能说连接管理完全与HTTP无关。
毕竟,HTTP/1.1已经通过持久连接机制和Connection字段,对底层连接的复用与关闭作出了明确规定。
所以,我们更应该这样理解:
HTTP负责应用层的请求与响应规则,TCP负责底层连接和可靠传输,而HTTP可以通过协议机制决定是否复用已有的传输连接。
这也解释了为什么我们之前编写迷你HTTP服务器时,要尽量将HTTP协议处理逻辑与TCP网络通信逻辑进行解耦。
TCP通信模块负责建立连接、接收和发送字节数据,而HTTP模块则负责解析请求报文、处理请求并构建响应。
两者各司其职,既能让代码结构更加清晰,也方便后续维护和扩展。
3.2 如何理解HTTP的"无状态":服务器为什么不记得你?
讲完无连接,我们再来看HTTP的另一个重要特性------无状态(Stateless)。
相比无连接,无状态其实更容易理解。
所谓的无状态,就是说:HTTP协议本身不会自动记住客户端之前发送过什么请求,也不会因为客户端曾经访问过某个资源,就自动建立起持续的用户状态。
什么意思呢?我们可以把一个简单的HTTP服务器理解成资源提供者。
客户端发送请求,服务器解析请求,再根据请求的内容找到对应资源,最后构建HTTP响应并返回。
对于这种基本的请求与响应过程来说,服务器并不需要知道客户端之前访问过什么。
比如,你打开了我的博客首页,服务器收到请求后,正常返回首页内容。
过了一会儿,你又刷新了一次页面。
难道服务器会因为你刚才已经访问过首页,就直接拒绝这次请求吗?
当然不会!
它仍然可以按照当前请求进行处理,并返回对应的响应。
因为从HTTP协议本身来看,这两次请求是相互独立的。协议不会自动帮服务器记住:"哦,这个用户刚才已经访问过首页了。"
不过,HTTP无状态,并不代表服务器不能主动保存用户状态。
服务器完全可以通过数据库、Session等机制,记录用户的登录信息、访问状态等内容。只是这些功能需要应用程序额外实现,并不是HTTP协议自动提供的。
那么问题又来了。
既然HTTP本身不负责记住用户的历史状态,那我们平时登录网站时,为什么只需要输入一次账号和密码,后面切换页面却不用反复登录呢?
总不能每访问一个页面,都让用户重新输入一次账号密码吧?
显然不行!
为了解决这个问题,我们就需要想办法让服务器能够识别客户端,并将不同请求关联起来。
于是,Cookie和Session这样的状态管理机制就派上用场了。
其中,Cookie可以让浏览器保存一些由服务器设置的信息,并在符合条件的后续请求中自动携带这些信息。服务器就可以利用这些信息,识别用户或者关联之前保存的会话状态。
不过,这里可别把Cookie和浏览器缓存搞混了。
浏览器缓存主要是为了避免重复下载相同的资源,减少网络传输开销;而Cookie更侧重于在不同HTTP请求之间传递少量状态信息。
一个解决资源重复获取的问题,另一个帮助解决用户状态识别的问题,两者并不是一回事。
那么,Cookie到底是如何工作的?浏览器又是怎么通过Cookie,让服务器知道"我之前已经登录过了"呢?这就要引出我们接下来要讲的重点------Cookie与Session。
四、Cookie详解:HTTP如何实现客户端状态记录
4.1 Cookie为什么会出现:从HTTP无状态的局限说起
前面我们已经讲过,HTTP是一种无状态协议,它本身不会自动记录客户端之前的请求信息。
对于普通的网页访问来说,这倒不是什么大问题。毕竟,客户端请求什么资源,服务器就返回什么资源,每次请求独立处理就可以了。
但是,如果涉及用户登录、身份认证这些场景,无状态的弊端就逐渐暴露出来了。
为什么这么说呢?我们来举个简单的例子。假设你正在访问一个网站,想要查看某个必须登录后才能访问的资源。
整个过程大致是这样的:
-
用户首次访问资源: 浏览器(B端)向服务器(S端)发送HTTP Request,请求访问某个需要登录才能查看的页面。由于用户还没有登录,服务器无法确认用户身份,于是返回认证失败或需要登录的响应,提示用户先完成登录。
-
用户输入账号密码: 用户按照提示输入用户名和密码,再次向服务器发送登录请求。服务器收到请求后,对用户名和密码进行校验。如果验证通过,就说明用户身份认证成功,服务器可以返回登录成功的响应,并允许用户访问相应资源。
-
用户继续访问其他资源: 登录成功后,用户又点击了网站中的另一个页面,浏览器随即向服务器发起新的HTTP请求。
-
服务器再次要求登录: 问题来了!如果服务器没有额外的状态管理机制,那么面对这次新的请求,它就无法自动判断当前用户是否已经登录。为了保护需要身份认证的资源,只能再次要求用户提供身份凭证。
你想想,这是不是很麻烦?
明明刚才已经输入过账号密码,也成功登录了,结果每点击一个新的页面,服务器都要重新确认一次身份。
如果访问十个页面,就得反复登录十次,那用户估计早就不耐烦了。显然,这种体验是我们无法接受的。
那么问题来了:既然HTTP本身不会自动记住用户的登录状态,有没有办法让服务器知道,这个用户之前已经登录过了呢?
当然有!我们可以想办法给客户端保存一个能够标识会话身份的信息,让浏览器在后续请求中自动携带它。这样一来,服务器收到新的请求时,就可以根据这个信息识别对应的用户会话,而不需要每次都重新验证用户名和密码。
于是,Cookie就派上用场了。
Cookie可以让服务器通过HTTP响应向浏览器设置一些信息,再由浏览器在符合条件的后续请求中自动携带这些信息。
不过要注意,Cookie本身并不等于登录状态,也不会自动完成用户身份认证。 它只是提供了一种在客户端保存并随请求传递信息的机制。服务器还需要配合Session等状态管理方案,才能实现完整的登录状态保持。
说白了,HTTP本身没有记忆,但我们可以通过Cookie和Session,让原本相互独立的请求建立起联系。
那么,Cookie究竟是如何保存信息的?浏览器又是怎么把这些信息重新发送给服务器的?
4.2 Cookie是什么:基本概念与作用
前面我们已经知道,HTTP本身是无状态的,它并不会自动记住客户端之前的请求信息。那么问题来了,既然HTTP没有记忆,服务器又是怎么知道当前访问的用户,和刚才登录的用户是同一个人呢?
这时候,Cookie就派上用场了。
所谓的HTTP Cookie ,也叫Web Cookie、浏览器Cookie,简称Cookie。说白了,它就是服务器发送给浏览器,并由浏览器保存的一小段数据。
当浏览器后续再次向服务器发起请求时,如果满足Cookie的发送条件,就会自动将对应的Cookie携带在HTTP请求中,一起发送给服务器。
这样一来,服务器就可以根据Cookie中的信息,将前后两次原本独立的HTTP请求关联起来。
举个简单的例子。
假设你第一次访问某个网站,并成功登录了自己的账号。服务器在验证身份后,可以通过HTTP响应向浏览器设置一个Cookie。
浏览器收到后,就会按照相应的规则将它保存起来。
等你下次再访问这个网站时,浏览器就可以自动携带之前保存的 Cookie。服务器收到请求后,再根据Cookie中的会话标识查找对应的登录状态,从而判断用户是否已经完成身份认证。
这样一来,用户就不需要每点击一个页面,都重新输入一次账号和密码了。
当然,Cookie的用途并不只有保持登录状态。比如,记住用户的语言设置、保存网站偏好等,也都可以通过Cookie来实现。
不过,这里需要注意:Cookie本身只是保存和传递数据的一种机制,并不意味着服务器收到Cookie后,就一定能够确认用户身份。 真正的身份验证,还需要配合服务器端的会话管理等机制来完成。
简单来说,Cookie就像浏览器随身携带的一张小纸条。服务器可以往上面写一些信息,浏览器负责保存,并在符合条件的后续请求中把它带回来。
而服务器就可以利用这张"小纸条",将不同的HTTP请求联系起来,在无状态的HTTP协议之上,实现一定程度的状态管理。
那么,Cookie究竟是怎么从服务器发送到浏览器的?浏览器又是通过什么方式将它重新带回服务器的呢?接下来,我们就从HTTP报文的角度,看看Cookie的具体工作原理。
4.3 Cookie的工作原理:从服务器下发到客户端携带
前面我们已经了解了 Cookie 的基本概念,知道了它可以帮助浏览器保存一些信息,并在后续请求中将这些信息重新发送给服务器。
那么问题来了,Cookie 到底是怎么工作的?服务器又是如何把 Cookie 交给浏览器的呢?
其实,整个过程并不复杂。
这里我画了一张板书,大家可以结合下面的流程来看一下。

(1)服务器通过Set-Cookie设置Cookie
当用户第一次访问某个网站时,浏览器会向服务器发送 HTTP 请求。
服务器收到请求后,可以在返回的HTTP响应头中添加一个Set-Cookie字段,用于告诉浏览器需要保存哪些Cookie信息。
比如:
bash
HTTP/1.1 200 OK
Set-Cookie: session_id=abc123; Path=/; HttpOnly
Content-Type: text/html
这里的Set-Cookie就是服务器用来设置Cookie的响应头字段,而session_id=abc123则是一个简单的Cookie键值对。
(2)浏览器接收并保存 Cookie
浏览器收到服务器返回的响应后,会解析其中的Set-Cookie字段,并根据Cookie的属性和浏览器自身的策略,决定是否保存。
通常情况下,Cookie会与对应的域名、路径等作用范围关联起来,并不是所有网站都能随意读取或使用它。
至于Cookie能够保存多久、什么时候失效,则取决于它的有效期设置以及浏览器的处理策略。
(3)浏览器在后续请求中自动携带 Cookie
当浏览器再次访问符合Cookie发送条件的地址时,就会自动在HTTP请求头中添加Cookie字段,将之前保存的信息发送给服务器。
比如:
GET /user HTTP/1.1
Host: www.example.com
Cookie: session_id=abc123
服务器收到这个请求后,就可以读取Cookie字段,从中获取对应的信息。
如果这个Cookie保存的是会话标识,服务器还可以根据该标识查询对应的Session,判断当前用户是否处于登录状态。
这样一来,服务器就能够将前后两次原本独立的HTTP请求联系起来,而不需要用户每次访问页面都重新登录。
不过,这里还有一点需要特别注意:
Cookie并不是浏览器在每次请求时都会无条件携带的。
浏览器会根据Cookie的Domain、Path、Secure、SameSite等属性,以及实际的请求环境,判断是否应该发送对应的Cookie。
所以,我们可以把Cookie的工作过程简单概括为:
服务器通过 Set-Cookie****设置Cookie → 浏览器接收并保存 → 后续符合条件的请求自动携带 Cookie**→ 服务器读取并处理Cookie信息。**
说白了,Set-Cookie负责把信息交给浏览器保存,而Cookie则负责在后续请求中把这些信息带回服务器。
这两个HTTP头部字段相互配合,就构成了Cookie最基本的工作机制。
4.4 Cookie的分类:不同类型与生命周期
前面我们已经了解了Cookie的工作原理,知道了浏览器可以保存服务器设置的 Cookie,并在后续请求中自动携带。
不过,这里又有一个问题:Cookie被浏览器保存下来后,会一直存在吗?还是说关闭浏览器之后就自动消失了?
其实,Cookie并不都是永久保存的。根据生命周期的不同,我们通常可以将Cookie分为两种:
-
会话Cookie(Session Cookie): 这类Cookie没有设置明确的过期时间,通常只在当前浏览器会话期间有效。当浏览器会话结束后,Cookie就会失效。不过,有些浏览器支持会话恢复功能,即使关闭后重新打开,也可能恢复之前的会话Cookie。
-
持久Cookie(Persistent Cookie): 这类Cookie设置了明确的过期时间(Expires)或有效时长(Max-Age)。只要没有过期,也没有被用户主动清除,就可以在浏览器关闭并重新打开后继续使用。
说白了,会话Cookie更偏向于临时保存,而持久Cookie则可以跨越多个浏览器会话继续存在。
那么,持久Cookie到底保存在哪里呢?
其实,浏览器通常会将需要持久保存的Cookie数据存储在本地的浏览器配置目录中。以Chrome、Edge等基于Chromium的浏览器为例,相关数据一般由浏览器内部的数据库进行管理。
不过,这并不意味着我们直接找到对应的文件,就能像打开普通文本文件一样查看Cookie。
因为这些数据可能采用SQLite数据库等格式存储,而且部分Cookie的值还可能经过加密处理。直接用记事本打开,看到的可能是一堆乱码,甚至根本无法正常读取。
那我们平时想查看Cookie,该怎么办呢?其实没必要这么麻烦!
浏览器本身就提供了开发者工具,可以直接查看当前网站保存的Cookie信息。
比如,在Chrome或Edge浏览器中,我们可以按下F12打开开发者工具,然后依次找到:
Application(应用程序)→ Storage(存储)→ Cookies
选择对应的网站地址后,就可以查看Cookie的名称、值、所属域名、有效期等信息。
这里顺便提一句,Chrome并不是完全看不到Cookie,只是随着浏览器隐私保护机制不断完善,部分第三方Cookie可能受到限制,某些Cookie也可能因为作用域或浏览器策略而没有显示在当前网站的列表中。
为了方便后面的演示,我们这里统一使用Microsoft Edge浏览器。
至于怎么查看 Cookie、修改Cookie,以及Cookie过期之后会发生什么,接下来我们就结合浏览器的开发者工具,实际操作一下。

4.5 Cookie的安全隐患:信息泄露与身份冒用风险
前面我们已经了解了Cookie的工作原理,知道了浏览器可以保存Cookie,并在后续请求中自动携带这些信息。
不过,Cookie虽然给用户带来了不少便利,但如果使用不当,同样可能带来严重的安全隐患。
为什么这么说呢?大家想一想,如果服务器利用Cookie中的会话标识来判断用户是否已经登录,那么一旦这个标识被别人窃取,会发生什么?
这里我画了一张板书,大家可以结合下面的过程来看一下。
假设你已经登录了某个网站,服务器通过Set-Cookie给浏览器设置了一个用于维持登录状态的会话标识。
正常情况下,浏览器会保存这个Cookie,并在后续请求中自动携带。服务器收到后,再根据这个标识找到对应的会话信息,判断用户是否已经登录。到这里,一切都很正常。
但问题来了:如果黑客通过木马病毒,偷偷获取了你浏览器中保存的Cookie呢?
整个攻击过程大致可以分为以下几个步骤:
-
用户正常登录网站: 用户输入账号和密码,服务器验证通过后,创建对应的登录会话,并通过Set-Cookie将会话标识发送给浏览器保存。
-
黑客窃取Cookie: 假设用户的电脑不小心感染了木马病毒,恶意程序就可能尝试读取浏览器保存的Cookie数据,并将窃取到的信息发送给攻击者。
-
黑客冒用用户会话: 攻击者获取有效的会话Cookie后,就可能尝试在自己的请求中携带这个Cookie,冒充原来的用户向服务器发送HTTP请求。
-
服务器识别会话: 如果服务器仅依赖这个会话标识进行身份识别,而且该会话尚未过期或失效,那么服务器就可能误以为请求来自已经登录的用户,从而允许访问相应资源。

你看,攻击者可能根本不需要知道用户的账号密码,只要拿到了有效的登录会话凭证,就有机会冒用用户身份。
这种攻击通常被称为会话劫持(Session Hijacking)。
如果网站缺少必要的安全防护,攻击者就可能利用被盗会话访问用户的私人信息,甚至尝试执行转账、修改资料等敏感操作。
当然,这并不意味着拿到Cookie就一定能够盗取账户资金。实际能否成功,还取决于服务器是否进行了额外的身份验证、会话校验以及敏感操作确认。
另外,这里还有一个容易误解的地方。
Cookie并不是专门用来保存用户名和密码的,更不应该直接保存用户的明文密码。
在实际开发中,服务器通常会将随机生成的会话标识保存到Cookie中,而真正的用户登录状态则保存在服务器端。
所以,Cookie的风险不一定在于泄露密码,而在于某些Cookie本身就可能代表用户已经通过身份认证。
那么,有没有办法降低这些安全风险呢?当然有!
比如,使用HTTPS保护数据在网络中的传输,通过HttpOnly限制JavaScript读取Cookie,利用 Secure 限制Cookie通过安全连接发送,再配合SameSite、会话过期机制和敏感操作二次验证等措施,降低Cookie被窃取或滥用的风险。
不过,这些措施各有自己的保护范围。例如,HttpOnly能够限制网页脚本读取Cookie,却不能保证已经感染恶意软件的电脑绝对安全。
说白了,Cookie只是一个保存和传递信息的工具,真正需要重视的是我们往里面放了什么,以及服务器如何使用这些信息。
尤其是涉及用户登录状态时,一旦会话凭证泄露,就可能带来身份冒用的风险。
所以,在实际开发中,我们既要利用Cookie解决HTTP无状态带来的问题,也要做好相应的安全防护,不能为了方便就忽略了安全性。
五、Session会话管理:从服务端状态保存到安全防护

前面我们已经讲过,Cookie虽然能够帮助服务器识别用户,但如果使用不当,也可能带来严重的安全隐患。
尤其是当我们直接把用户名、密码等敏感信息保存在Cookie中时,一旦这些信息被窃取,后果可就比较严重了。
那么,有没有办法既保留 Cookie 的便利性,又尽可能避免用户的敏感信息直接暴露在客户端呢?
当然有!这时候,我们就可以引入另一个重要的概念------Session(会话)。
不过,先说明一点:Session主要解决的是敏感会话数据的服务端管理问题,并不能从根本上杜绝身份冒用或会话劫持。
5.1 Session的基本原理:服务器如何识别用户身份
我们还是通过用户登录的例子,来看看Session到底是怎么工作的。
假设用户在登录页面中输入用户名和密码:
bash
username=zs&passwd=123456
当用户点击登录按钮后,浏览器就会向服务器发送登录请求,服务器收到请求后,对用户名和密码进行校验。
如果验证通过,接下来会发生什么呢?
服务器不会直接把用户的账号密码保存到Cookie中,而是可以在服务端创建一个Session,用来维护当前用户的会话状态。
比如,Session中可以保存用户ID、登录状态、相关权限等信息。
随着登录用户越来越多,服务器就需要管理多个Session。我们可以采用之前学习过的**"先描述,再组织"**的思路,将这些会话信息组织起来,方便后续查找和管理。
那么问题来了:服务器内部维护了这么多Session,怎么知道某个请求对应的是哪一个用户呢?
很简单,给每个 Session分配一个唯一且难以猜测的标识,也就是Session ID。
当用户登录成功后,服务器可以在HTTP响应中添加一个Set-Cookie字段,把对应的Session ID交给浏览器保存。
例如:
HTTP/1.1 200 OK
Set-Cookie: session_id=12345678adsjfksd; Path=/; HttpOnly; Secure; SameSite=Lax
Content-Type: text/html
这里的 session_id=12345678adsjfksd 只是为了方便理解而写出的示例,实际开发中应该使用安全随机数生成足够长、不可预测的会话标识。
浏览器收到响应后,会按照Cookie的相关规则保存这个Session ID。
等到用户再次访问网站时,浏览器就可以在符合条件的HTTP请求中自动携带对应的Cookie:
GET /user HTTP/1.1
Host: www.example.com
Cookie: session_id=12345678adsjfksd
服务器收到请求后,读取其中的Session ID,再根据这个标识找到服务端保存的Session,判断当前用户是否已经登录,以及是否有权限访问对应资源。
这样一来,浏览器就不需要每次都重新提交用户名和密码了。
而且,用户的敏感会话数据主要保存在服务器端,浏览器只需要保存一个用于关联会话的标识。
说白了,Cookie负责携带Session ID,Session负责在服务器端保存和管理用户的会话状态。
两者配合起来,就能够在HTTP无状态的基础上,实现用户登录状态的保持。
5.2 Session就一定安全吗:会话标识泄露带来的风险
看到这里,可能有人会觉得:
既然用户的敏感信息已经保存在服务器端,浏览器只保存一个Session ID,那安全问题不就解决了吗?
**先别急着下结论!**我们再来想一个问题。
假设黑客通过某种手段,成功获取了用户的Session ID,比如:
session_id=12345678adsjfksd
那么,黑客能不能携带这个Session ID,冒充用户向服务器发送请求呢?
答案是:有可能!
因为在基于Session ID的认证机制中,服务器通常会根据客户端提供的会话标识,查找对应的登录状态。
如果攻击者拿到了一个仍然有效的Session ID,而服务器又缺少额外的安全校验,那么攻击者就有可能利用它冒充已经登录的用户。
注意,黑客甚至不需要知道用户的账号密码,只需要获取有效的会话凭证,就可能完成身份冒用。
这也正是我们前面提到的**会话劫持(Session Hijacking)**问题。
所以,Session虽然能够避免将敏感会话数据直接保存在客户端,但并不能保证会话标识永远不会被窃取。
5.3 如何提高会话安全性:从会话保护到异常检测
既然Session ID也可能被盗,那我们在实际开发中,就需要配合其他安全措施。
比如:
-
异常登录检测: 当系统发现用户的登录设备、访问行为等出现明显异常时,可以进一步进行身份验证,必要时终止可疑会话。
-
IP 地址与设备信息辅助校验: 结合用户的IP地址、设备信息等判断会话是否存在异常。不过,IP 地址本身可能变化,因此不能仅凭IP变化就认定用户身份被盗。
-
设置Cookie安全属性: 通过HttpOnly、Secure、SameSite等属性,降低Cookie被恶意脚本读取、通过非安全连接传输或遭到跨站请求滥用的风险。
-
会话过期与主动失效: 为Session设置合理的有效期,在用户退出登录后及时使会话失效,并在登录或权限发生重要变化时更新会话标识。
-
敏感操作二次验证: 对于修改密码、资金转账等高风险操作,即使用户已经登录,也可以要求再次验证身份。
另外,HTTPS同样不能少! 它能够保护HTTP数据在传输过程中的机密性和完整性,减少会话凭证在网络传输中被窃取的风险。
当然,没有哪一种措施能够保证绝对安全。我们真正需要做的,是通过多种防护手段相互配合,尽可能降低会话被盗用的风险。
5.4 Cookie与Session总结:会话管理与会话保持机制
讲到这里,Cookie和Session之间的关系应该就比较清楚了。
Cookie主要负责在浏览器端保存和传递少量信息,而Session则可以在服务器端维护用户的会话状态。
在常见的服务端Session方案中,浏览器通过Cookie携带Session ID,服务器再根据这个 ID 找到对应的Session,从而识别用户身份,并保持登录状态。
这种利用Cookie和Session等机制,将多个独立的HTTP请求关联起来、持续管理用户会话状态的过程,通常就涉及我们所说的会话管理(Session Management)与会话保持(Session Persistence)。
不过要记住,Cookie和Session并不是必须绑定在一起使用的。Cookie可以独立保存网站偏好等信息,而Session ID也可以通过其他合适的方式传递。
说到底,HTTP本身虽然是无状态的,但我们可以在应用层引入额外的状态管理机制,让服务器能够识别用户,并将前后多次请求联系起来。
这也正是Cookie和Session Web开发中如此重要的原因。
好了,到这里,本篇关于HTTP协议的内容就讲得差不多了。
这一篇我们先把前面没有讲完的HTTP请求方法补充了一下,接着从HTTP/1.0的短连接出发,分析了频繁建立和断开TCP连接带来的性能开销,以及HTTP/1.1如何通过持久连接机制和Connection字段,实现同一条TCP连接的重复利用。
随后,我们又回过头来聊了聊HTTP的无连接与无状态特性。尤其是无状态这一点,虽然让HTTP的请求与响应处理更加简单,却也带来了一个问题:服务器该如何记住用户的登录状态呢?
于是,我们顺着这个问题,引出了Cookie和Session。
从Cookie的基本概念、工作原理,到它可能带来的会话劫持风险,再到Session如何配合Cookie实现用户身份识别与会话保持,相信大家对这两个概念已经有了更清晰的认识。
其实,回顾前面几篇HTTP文章,我们会发现,很多知识点并不是孤立存在的。从请求与响应,到状态码、报头字段,再到连接管理和会话保持,它们之间都有着紧密的联系。
而我们学习HTTP,也不只是为了记住几个请求方法、几个状态码。更重要的是弄清楚:浏览器和服务器之间究竟是怎么通信的?为什么要这样设计?这些机制又解决了哪些实际问题?
毕竟,只有真正理解了协议背后的设计思想,后面自己动手编写HTTP服务器,或者开发其他网络程序时,才能知道代码为什么要这样写,而不只是照着接口调用。
到这里,我们的HTTP协议基础知识也算是告一段落了。当然,HTTP还有很多值得深入研究的内容,后面遇到了,我们再结合实际开发慢慢展开。
好了,本篇文章就先聊到这里。如果文章中有什么理解不到位或者讲错的地方,也欢迎大家在评论区指出。技术这东西,本来就是在不断学习和交流中慢慢积累起来的。
感谢大家的阅读,我们下篇文章再见!