为什么我打开很多网站,不用再次输入密码?
HTTP协议不是无状态,无连接的吗?服务器凭什么记住我?
1:HTTP为什么需要Cookie/Session
在认识Cookie和Session之前,必须先知道HTTP的无状态特性:
HTTP协议每一次请求都是独立的,互补管理的,服务器不会记住上一次请求的用户信息。
举个例子:你第一次访问 B 站输入账号密码登录 → 服务器验证通过;你刷新页面再发请求 → 服务器完全不认识你,又要你重新登录。
这显然不符合日常使用习惯,所以我们需要让服务器记住用户的机制 ------ 这就是 Cookie 和 Session 的核心作用。
2:Cookie
1:什么是Cookie
HTTP Cookie(浏览器小饼干):服务器给浏览器的一小块数据,浏览器存在本地,下次访问同一服务器时,会自动把这块数据带给服务器,帮服务器识别用户。
Cookie = 服务器给你的专属小纸条,你揣在兜里(浏览器本地),每次去这家店(访问服务器),都主动把小纸条给店员看,店员就认出你了。
2:Cookie的工作流程
- 首次请求 :你第一次访问网站,服务器在响应头里加
Set-Cookie,把小纸条发给浏览器; - 本地存储:浏览器收下小纸条,按域名存到本地;
- 后续请求 :再访问这个网站,浏览器自动在请求头加
Cookie,把小纸条带给服务器。
3:Cookie的两种类型
- 会话 Cookie :没设置过期时间,关闭浏览器就失效;
- 持久 Cookie:设置了过期时间,关闭浏览器还在,跨会话保留。
4:Cookie核心属性
| 属性 | 作用 |
|---|---|
name=value |
Cookie 的名字和内容,核心标识 |
expires |
设置过期时间,决定 Cookie 活多久 |
path |
限制 Cookie 只在某个路径下生效 |
secure |
只在 HTTPS 安全连接下传输 |
HttpOnly |
禁止 JS 脚本读取,防 XSS 攻击 |
5:Cookie的致命问题
Cookie 存在浏览器本地 ,数据完全暴露,容易被篡改、窃取。如果直接把用户名、密码存在 Cookie 里,隐私会直接泄露!这就是我们需要Session的原因。
3:Session
1:什么是session
HTTP Session:服务器用来存储用户状态的机制,数据存在服务器,只给服务器发一个唯一编号(Session ID)。
简单来说:
Session = 服务器的用户花名册,只记载服务器自己手里;
Session ID= 小纸条上的编号,浏览器只存这个编号,不存隐私数据。
2:Session的工作流程
1:首次登录:服务器验证通过,创建专属 Session,生成唯一 Session ID;
2:下发编号:服务器通过 Cookie 把Session ID发给浏览器;
3:本地存编号:浏览器只存 Session ID,隐私数据全在服务器;
4:后续请求:浏览器带 Session ID → 服务器查花名册 → 认出用户。
3:Session的核心特点
- 数据存在服务器端,隐私更安全;
- 可设置超时时间,长时间不操作自动失效;
- 用户登出时,服务器可直接销毁 Session,强制失效。
4:Cookie和Session的对比
| 对比维度 | Cookie | Session |
|---|---|---|
| 存储位置 | 客户端(浏览器) | 服务器端 |
| 存储内容 | 少量文本数据 | 大量用户隐私数据 |
| 安全性 | 低(易被窃取) | 高(仅传编号) |
| 生命周期 | 可持久存储 | 依赖会话 / 超时 |
| 存储大小 | 有限(约 4KB) | 无严格限制 |
核心结论:Cookie和Session必须结合使用--Cookie负责传Session ID,Session负责存用户数据,兼顾识别能力和安全性、
5:真实场景
回到开头的问题,B 站登录的完整逻辑就是:
- 你输入账号密码,B 站服务器验证通过;
- 服务器创建 Session,存你的用户信息,生成 Session ID;
- 服务器通过
Set-Cookie把 Session ID 发给你的浏览器; - 后续你刷视频、发评论,浏览器自动带 Session ID 给 B 站服务器;
- 服务器通过 Session ID 查到你的信息,认出你是已登录用户。
6:具体实现
在上一节的HTTP服务器基础上
1:CookieUtil.h
cpp
#ifndef COOKIEUTIL_H
#define COOKIEUTIL_H
#include <string>
#include <unordered_map>
// ########################### 新手注释 ###########################
// Cookie 工具类:专门处理 HTTP 协议里f的 Cookie
// 功能1:解析浏览器发过来的 Cookie
// 功能2:构建服务器发给浏览器的 Set-Cookie 响应头
// ################################################################
/* 为什么要单独写CookieUtil?
* 1. 解耦:服务器不用管Cookie怎么解析,只需要调用工具类的方法
* 2. 可复用:以后其他项目需要处理Cookie,直接复制这个类就行
* 3. 好维护:Cookie的格式变了,只改这个类,不影响服务器
*/
class CookieUtil {
public:
// 解析请求头里的所有 Cookie,返回键值对
// 参数:request 就是浏览器发给服务器的完整HTTP请求
// 返回:键值对形式的Cookie,比如{"SESSIONID": "abc123"}
static std::unordered_map<std::string, std::string> parseCookies(const std::string& request);
// 构建Set-Cookie响应头
// 参数:key是Cookie的名字(比如SESSIONID),value是值(SessionID),maxAge是过期时间(秒)
// 比如构建出来的结果是:Set-Cookie: SESSIONID=abc123; Path=/; HttpOnly; Max-Age=3600
static std::string buildSetCookie(const std::string& key, const std::string& value, int maxAge = 3600);
// 构建清除 Cookie 的响应头(让浏览器删掉这个 Cookie)
static std::string buildClearCookie(const std::string& key);
};
#endif
2:CookieUtil.cpp
cpp
#include "../include/CookieUtil.h"
#include <sstream>
#include <algorithm>
/*
* 解析Cookie:从HTTP请求头里找"Cookie: "字段,然后拆成键值对
* 举个例子,浏览器发的请求里有这么一段:
* Cookie: SESSIONID=abc123; username=test
* 这个函数会把它解析成:{"SESSIONID": "abc123", "username": "test"}
*/
// 解析请求里的所有Cookie
std::unordered_map<std::string, std::string> CookieUtil::parseCookies(const std::string &request)
{
std::unordered_map<std::string, std::string> cookieMap;
// 1:找请求头的"Cookie:" 行
size_t cookiePos = request.find("Cookie:");
// npos表示string查找失败的返回值通常是-1
if (cookiePos == std::string::npos)
return cookieMap;
// 2:找到这一行的结尾(HTTP头每行以\r\n结束)
size_t endPos = request.find("\r\n", cookiePos);
if (endPos == std::string::npos)
return cookieMap;
// 3:取出Cookie的内容,比如"SESSION=abc123;username=test"
std::string cookieStr = request.substr(cookiePos + 8, endPos - (cookiePos + 8));
// 4::按照分好分割成键值对
std::stringstream ss(cookieStr);
std::string pair;
while (std::getline(ss, pair, ';'))
{
// 去掉前后空格
pair.erase(0, pair.find_first_not_of(" "));
pair.erase(pair.find_last_not_of(" ")+1);
if (pair.empty())
continue;
// 按照等号划分key和value
size_t eqPos = pair.find("=");
if (eqPos == std::string::npos)
continue;
std::string key = pair.substr(0, eqPos);
std::string value = pair.substr(eqPos + 1);
cookieMap[key] = value;
}
return cookieMap;
}
/*
* 构建Set-Cookie响应头(服务器给浏览器发小票)
* 每个参数的作用(新手必懂):
* - key/value:Cookie的名字和值,比如SESSIONID=abc123
* - Path=/:表示这个Cookie对网站所有路径都有效
* - HttpOnly:禁止JS读取Cookie,防止黑客偷Cookie(防XSS攻击,安全必备)
* - Max-Age=3600:Cookie1小时后过期,浏览器会自动删掉
*/
// 构建Set-Cookie 响应头
std::string CookieUtil::buildSetCookie(const std::string &key, const std::string &value, int maxAge)
{
// path=/: 这个cookie在整个网站都有效
// HttpOnly: 禁止 JS 读取Cookie,防止 XSS 偷Cookie
// Max-Age: Cookie有效期,单位秒,过期自动删除
return "Set-Cookie:" + key + "=" + value +
"; Path=/; HttpOnly; Max-Age=" + std::to_string(maxAge) + "\r\n";
}
/*
* 清除Cookie:把Cookie的过期时间设为0,浏览器收到就会立刻删掉这个Cookie
* 登出的时候调用,让浏览器的小票作废
*/
// 清楚Cookie:把有效期设为0,浏览器立刻删除
std::string CookieUtil::buildClearCookie(const std::string &key)
{
return "Set-Cookie:" + key + "=; Path=/; HttpOnly; Max-Age=0\r\n";
}
3:SessionManager.h
cpp
#ifndef SESSION_MANAGER_H
#define SESSION_MANAGER_H
#include <string>
#include <sstream>
#include <unordered_map>
#include <mutex>
#include <ctime>
#include <random>
/*
* Session里存的用户数据结构
* 你可以在这里加你需要的用户信息,比如用户ID、权限等级等
*/
struct SessionData
{
std::string username; // 用户名(示例数据,登录时用)
time_t expireTime; // Session的过期时间戳,到时间就作废
};
/*
* 为什么要用单例模式?
* 整个服务器里,Session池只能有一个,所有请求都共用这一个「客户信息表」
* 单例模式能保证全局只有一个实例,不会重复创建多个Session池
*/
class SessionManager
{
public:
// 获取全局唯一的SessionManager实例
static SessionManager& getInstance();
// 禁止拷贝和赋值,防止别人复制出多个实例
SessionManager(const SessionManager &) = delete;
SessionManager &operator=(const SessionManager &) = delete;
// 创建新的Session(用户登录时调用)
// 参数:username是用户名,timeout是Session过期时间(秒,默认1小时)
// 返回:生成的唯一SessionID(给Cookie用的)
std::string createSession(const std::string &username, int timeout = 3600);
// 校验Session是否有效(用户访问页面时调用)
// 参数:sessionId是从Cookie里拿到的编号
// 返回:true表示Session有效,用户已登录;false表示无效/过期
bool isSessionValid(const std::string &sessionId);
// 从事Session里面获取用户名(登录后显示信息用)
std::string getUsername(const std::string &sessionId);
// 销毁Session(用户登出时调用,把服务器里的用户信息删掉)
void destroySession(const std::string &sessionId);
// 清理所有过期的Session(防止Session池越来越大,占内存)
void clearExpiredsession();
private:
//私有构造函数,外部不能new,只能通过getInstance()获取
SessionManager() = default;
//生成唯一的SessionId(时间戳+随机数,保证不重复)
std::string generateSessionId();
// 服务器的Session池(客户信息表)
// key:SessionID(小票编号),value:用户信息+过期时间
std::unordered_map<std::string ,SessionData> sessions_;
//互斥锁,多线程处理时,防止多个线程同时修改sessions_导致数据错误;
std::mutex mutex_;
};
#endif
4:SessionManager.cpp
cpp
#include "../include/SessionManager.h"
/*
* 单例模式的核心:静态局部变量,第一次调用时初始化,之后一直用这个实例
* 这是C++里线程安全的单例写法,新手不用深究,记住这个模板就行
*/
SessionManager& SessionManager::getInstance()
{
static SessionManager instance;
return instance;
}
/*
* 生成唯一的SessionID:时间戳+16位随机数,保证不会重复
* 为什么不用纯随机?防止两个用户同时登录生成相同的ID
*/
std::string SessionManager::generateSessionId()
{
// 1获取当前时间戳(秒),确保时间上不重复
time_t now = time(nullptr);
std::stringstream ss;
ss << now << "_";
//2生成16位十六进制随机数,保证随机不重复
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(0,15);
for(int i=0;i<16;i++)
{
ss<<std::hex<<dis(gen);
}
return ss.str();
}
/*
* 创建Session:用户登录时调用,生成一个新的Session存在服务器里
* 用std::lock_guard自动加锁,函数结束自动解锁,不用手动管理锁
*/
std::string SessionManager::createSession(const std::string& username,int timeout)
{
//枷锁,防止多个用户同时登录,同时修改session_导致数据错乱
std::lock_guard<std::mutex> lock(mutex_);
//1生成唯一sessionId
std::string sessionId =generateSessionId();
//2填充session数据:用户名+过期时间(当前时间+timeout秒)
SessionData data;
data.username = username;
data.expireTime = time(nullptr) +timeout;
//3存到服务器的Session池里
sessions_[sessionId] =data;
return sessionId;
}
/*
* 校验Session是否有效:用户每次访问页面时,都要先校验Session
* 会自动清理过期的Session,不用单独调用clearExpiredSessions
*/
bool SessionManager::isSessionValid(const std::string& sessionId)
{
std::lock_guard<std::mutex> lock(mutex_);
//1先看SessionId在不在池子里
auto it =sessions_.find(sessionId);
if(it==sessions_.end())
{
return false;
}
//2再看Session有没有过期
if(it->second.expireTime<time(nullptr))
{
//过期了
sessions_.erase(it);
return false;
}
//没过期
return true;
}
/*
* 从Session里获取用户名,登录后显示欢迎信息用
*/
std::string SessionManager::getUsername(const std::string& sessionId)
{
std::lock_guard<std::mutex> lock(mutex_);
if(sessions_.count(sessionId))
{
return sessions_[sessionId].username;
}
return "";
}
/*
* 销毁Session:用户登出时调用,把服务器里的Session删掉
*/
void SessionManager::destroySession(const std::string& sessionId)
{
std::lock_guard<std::mutex> lock(mutex_);
sessions_.erase(sessionId);
}
/*
* 清理所有过期的Session:防止Session池越来越大,占内存
* 你可以在服务器启动后开个定时线程调用这个方法,这里我们简化处理,在校验时自动清理
*/
void SessionManager::clearExpiredsession()
{
std::lock_guard<std::mutex> lock(mutex_);
time_t now = time(nullptr);
for (auto it = sessions_.begin(); it != sessions_.end();) {
if (it->second.expireTime < now) {
// 过期了,删掉
it = sessions_.erase(it);
} else {
++it;
}
}
}
5:第二版HttpServer.cpp
cpp
#include "../include/HttpServer.h"
// 新增Cookie和Session
#include "../include/CookieUtil.h"
#include "../include/SessionManager.h"
#include <iostream>
#include <fstream>
#include <sstream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 缓冲区大小(读取浏览器请求)
#define BUFFER_SIZE 4096
// 构造函数,初始化端口
HttpServer::HttpServer(int port) : port(port), server_fd(-1) {}
// 获取服务器公网IP
std::string HttpServer::getPublicIP()
{
char buffer[128] = {0};
// 调用Linux命令查看公网IP
FILE *fp = popen("curl -s ifconfig.me", "r");
if (fp)
{
fgets(buffer, sizeof(buffer), fp);
pclose(fp);
}
if (strlen(buffer) == 0)
{
return "请手动查询公网IP";
}
return std::string(buffer);
}
// 1/初始化socket Tcp服务器(HTTP基于TCP)
bool HttpServer::initSocket()
{
// 1:创建socket文件描述符
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1)
{
std::cerr << "Socket 创建失败" << std::endl;
return false;
}
// 2:设置端口复用(防止重启报错)
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
// 3:绑定服务器IP和端口
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(port);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0)
{
std::cerr << "端口绑定失败" << std::endl;
return false;
}
// 4:监听端口(最大连接数5)
if (listen(server_fd, 5) < 0)
{
std::cerr << "监听失败" << std::endl;
return false;
}
// 自动打印公网IP
std::string ip = getPublicIP();
std::cout << "================================" << std::endl;
std::cout << "服务器启动成功,监听窗口:" << port << std::endl;
std::cout << "你的服务器公网IP:" << ip << std::endl;
std::cout << "浏览器访问地址:http://:" << ip << ":" << port << std::endl;
std::cout << "================================" << std::endl;
return true;
}
// 前端处理函数
// 修改readFrontEndFile,加上错误日志,方便排查
std::string HttpServer::readFrontEndFile(const std::string &filename)
{
std::string path = "web/" + filename;
std::cout << "尝试读取文件: " << path << std::endl; // 加日志!
std::ifstream file(path);
if (!file.is_open())
{
std::cerr << "文件打开失败: " << path << std::endl; // 错误日志
return "<h1>文件未找到!</h1>";
}
std::stringstream buffer;
buffer << file.rdbuf();
file.close();
std::cout << "文件读取成功,大小: " << buffer.str().size() << " 字节" << std::endl;
return buffer.str();
}
// // 处理客户端请求//版本1,不需要cookie和session
// void HttpServer::handleClient(int client_fd)
// {
// char buffer[BUFFER_SIZE] = {0};
// read(client_fd, buffer, sizeof(buffer));
// // 打印请求
// std::cout << "\n====浏览器HTTP请求====\n"
// << buffer << "\n=====================\n";
// // 1:读取独立前端页面
// std::string html = readFrontEndFile();
// // 2:标准HTTP响应协议
// std::string response =
// "HTTP/1.1 200 OK\r\n"
// "Content-Type: text/html; charset=utf-8\r\n"
// "Connection: close\r\n"
// "\r\n" +
// html;
// // 发送给浏览器
// write(client_fd, response.c_str(), response.size());
// close(client_fd);
// }
// 升级版本2,使用Cookie和Session
void HttpServer::handleClient(int client_fd)
{
char buffer[BUFFER_SIZE] = {0};
read(client_fd, buffer, sizeof(buffer));
std::string request(buffer);
// 打印完整请求,方便查看cookie和请求头
std::cout << "\n====浏览器HTTP请求====\n"
<< buffer << "\n=====================\n";
//-------------------------First Step,解析cookie
// 用CookieUtil从请求里面解析出所有cookie,比如SESSIONID=???
auto cookies = CookieUtil::parseCookies(request);
// 从Cookie里拿到SESSIONID,如果有
std::string sessionId = cookies["SESSIONID"];
// 获取全局唯一的Session管理器
SessionManager &sessionMgr = SessionManager::getInstance();
//------------------------Second Step,判断请求路径,处理不同业务
std::string response;
// 场景1,用户访问登录页面(GET /login.html)
size_t loginPos = request.find("GET /login.html");
std::cout << "查找GET /login.html的位置: " << loginPos << std::endl; // 新增日志
if (loginPos != std::string::npos)
{
std::cout << "✅ 成功匹配到 /login.html 请求" << std::endl;
std::string html = readFrontEndFile("login.html");
std::cout << "📄 读取到的HTML内容长度: " << html.size() << std::endl;
response = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Connection: close\r\n"
"\r\n" +
html;
}
// 场景2,用户提交登录请求(POST /login)
else if (request.find("POST /login") != std::string::npos)
{
// 这里简化处理,直接用固定用户名"test_user",你可以改成从POST请求里读用户名密码
std::string username = "test_user";
// 1. 调用SessionManager创建新的Session,生成SessionID
std::string newSessionId = sessionMgr.createSession(username);
// 2. 调用CookieUtil构建Set-Cookie响应头,把SessionID发给浏览器
std::string setCookieHeader = CookieUtil::buildSetCookie("SESSIONID", newSessionId);
// 3. 登录成功,用302重定向跳转到首页(浏览器收到302会自动跳转到Location指定的地址)
response = "HTTP/1.1 302 Found\r\n" + setCookieHeader + "Location: /\r\n" // 跳转到首页
+ "\r\n";
}
// 场景3:用户点击登出(GET /logout)
else if (request.find("GET /logout") != std::string::npos)
{
// 1. 调用SessionManager销毁Session
sessionMgr.destroySession(sessionId);
// 2. 调用CookieUtil构建清除Cookie的响应头,让浏览器删掉SESSIONID
std::string clearCookieHeader = CookieUtil::buildClearCookie("SESSIONID");
// 3. 重定向到登录页
response = "HTTP/1.1 302 Found\r\n" + clearCookieHeader + "Location: /login.html\r\n" + "\r\n";
}
// 场景4:用户访问首页(GET /)
else if (request.find("GET / HTTP") != std::string::npos || request.find("GET /?") != std::string::npos)
{
// 校验Session是否有效(用户是否登录)
if (sessionMgr.isSessionValid(sessionId))
{
// 已登录:获取用户名,显示欢迎信息
std::string username = sessionMgr.getUsername(sessionId);
// 读取index.html,把用户名替换进去(或者直接写死,这里简化处理)
std::string html = "<h1>Hello World! 🎉</h1>"
"<p>欢迎你," +
username + "!你已经成功登录啦~</p>"
"<a href='/logout'>点击这里登出</a>";
response = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Connection: close\r\n"
"\r\n" +
html;
}
else
{
// 未登录:重定向到登录页
response = "HTTP/1.1 302 Found\r\n"
"Location: /login.html\r\n"
"\r\n";
}
}
// 场景5:其他请求(比如访问不存在的页面),返回404
else
{
response = "HTTP/1.1 404 Not Found\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Connection: close\r\n"
"\r\n<h1>404 页面不存在</h1>";
}
// 把响应发给浏览器
write(client_fd, response.c_str(), response.size());
close(client_fd);
}
// 启动服务器
void HttpServer::start()
{
if (!initSocket())
return;
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
while (true)
{
int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
std::cout << "新链接:" << inet_ntoa(client_addr.sin_addr) << std::endl;
handleClient(client_fd);
}
}
// 关闭服务器
void HttpServer::stop()
{
if (server_fd != -1)
close(server_fd);
}