HTTP的Cookie和Session

为什么我打开很多网站,不用再次输入密码?

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 站登录的完整逻辑就是:

  1. 你输入账号密码,B 站服务器验证通过;
  2. 服务器创建 Session,存你的用户信息,生成 Session ID;
  3. 服务器通过Set-Cookie把 Session ID 发给你的浏览器;
  4. 后续你刷视频、发评论,浏览器自动带 Session ID 给 B 站服务器;
  5. 服务器通过 Session ID 查到你的信息,认出你是已登录用户。

6:具体实现

在上一节的HTTP服务器基础上

https://github.com/silin-code/study-code/tree/f1ffdc40b144d018a54d85e4a01a8bb3bfe01fd3/http-server-cpp

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);
}
相关推荐
小明同学012 小时前
linux进程(下)
linux·服务器·c++
pengyi8710152 小时前
共享IP关联风险排查技巧,及时规避封禁隐患
网络·网络协议·tcp/ip
汉克老师2 小时前
GESP2023年12月认证C++三级( 第一部分选择题(1-8))
c++·string·字符数组·gesp三级·gesp3级
PinTrust SSL证书2 小时前
Geotrust企业型OV通配符SSL
网络协议·网络安全·小程序·https·云计算·ssl
俺不要写代码2 小时前
lambda表达式理解
c++·算法
澈2072 小时前
动态内存管理:从基础到实战详解
c++·算法
想唱rap2 小时前
C++11之包装器
服务器·开发语言·c++·算法·ubuntu
wuminyu2 小时前
专家视角看Java的线程是如何run起来的过程
java·linux·c语言·jvm·c++
REDcker2 小时前
C++ std::move实现原理与vector扩容移动语义
开发语言·c++·c