前言
在日常的Web开发与网络通信中,有几个问题困扰:网站是如何记住我的登录状态的?HTTP协议明明是无状态的,为什么我刷新页面之后依然保持着登录?这些问题背后,隐藏着Web开发中三个最核心的技术概念——Cookie、Session和HTTPS。
本文将系统梳理HTTP Cookie与Session的工作机制,以及HTTPS协议的加密原理与证书体系。
一、登录场景
1.1 熟悉的场景
打开B站,输入账号密码登录。关闭浏览器,第二天再次打开B站,发现依然处于登录状态——不需要重新输入密码。
问题来了:B站是如何“认识”我这个登录用户的?
答案要从HTTP协议的特性说起。
1.2 HTTP的无状态特性
HTTP协议被设计为无状态的——这意味着每次客户端与服务器之间的请求-响应都是独立的,服务器不会自动记住两次请求是否来自同一个客户端。
就像去一家咖啡馆,每次点单店员都不记得你之前来过,每次都要从头开始交流。
无状态设计的好处是简化了协议,降低了服务器的负担。但也带来了一个明显的问题:服务器无法自动识别连续请求是否来自同一用户。
试想一下,如果没有额外的机制:用户登录后访问个人中心,服务器无法识别“该用户已登录”;连续两次添加商品到购物车,服务器无法关联为同一用户的操作。
1.3 会话管理
为了解决HTTP无状态带来的问题,Web应用需要实现以下能力:
| 目标 | 说明 |
|---|---|
| 身份识别 | 确定请求来自哪个客户端 |
| 状态保持 | 记录用户的登录状态、操作历史等 |
| 安全性 | 防止身份伪造和信息泄露 |
为了实现这些目标,Cookie和Session应运而生。
二、HTTP Cookie
2.1 什么是Cookie
HTTP Cookie是服务器发送到用户浏览器并保存在浏览器上的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带并发送到服务器上。
简单来说:Cookie就像是服务器贴在用户浏览器上的一张“小纸条”,上面写着一些信息。浏览器在后续访问同一网站时,会自动把这张纸条展示给服务器看。
2.2 原理
Cookie的工作流程可以分为三个步骤:
第一步:服务器下发Cookie
当用户第一次访问网站时,服务器会在HTTP响应头中设置Set-Cookie字段,将Cookie发送到用户的浏览器。
第二步:浏览器保存Cookie
浏览器接收到Cookie后,会将其保存在本地。
第三步:浏览器自动携带Cookie
在后续的请求中,浏览器会自动在HTTP请求头中携带Cookie字段,将之前保存的Cookie信息发送给服务器。
2.3 格式
HTTP响应头中的Set-Cookie字段用于给浏览器设置Cookie值。一个完整的示例如下
Set-Cookie: username=peter; expires=Thu, 18 Dec 2024 12:00:00 UTC; path=/; domain=.example.com; secure; HttpOnly2.4 属性
| 属性 | 说明 | 示例 |
|---|---|---|
| Name=Value | Cookie的名称与值 | username=peter |
| Expires | 过期时间(RFC 1123格式) | Thu, 18 Dec 2024 12:00:00 UTC |
| Path | 作用路径 | path=/表示全站生效 |
| Domain | 作用域名(点前缀表示包含子域名) | domain=.example.com |
| Secure | 仅HTTPS协议发送 | 增加安全性 |
| HttpOnly | 禁止JavaScript访问 | 防止XSS攻击 |
过期时间:如果设置了expires属性,Cookie将在指定的时间后过期;如果没有设置,则Cookie默认为会话Cookie,浏览器关闭即失效。
GMT与UTC:GMT基于地球自转,而UTC基于原子钟,更加精确。在实际使用中两者差别很小,多数情况下可以互换使用。
2.5 分类
| 类型 | 特点 | 存储位置 |
|---|---|---|
| 会话Cookie | 浏览器关闭时失效 | 浏览器内存 |
| 持久Cookie | 带有明确过期时间 | 浏览器本地文件 |
如果Cookie是持久性的,它实际上是浏览器特定目录下的一个文件。但直接查看这些文件会看到乱码,因为Cookie文件通常以二进制或SQLite格式存储。
2.6 用途
Cookie在实际应用中有三大用途:
用户认证和会话管理
跟踪用户行为
缓存用户偏好
2.7 例子
以下是一个用C++实现的简易HTTP服务器中设置Cookie的示例:
// 生成RFC 1123格式的过期时间 std::string GetMonthName(int month) { std::vector<std::string> months = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; return months[month]; } std::string GetWeekDayName(int day) { std::vector<std::string> weekdays = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; return weekdays[day]; } std::string ExpireTimeUseRfc1123(int t) { // t为秒级别的未来UTC时间 time_t timeout = time(nullptr) + t; struct tm* tm = gmtime(&timeout); // gmtime获取UTC时间,不能用localtime(带时区) char timebuffer[1024]; snprintf(timebuffer, sizeof(timebuffer), "%s, %02d %s %d %02d:%02d:%02d UTC", GetWeekDayName(tm->tm_wday).c_str(), tm->tm_mday, GetMonthName(tm->tm_mon).c_str(), tm->tm_year + 1900, tm->tm_hour, tm->tm_min, tm->tm_sec); return timebuffer; } // 设置Cookie std::string ProveCookieWrite() { return "Set-Cookie: username=zhangsan;"; } // 设置带过期时间的Cookie(1分钟后过期) std::string ProveCookieTimeOut() { return "Set-Cookie: username=zhangsan; expires=" + ExpireTimeUseRfc1123(60) + ";"; } // 设置路径限制 std::string ProvePath() { return "Set-Cookie: username=zhangsan; path=/a/b;"; }测试结果:当浏览器访问服务器时,服务器通过Set-Cookie响应头写入Cookie。刷新浏览器后,Cookie会自动在HTTP请求头中被提交回服务器。
2.8 隐患
Cookie存储在客户端,存在两个风险:
被篡改:用户或攻击者可以修改Cookie的内容
被窃取:如果传输过程未加密,Cookie可能被中间人截获
如果Cookie中直接存放了用户名、密码等敏感信息,一旦泄露后果不堪设想。
为了缓解这些风险,以下措施:
Secure标志:确保Cookie仅在HTTPS连接上传输
HttpOnly标志:防止JavaScript读取Cookie,抵御XSS攻击
三、HTTP Session
3.1 为什么需要Session
既然Cookie存在安全隐患——用户的私密数据保存在浏览器端,容易被盗取或泄露,那能不能把敏感数据放到服务器端存储呢?
Session就是为了解决这个问题而生的。
3.2 什么是Session
HTTP Session是服务器用来跟踪用户与服务器交互期间用户状态的机制。由于HTTP协议是无状态的,服务器需要通过Session来“记住”用户的信息。
如果说Cookie是“会员卡”(客户端持有),那么Session就是“保险柜”(服务器持有)。
3.3 原理
Session的工作流程如下:
第一步:创建Session
当用户首次访问网站时,服务器会为用户创建一个唯一的Session对象。
第二步:生成Session ID
服务器为这个Session对象生成一个唯一的Session ID,并通过Cookie将其发送到客户端。
第三步:客户端携带Session ID
客户端在后续的请求中会自动携带这个Session ID。
第四步:服务器识别用户
服务器通过Session ID来识别用户,从而获取该用户的会话信息。
3.4 区别
| 维度 | Cookie | Session |
|---|---|---|
| 存储位置 | 客户端 | 服务器端 |
| 安全性 | 较低 | 较高 |
| 数据大小 | 有限制 | 无限制 |
| 生命周期 | 可长期存储 | 默认30分钟超时 |
| 资源消耗 | 客户端承担 | 服务器承担 |
3.5 安全性
与Cookie相比,Session在安全性上有天然的优势——用户的状态信息存储在服务器端,客户端只持有一个Session ID。
即使Session ID被窃取,攻击者获得的也只是一个标识符,而不是用户的敏感数据。此外,服务器可以主动管理Session的有效性,比如检测异地登录、强制Session失效等。
当然,Session ID本身在传输过程中仍有可能被截获,因此仍需要配合HTTPS和合适的Cookie属性来增强安全性。
3.6 超时与失效
Session可以设置超时时间,超过这个时间后Session会自动失效。服务器也可以主动使Session失效,例如当用户登出时。
默认超时时间通常为30分钟,每次请求都会“续期”。
3.7 示例
以下是一个用C++模拟Session管理的实现:
Session.hpp——Session对象定义与管理器
// Session对象:存储用户状态信息 class Session { public: Session(const std::string &username, const std::string &status) : _username(username), _status(status) { _create_time = time(nullptr); } std::string _username; std::string _status; uint64_t _create_time; }; using session_ptr = std::shared_ptr<Session>; // Session管理器:管理所有Session对象 class SessionManager { public: SessionManager() { srand(time(nullptr) ^ getpid()); } // 添加Session,生成Session ID std::string AddSession(session_ptr s) { uint32_t randomid = rand() + time(nullptr); std::string sessionid = std::to_string(randomid); _sessions.insert(std::make_pair(sessionid, s)); return sessionid; } // 根据Session ID获取Session session_ptr GetSession(const std::string sessionid) { if (_sessions.find(sessionid) == _sessions.end()) return nullptr; return _sessions[sessionid]; } private: std::unordered_map<std::string, session_ptr> _sessions; };HttpProtocol.hpp——HTTP请求处理中的Session集成
class Http { public: Http(uint16_t port) { _tsvr = std::make_unique<TcpServer>(port, std::bind(&Http::HandlerHttp, this, std::placeholders::_1)); _tsvr->Init(); _session_manager = std::make_unique<SessionManager>(); } // 生成Set-Cookie响应头,写入Session ID std::string ProveSession(const std::string &session_id) { return "Set-Cookie: sessionid=" + session_id + ";"; } std::string HandlerHttp(std::string request) { HttpRequest req; HttpResponse resp; req.Deserialize(request); req.Parse(); static int number = 0; // /login路径:模拟登录,创建Session if (req.Url() == "/login") { std::string sessionid = req.SessionId(); if (sessionid.empty()) { // 首次登录 std::string user = "user-" + std::to_string(number++); session_ptr s = std::make_shared<Session>(user, "logged"); std::string sessionid = _session_manager->AddSession(s); resp.AddHeader(ProveSession(sessionid)); } else { // 已登录,验证Session session_ptr s = _session_manager->GetSession(sessionid); if (s != nullptr) { // Session有效,用户活跃中 } } } resp.SetCode(200); resp.SetDesc("OK"); resp.AddHeader("Content-Type: text/html"); resp.AddContent("<html><h1>helloworld</h1></html>"); return resp.Serialize(); } private: std::unique_ptr<TcpServer> _tsvr; std::unique_ptr<SessionManager> _session_manager; };3.8 测试
用两个不同的浏览器(如Chrome和Edge)分别访问服务器的/login路径:
删除浏览器中指定服务器的所有历史Cookie
浏览器A访问/login,服务器创建Session并写入Cookie
浏览器B访问/login,服务器为其创建新的Session
两个浏览器访问任意站点资源,服务器能够区分它们
测试结果:服务器端日志显示,不同浏览器携带不同的Session ID,服务器能准确识别每一个客户端。
3.9 小结
Cookie和Session不是“二选一”的对立关系,而是“好搭档”:
Cookie负责“传递钥匙”:在客户端存储Session ID
Session负责“保管物品”:在服务器端存储用户状态数据
Session ID是桥梁:通过Cookie传递的Session ID将客户端与服务器端的Session对象关联起来
四、HTTPS协议
4.1 为什么需要HTTPS
前文提到,Cookie和Session解决了“识别用户”的问题,但传输过程中的安全性问题依然存在。
HTTP协议的内容是以明文方式传输的。这些明文数据会经过路由器、WiFi热点、通信运营商、代理服务器等多个物理节点。如果信息在传输过程中被劫持,传输的内容就完全暴露了。更严重的是,劫持者还可以篡改传输的信息且不被双方察觉——这就是中间人攻击。
一个经典的例子是运营商劫持:用户点击下载某个APP的按钮,结果下载下来的却是另一个推广软件。这就是因为运营商在传输过程中篡改了HTTP响应内容。
HTTPS正是为了解决这些问题而生的——它在HTTP协议的基础上引入了一个加密层。
4.2 概念
4.2.1 什么是加密
加密:把明文进行一系列变换,生成密文。
解密:把密文再进行一系列变换,还原成明文。
密钥:在加密和解密过程中辅助完成变换的数据。
历史小知识:加密解密已经发展成一门独立的学科——密码学。密码学的奠基人之一正是计算机科学的祖师爷艾伦·麦席森·图灵。
4.2.2 对称加密
对称加密采用单钥密码系统,加密和解密使用同一个密钥。
特点:
算法公开
计算量小
加密速度快
加密效率高
常见算法:DES、3DES、AES等。
简单示例:明文a = 1234,密钥key = 8888,加密a ^ key得到密文9834。解密时9834 ^ key还原为1234。
4.2.3 非对称加密
非对称加密需要两个密钥:公钥和私钥。
特点:
算法强度复杂
安全性依赖于算法与密钥
加密解密速度比对称加密慢很多
常见算法:RSA、DSA、ECDSA等。
核心机制:
用公钥加密的密文,只有对应的私钥能解密
反过来,用私钥加密的密文,也只有对应的公钥能解密
生活类比:B给A一把锁(公钥),A把文件锁进盒子,只有B拿着钥匙(私钥)才能打开。
4.2.4 数据摘要
数据摘要利用单向散列函数对信息进行运算,生成一串固定长度的数字摘要。
特点:
不是加密
从摘要很难反推原文
常用于判断数据是否被篡改
常见算法:MD5、SHA1、SHA256、SHA512等。
4.2.5 数字签名
数字签名= 摘要经过加密。
4.3 HTTPS的加密方案
方案一:仅使用对称加密
如果通信双方持有同一个密钥,且没有别人知道,通信是安全的。
问题:服务器要为大量客户端提供服务,每个客户端都需要不同的密钥。密钥的传输本身也需要加密——这就成了“先有鸡还是先有蛋”的循环问题。
方案二:仅使用非对称加密
服务器将公钥以明文传输给浏览器,浏览器用公钥加密数据发送给服务器。
问题1:服务器到浏览器的数据传输如何保障安全?如果服务器用私钥加密数据,浏览器用公钥解密——但公钥一开始是明文传输的,中间人也能获取。
问题2:非对称加密速度太慢,不适合大量数据传输。
方案三:双方都使用非对称加密
客户端和服务端各自拥有公钥和私钥,并交换公钥。
问题:效率太低,且仍然存在中间人攻击的风险。
方案四:非对称加密 + 对称加密
这是最接近正确答案的方案:
服务端具有非对称公钥S和私钥S'
客户端发起HTTPS请求,获取服务端公钥S
客户端在本地生成对称密钥C,用公钥S加密后发送给服务器
服务器用私钥S'解密,获得对称密钥C
后续通信全部使用对称密钥C进行加密
优点:兼顾了非对称加密的安全性和对称加密的效率。
但是:这个方案仍然存在一个致命漏洞——中间人攻击。
4.4 中间人攻击
中间人攻击是HTTPS需要解决的核心威胁。
攻击过程如下:
服务器拥有公钥S和私钥S',中间人拥有公钥M和私钥M'
服务器明文传送公钥S给客户端
中间人劫持报文,提取公钥S保存,将自己的公钥M替换进去
客户端收到公钥M,生成对称密钥X,用M加密后发送
中间人劫持报文,用私钥M'解密获得X,再用公钥S加密后转发给服务器
服务器用私钥S'解密获得X
双方开始用X进行对称加密通信——但一切都在中间人的掌控之中
问题本质:客户端无法确认收到的公钥是否真的来自目标服务器。
4.5 CA证书与数字签名
4.5.1 什么是CA证书
为了解决“如何确认公钥的来源可信”这个问题,引入了CA(证书颁发机构)。
服务端在使用HTTPS前,需要向CA机构申领一份数字证书。证书就像网络世界的“身份证”,包含以下信息:
| 信息项 | 说明 |
|---|---|
| 证书发布机构 | 签发该证书的CA |
| 证书有效期 | 证书的起止时间 |
| 公钥 | 服务端的公钥 |
| 证书所有者 | 域名等身份信息 |
| 数字签名 | CA对证书内容的签名 |
4.5.2 数字签名的形成
CA机构签发证书时形成数字签名的过程:
CA机构拥有非对称加密的私钥A和公钥A'
CA对服务端申请的证书明文数据进行Hash,形成数据摘要
CA用私钥A对数据摘要进行加密,得到数字签名S
证书明文 + 数字签名S =数字证书
4.5.3 客户端验证证书
客户端收到证书后进行校验:
第一步:检查基本信息
证书有效期是否过期
证书发布机构是否受信任
第二步:验证证书是否被篡改
从操作系统中获取该CA的公钥A'
用公钥A'对数字签名解密,得到一个Hash值
计算整个证书明文的Hash值
对比hash1和hash2是否相等
相等则说明证书未被篡改
4.6 HTTPS工作流程
综合以上所有机制,HTTPS的完整工作流程如下:
第一阶段:证书验证
客户端向服务器发起HTTPS请求
服务器返回CA签发的数字证书
客户端验证证书有效)
验证通过后,客户端信任证书中的公钥
第二阶段:密钥协商
客户端生成一个随机对称密钥
客户端用证书中的服务器公钥加密这个对称密钥
服务器用自己的私钥解密,获得对称密钥
第三阶段:数据传输
客户端和服务器使用这个对称密钥进行加密通信
所有后续数据都经过对称加密传输
五、总结
5.1 Cookie与Session
Cookie是存储在客户端的小型文本数据,用于在浏览器和服务器之间传递状态信息。它解决了“客户端如何向服务器证明身份”的问题,但数据存储在客户端,存在安全风险。
Session是存储在服务器端的用户状态数据,通过Session ID与客户端关联。它将敏感数据保留在服务器,客户端只持有一把“钥匙”。
两者协作:Session ID通常通过Cookie传递,Cookie负责“传递钥匙”,Session负责“保管物品”。
5.2 HTTPS
HTTPS = HTTP + SSL/TLS,在HTTP基础上增加了加密层。
对称加密效率高但密钥分发困难;非对称加密安全性高但速度慢。
HTTPS采用混合加密:非对称加密用于证书验证和密钥协商,对称加密用于实际数据传输。
CA证书解决了“公钥来源可信”的问题,数字签名确保证书未被篡改。