5-1、HTTP cookie与session

目录

[HTTP Cookie 定义](#HTTP Cookie 定义)

工作原理:

分类

安全性

用途

[认识 cookie](#认识 cookie)

基本格式

[完整的 Set-Cookie 示例](#完整的 Set-Cookie 示例)

注意事项

[Cookie 的生命周期](#Cookie 的生命周期)

安全性考虑

[实验测试 Cookie](#实验测试 Cookie)

流程

测试1:Cookie写入

测试2:Cookie自动提交

测试3:Cookie过期时间(expires)

测试4:Cookie路径限制(path)

代码

main.cpp

TcpServer.hpp

HttpServer.hpp

HttpProtocol.hpp

[引入 HTTP Session](#引入 HTTP Session)

定义

工作原理

安全性:

超时和失效:

用途:

实验测试session

Session工作流程

测试1:基本登录流程

测试2:Session自动携带

​编辑

测试3:Session隔离

测试4:Session过期(服务器重启)

​编辑

总结

[1. Session的本质](#1. Session的本质)

[2. Cookie的作用](#2. Cookie的作用)

[3. 安全性考虑](#3. 安全性考虑)

[4. Session的局限性](#4. Session的局限性)

[5. 实际应用](#5. 实际应用)

代码:

Session.hpp

HttpProtocol.hpp

HttpServer.hpp

main.cpp

makefile

总结:


关于登录的场景 - B 站登录和未登录

• 问题:B 站是如何认识我这个登录用户的?

• 问题:HTTP 是无状态,无连接的,怎么能够记住我?

HTTP Cookie(也称为 Web Cookie、浏览器 Cookie 或简称 Cookie)是服务器发送到 用户浏览器并保存在浏览器上的一小块数据 ,它会在浏览器之后向同一服务器再次发 起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一 浏览器,如保持用户的登录状态、记录用户偏好等。

工作原理:

○ 当用户第一次访问网站时,服务器会在响应的 HTTP 头中设置 Set-Cookie 字段,用于发送 Cookie 到用户的浏览器。

○ 浏览器在接收到 Cookie 后,会将其保存在本地(通常是按照域名进行存 储)。

○ 在之后的请求中,浏览器会自动在 HTTP 请求头中携带 Cookie 字段,将之 前保存的 Cookie 信息发送给服务器。

分类

○ 会话 Cookie(Session Cookie):在浏览器关闭时失效。

○ 持久 Cookie(Persistent Cookie):带有明确的过期日期或持续时间, 可以跨多个浏览器会话存在。

○ 如果 cookie 是一个持久性的 cookie,那么它其实就是浏览器相关的,特 定目录下的一个文件。但直接查看这些文件可能会看到乱码或无法读取的内容, 因为 cookie 文件通常以二进制或 sqlite 格式存储。一般我们查看,直接在浏览 器对应的选项中直接查看即可

安全性

• 由于 Cookie 是存储在客户端的,因此存在被篡改或窃取的风险。

用途

○ 用户认证和会话管理(最重要)

○ 跟踪用户行为

○ 缓存用户偏好等

○ 比如在 chrome 浏览器下,可以直接访问:chrome://settings/cookies

○ HTTP 存在一个报头选项:Set-Cookie, 可以用来进行给浏览器设置 Cookie 值。

○ 在 HTTP 响应头中添加,客户端(如浏览器)获取并自行设置并保存 Cookie。

基本格式

cpp 复制代码
Set-Cookie: <name>=<value>
其中 <name> 是 Cookie 的名称,<value> 是 Cookie 的值
cpp 复制代码
Set-Cookie: username=peter; expires=Thu, 18 Dec 2024 12:00:00
UTC; path=/; domain=.example.com; secure; HttpOnly

时间格式必须遵守 RFC 1123 标准,具体格式样例:Tue, 01 Jan 2030 12:34:56 GMT 或者 UTC(推荐)。

关于时间解释

○ Tue: 星期二(星期几的缩写)

○ ,: 逗号分隔符

○ 01: 日期(两位数表示)

○ Jan: 一月(月份的缩写)

○ 2030: 年份(四位数)

○ 12:34:56: 时间(小时、分钟、秒)

○ GMT: 格林威治标准时间(时区缩写)

GMT vs UTC了解即可

GMT(格林威治标准时间)和 UTC(协调世界时)是两个不同的时间标准,但它们 在大多数情况下非常接近,常常被混淆。以下是两者的简单解释和区别:

  1. GMT(格林威治标准时间):

○ GMT 是格林威治标准时间的缩写,它是以英国伦敦的格林威治区为基准 的世界时间标准。 ○ GMT 不受夏令时或其他因素的影响,通常用于航海、航空、科学、天文 等领域。

○ GMT 的计算方式是基于地球的自转和公转。

  1. UTC(协调世界时):

○ UTC 全称为"协调世界时",是国际电信联盟(ITU)制定和维护的标准时 间。

○ UTC 的计算方式是基于原子钟,而不是地球的自转,因此它比 GMT 更准 确。据称,世界上最精确的原子钟 50 亿年才会误差 1 秒。

○ UTC 是现在用的时间标准,多数全球性的网络和软件系统将其作为标准 时间。

区别:

• 计算方式:GMT 基于地球的自转和公转,而 UTC 基于原子钟。

• 准确度:由于 UTC 基于原子钟,它比基于地球自转的 GMT 更加精确。 在实际使用中,GMT 和 UTC 之间的差别通常很小,大多数情况下可以互换使用。但 在需要高精度时间计量的场合,如科学研究、网络通信等,UTC 是更为准确的选择

cpp 复制代码
Set-Cookie: username=peter; expires=Thu, 18 Dec 2024 12:00:00
UTC; path=/; domain=.example.com; secure; HttpOnly

关于其他可选属性的解释

○ expires= :设置 Cookie 的过期日期/时间。如果未指定此属 性,则 Cookie 默认为会话 Cookie,即当浏览器关闭时过期。

○ path= :限制 Cookie 发送到服务器的哪些路径。默认 为设置它的路径。

○ domain= :指定哪些主机可以接受该 Cookie。默 认为设置它的主机。

○ secure :仅当使用 HTTPS 协议时才发送 Cookie。这有助于防止 Cookie 在不安全的 HTTP 连接中被截获

○ HttpOnly :标记 Cookie 为 HttpOnly,意味着该 Cookie 不能被 客户端脚本(如 JavaScript)访问。这有助于防止跨站脚本攻击(XSS)。

以下是对 Set-Cookie头部字段的简洁描述

属性 描述
username peter 这是 Cookie 的名称和值,标识用户名为 "peter"。
expires Thu, 18 Dec 2024 12:00:00 UTC 指定 Cookie 的过期时间。在这个例子中,Cookie 将在 2024 年 12 月 18 日 12:00:00 UTC 后过期。
path / 定义 Cookie 的作用范围。这里设置为根路径 /,意味着 Cookie 对 .example.com 域名下的所有路径都可用。
domain .example.com 指定哪些域名可以接收这个 Cookie。点前缀 (.) 表示包括所有子域名。
secure - 指示 Cookie 只能通过 HTTPS 协议发送,不能通过 HTTP 协议发送,增加安全性。
HttpOnly - 阻止客户端脚本(如 JavaScript)访问此 Cookie,有助于防止跨站脚本攻击 (XSS)。

注意事项

○ 每个 Cookie 属性都以分号(;)和空格( )分隔。

○ 名称和值之间使用等号(=)分隔。

○ 如果 Cookie 的名称或值包含特殊字符(如空格、分号、逗号等),则需要 进行 URL 编码。

○ 如果设置了 expires 属性,则 Cookie 将在指定的日期/时间后过期。

○ 如果没有设置 expires 属性,则 Cookie 默认为会话 Cookie,即当浏览器 关闭时过期。

安全性考虑

○ 使用 secure 标志可以确保 Cookie 仅在 HTTPS 连接上发送,从而提高安 全性。

○ 使用 HttpOnly 标志可以防止客户端脚本(如 JavaScript)访问 Cookie, 从而防止 XSS 攻击。

○ 通过合理设置 Set-Cookie 的格式和属性,可以确保 Cookie 的安全性、有效 性和可访问性,从而满足 Web 应用程序的需求

通过一个精简的HTTP服务器,演示Cookie的核心功能:

  • Cookie的设置(Set-Cookie)

  • Cookie的自动提交

  • Cookie的过期时间(expires)

  • Cookie的路径限制(path)

通过这个小实验掌握以下内容:

  1. 理解Cookie的工作原理

  2. 掌握Set-Cookie响应头的使用

  3. 理解expires属性的作用

  4. 理解path属性的作用

  5. 能够实现简单的Cookie管理

项目结构:

main.cpp # 主程序入口

HttpServer.hpp # HTTP服务器(核心逻辑)

HttpProtocol.hpp # HTTP协议解析(请求/响应)

TcpServer.hpp # TCP服务器(网络通信)

Makefile # 编译脚本

流程

  1. Cookie写入流程
复制代码
浏览器请求 /login
    ↓
服务器处理请求
    ↓
构造响应头:Set-Cookie: username=zhangsan
    ↓
发送响应给浏览器
    ↓
浏览器保存Cookie
  1. Cookie自动提交流程
复制代码
浏览器访问其他页面(如/check)
    ↓
浏览器检查是否有该域名的Cookie
    ↓
如果有,自动添加到请求头:Cookie: username=zhangsan
    ↓
服务器接收请求,从Cookie头中提取信息
  1. Set-Cookie属性
属性 说明 示例
name=value Cookie名称和值 username=zhangsan
expires 过期时间(RFC1123格式) expires=Thu, 18 Dec 2024 12:00:00 UTC
path 路径限制 path=/a/b
domain 域名限制 domain=.example.com
Secure 仅HTTPS发送 Secure
HttpOnly 禁止JS访问 HttpOnly

单独使用 Cookie,有什么问题?

• 我们写入的是测试数据,如果写入的是用户的私密数据呢?比如,用户名密码, 浏览痕迹等。

• 本质问题在于这些用户私密数据在浏览器(用户端)保存,非常容易被人盗取,更 重要的是,除了被盗取,还有就是用户私密数据也就泄漏了。

测试1:Cookie写入

测试路径/login

功能:服务器通过Set-Cookie响应头将Cookie写入浏览器

操作步骤

  1. 访问 http://localhost:8080/login

  2. 打开浏览器开发者工具(F12)

  3. 查看Application → Cookies

  4. 观察是否出现新的Cookie

测试2:Cookie自动提交

测试路径 :任意路径(如/check

功能:浏览器自动携带Cookie发送给服务器

操作步骤

  1. 先访问 /login 设置Cookie

  2. 然后访问 http://localhost:8080/check

  3. 观察服务器输出的请求头

预期结果: 服务器应该输出:

复制代码
请求头: Cookie: username=zhangsan

这说明浏览器自动携带了Cookie!


测试3:Cookie过期时间(expires)

测试路径/login(需修改代码)

功能:设置Cookie的过期时间

操作步骤

  1. 修改 HttpServer.hpp 中的代码:
cpp 复制代码
// 取消这行的注释
resp.AddHeader(ProveCookieTimeOut());
​
// 注释掉这行
// resp.AddHeader(ProveCookieWrite());
  1. 重新编译:make

  2. 重新运行:./cookie_server 8080

  3. 访问 /login

  4. 等待60秒

  5. 刷新页面,观察Cookie是否消失

预期结果

  • 设置后立即:Cookie存在

  • 60秒后:Cookie自动消失

代码解析

cpp 复制代码
std::string ProveCookieTimeOut() {
    // 60秒后过期
    return "Set-Cookie: username=zhangsan; expires=" +
           ExpireTimeUseRfc1123(60) + ";";
}
​
// 时间格式:Thu, 18 Dec 2024 12:00:00 UTC
std::string ExpireTimeUseRfc1123(int t) {
    time_t timeout = time(nullptr) + t;
    // 必须用gmtime(),不能用localtime()!
    struct tm *tm = gmtime(&timeout);
    // ... 生成RFC1123格式时间
}

注意

  • 必须使用 gmtime() 获取UTC时间

  • localtime() 返回本地时间,会导致Cookie过期时间错误

  • HTTP协议要求使用UTC时间


测试4:Cookie路径限制(path)

测试路径/login(需修改代码)

功能:限制Cookie只在特定路径下提交

操作步骤

  1. 修改 HttpServer.hpp 中的代码:
cpp 复制代码
// 取消这行的注释
resp.AddHeader(ProvePath());
​
// 注释掉其他两行
// resp.AddHeader(ProveCookieWrite());
// resp.AddHeader(ProveCookieTimeOut());
  1. 重新编译:make

  2. 重新运行:./cookie_server 8080

  3. 访问 /login(设置Cookie,路径为/a/b)

  4. 测试不同路径:

访问路径 是否提交Cookie 原因
/a/b/test 路径匹配
/a/b 路径匹配
/a/x 路径不匹配
/ 路径不匹配
/check 路径不匹配

预期结果

  • 访问 /a/b/test:服务器输出 Cookie: username=zhangsan

  • 访问 /a/x:服务器输出中没有Cookie

代码解析

cpp 复制代码
std::string ProvePath() {
    // 设置路径为/a/b
    return "Set-Cookie: username=zhangsan; path=/a/b;";
}

代码

main.cpp

cpp 复制代码
/*
 * HTTP Cookie 实验 - 主程序入口
 *
 * 用法: ./cookie_server [端口号]
 * 默认端口8080
 * 浏览器访问 http://localhost:8080/login 开始测试
 */

#include <iostream>
#include <string>
#include "HttpServer.hpp"

int main(int argc, char* argv[]) {
    uint16_t port = 8080;

    if (argc > 1) {
        port = std::stoi(argv[1]);
    }

    HttpServer server(port);
    server.Run();

    return 0;
}

TcpServer.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>

// 回调函数类型,用来处理HTTP请求
using HttpHandler = std::function<std::string(std::string)>;

class TcpServer {
public:
    TcpServer(uint16_t port, HttpHandler handler)
        : _port(port), _listen_fd(-1), _handler(handler) {}

    ~TcpServer() {
        if (_listen_fd >= 0)
            close(_listen_fd);
    }

    bool Init() {
        // 创建tcp socket
        _listen_fd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_fd < 0) {
            std::cerr << "创建socket失败" << std::endl;
            return false;
        }

        // 端口复用,不然重启的时候会报 address already in use
        int opt = 1;
        setsockopt(_listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

        // 绑定ip和端口
        struct sockaddr_in addr;
        memset(&addr, 0, sizeof(addr));
        addr.sin_family = AF_INET;
        addr.sin_port = htons(_port);
        addr.sin_addr.s_addr = INADDR_ANY;

        if (bind(_listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
            std::cerr << "绑定地址失败" << std::endl;
            close(_listen_fd);
            return false;
        }

        // 开始监听
        if (listen(_listen_fd, 5) < 0) {
            std::cerr << "监听失败" << std::endl;
            close(_listen_fd);
            return false;
        }

        std::cout << "TCP服务器启动在端口 " << _port << std::endl;
        return true;
    }

    // 主循环,不断accept新连接
    void Start() {
        while (true) {
            struct sockaddr_in client_addr;
            socklen_t client_len = sizeof(client_addr);
            int client_fd = accept(_listen_fd, (struct sockaddr*)&client_addr, &client_len);

            if (client_fd < 0) {
                std::cerr << "接受连接失败" << std::endl;
                continue;
            }

            std::cout << "新连接: " << inet_ntoa(client_addr.sin_addr)
                      << ":" << ntohs(client_addr.sin_port) << std::endl;

            HandleClient(client_fd);
            close(client_fd);
        }
    }

private:
    // 读请求、调回调、发响应
    void HandleClient(int client_fd) {
        char buffer[4096];
        memset(buffer, 0, sizeof(buffer));
        ssize_t bytes_read = recv(client_fd, buffer, sizeof(buffer) - 1, 0);

        if (bytes_read <= 0) {
            return;
        }

        std::string request(buffer, bytes_read);

        // 调用HttpServer里注册的回调来处理请求
        std::string response = _handler(request);

        send(client_fd, response.c_str(), response.length(), 0);
    }

    uint16_t _port;
    int _listen_fd;
    HttpHandler _handler;
};

HttpServer.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <memory>
#include <ctime>
#include <vector>
#include <sstream>
#include "TcpServer.hpp"
#include "HttpProtocol.hpp"

class HttpServer {
public:
    HttpServer(uint16_t port) : _port(port) {}
    ~HttpServer() {}

    void Run() {
        TcpServer *tsvr = new TcpServer(_port,
            std::bind(&HttpServer::HandlerHttp, this, std::placeholders::_1));

        if (!tsvr->Init()) {
            delete tsvr;
            return;
        }

        std::cout << "\nHTTP Cookie 实验服务器" << std::endl;
        std::cout << "访问 http://localhost:" << _port << "/login 开始测试\n" << std::endl;

        tsvr->Start();
        delete tsvr;
    }

private:
    // 返回月份英文缩写,给expires用的
    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];
    }

    // 生成RFC1123格式的UTC过期时间
    // 参数t是秒数,表示多少秒后过期
    // 注意这里必须用gmtime,不能用localtime,因为HTTP要求UTC时间
    std::string ExpireTimeUseRfc1123(int t) {
        time_t timeout = time(nullptr) + t;

        // 用gmtime拿UTC时间,localtime带时区会导致过期时间不对
        struct tm *tm = gmtime(&timeout);

        char timebuffer[1024];
        // 输出格式: "Thu, 18 Dec 2024 12:00:00 UTC"
        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;
    }

    // 测试1: 简单写入cookie,浏览器会保存并在后续请求里自动带上
    std::string ProveCookieWrite() {
        return "Set-Cookie: username=zhangsan;";
    }

    // 测试2: 带过期时间的cookie,60秒后自动失效
    std::string ProveCookieTimeOut() {
        return "Set-Cookie: username=zhangsan; expires=" +
               ExpireTimeUseRfc1123(60) + ";";
    }

    // 测试3: 带路径限制的cookie,只在/a/b路径下才会提交
    std::string ProvePath() {
        return "Set-Cookie: username=zhangsan; path=/a/b;";
    }

    // HTTP请求处理的总入口
    std::string HandlerHttp(std::string request) {
        HttpRequest req;
        HttpResponse resp;

        req.Deserialize(request);
        req.Parse();

        // 如果想看浏览器发了什么,把下面这行取消注释
        // req.DebugHttp();

        std::string url = req.Url();

        // /login - 测试cookie写入
        if (url == "/login") {
            std::cout << "[测试] 访问 /login - 测试Cookie写入" << std::endl;

            resp.SetCode(200);
            resp.SetDesc("OK");
            resp.AddHeader("Content-Type: text/html; charset=utf-8");

            // 通过注释/取消注释来切换测试功能
            resp.AddHeader(ProveCookieWrite());       // 测试cookie写入与自动提交
            // resp.AddHeader(ProveCookieTimeOut());   // 测试过期时间
            // resp.AddHeader(ProvePath());            // 测试路径限制

            resp.AddContent("<html><h1>Cookie已设置!</h1>"
                           "<p>刷新页面查看Cookie自动提交</p>"
                           "<p><a href='/check'>查看Cookie</a></p>"
                           "<p><a href='/a/b/test'>测试路径限制</a></p>"
                           "</html>");
        }
        // /check - 看浏览器有没有自动带上cookie
        else if (url == "/check") {
            std::cout << "[测试] 访问 /check - 查看Cookie自动提交" << std::endl;

            resp.SetCode(200);
            resp.SetDesc("OK");
            resp.AddHeader("Content-Type: text/html; charset=utf-8");

            std::string content = "<html><h1>Cookie检查</h1>";

            // 如果有sessionid就显示出来
            std::string sessionid = req.SessionId();
            if (!sessionid.empty()) {
                content += "<p>sessionid: " + sessionid + "</p>";
            }

            content += "<p>浏览器自动提交了Cookie!</p>";
            content += "<p><a href='/login'>重新设置Cookie</a></p>";
            content += "</html>";

            resp.AddContent(content);
        }
        // /a/b/* 路径 - 测试path属性
        else if (url.find("/a/b") == 0) {
            std::cout << "[测试] 访问 " << url << " - 测试路径限制" << std::endl;

            resp.SetCode(200);
            resp.SetDesc("OK");
            resp.AddHeader("Content-Type: text/html; charset=utf-8");

            std::string content = "<html><h1>路径测试: " + url + "</h1>";
            content += "<p>如果你设置了path=/a/b的Cookie,这里应该能看到</p>";
            content += "<p><a href='/login'>重新设置Cookie</a></p>";
            content += "<p><a href='/a/x/test'>测试其他路径</a></p>";
            content += "</html>";

            resp.AddContent(content);
        }
        // 其他路径,给个导航页
        else {
            std::cout << "[测试] 访问 " << url << std::endl;

            resp.SetCode(200);
            resp.SetDesc("OK");
            resp.AddHeader("Content-Type: text/html; charset=utf-8");

            std::string content = "<html><h1>Cookie实验服务器</h1>";
            content += "<p>可用的测试路径:</p>";
            content += "<ul>";
            content += "<li><a href='/login'>/login</a> - 设置Cookie</li>";
            content += "<li><a href='/check'>/check</a> - 查看Cookie</li>";
            content += "<li><a href='/a/b/test'>/a/b/test</a> - 测试路径限制</li>";
            content += "</ul>";
            content += "</html>";

            resp.AddContent(content);
        }

        return resp.Serialize();
    }

    uint16_t _port;
};

HttpProtocol.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <sstream>
#include <vector>
#include <map>

const std::string HttpSep = "\r\n";
const std::string BlankSep = " ";

// HTTP请求类,负责解析浏览器发来的请求
class HttpRequest {
public:
    HttpRequest() {}

    // 反序列化:把原始请求字符串拆成请求行、请求头等
    bool Deserialize(std::string &request) {
        std::string line;

        // 先解析请求行,比如 "GET /login HTTP/1.1"
        if (!GetLine(request, &line))
            return false;
        _req_line = line;

        // 循环解析请求头,遇到空行就结束
        while (true) {
            if (!GetLine(request, &line))
                break;

            // 空行表示头部结束,剩下的就是请求体
            if (line.empty()) {
                _req_content = request;
                break;
            }

            _req_header.push_back(line);
        }

        return true;
    }

    // 解析请求行和Cookie
    void Parse() {
        // 从请求行里拆出 method、url、http版本
        std::istringstream ss(_req_line);
        ss >> _method >> _url >> _http_version;

        // 从请求头里找 Cookie 那一行
        // Cookie格式: "Cookie: name1=value1; name2=value2"
        std::string prefix = "Cookie: ";
        for (auto &line : _req_header) {
            if (strncmp(line.c_str(), prefix.c_str(), prefix.size()) == 0) {
                std::string cookie = line.substr(prefix.size());
                _cookies.push_back(cookie);
                break;
            }
        }

        // 再从cookie里提取sessionid
        prefix = "sessionid=";
        for (const auto &cookie : _cookies) {
            if (strncmp(cookie.c_str(), prefix.c_str(), prefix.size()) == 0) {
                _sessionid = cookie.substr(prefix.size());
                break;
            }
        }
    }

    std::string Url() { return _url; }
    std::string Method() { return _method; }
    std::string SessionId() { return _sessionid; }

    // 调试用,打印请求内容
    void DebugHttp() {
        std::cout << "请求行: " << _req_line << std::endl;
        for (auto &line : _req_header) {
            std::cout << "请求头: " << line << std::endl;
        }
    }

    ~HttpRequest() {}

private:
    // 按 \r\n 拆分,每次取出一行
    bool GetLine(std::string &str, std::string *line) {
        auto pos = str.find(HttpSep);
        if (pos == std::string::npos)
            return false;
        *line = str.substr(0, pos);
        str.erase(0, pos + HttpSep.size());
        return true;
    }

    // 原始数据
    std::string _req_line;
    std::vector<std::string> _req_header;
    std::string _req_content;

    // 解析后的数据
    std::string _method;
    std::string _url;
    std::string _http_version;
    std::vector<std::string> _cookies;
    std::string _sessionid;
};

// HTTP响应类,负责构造返回给浏览器的响应
class HttpResponse {
public:
    HttpResponse() : _resp_blank(HttpSep), _http_version("HTTP/1.1"),
        _status_code(200), _status_code_desc("OK") {}

    void SetCode(int code) { _status_code = code; }
    void SetDesc(const std::string &desc) { _status_code_desc = desc; }

    // 添加响应头,比如 "Content-Type: text/html" 或 "Set-Cookie: xxx"
    void AddHeader(const std::string &header) {
        _resp_header.push_back(header + HttpSep);
    }

    void AddContent(const std::string &content) { _resp_content = content; }

    // 序列化成完整的HTTP响应字符串
    std::string Serialize() {
        MakeStatusLine();
        std::string response_str = _status_line;

        for (auto &header : _resp_header) {
            response_str += header;
        }

        response_str += _resp_blank;  // 空行分隔头部和体部
        response_str += _resp_content;

        return response_str;
    }

    ~HttpResponse() {}

private:
    // 拼状态行: "HTTP/1.1 200 OK\r\n"
    void MakeStatusLine() {
        _status_line = _http_version + BlankSep +
                       std::to_string(_status_code) + BlankSep +
                       _status_code_desc + HttpSep;
    }

    std::string _status_line;
    std::vector<std::string> _resp_header;
    std::string _resp_blank;
    std::string _resp_content;

    std::string _http_version;
    int _status_code;
    std::string _status_code_desc;
};

引入 HTTP Session

定义

HTTP Session 是服务器用来跟踪用户与服务器交互期间用户状态的机制。由于 HTTP 协议是无状态的(每个请求都是独立的),因此服务器需要通过 Session 来记住用户 的信息。

工作原理

当用户首次访问网站时,服务器会为用户创建一个唯一的 Session ID,并通过 Cookie 将其发送到客户端。 客户端在之后的请求中会携带这个 Session ID,服务器通过 Session ID 来识 别用户,从而获取用户的会话信息。

服务器通常会将 Session 信息存储在内存、数据库或缓存中

安全性:

与 Cookie 相似,由于 Session ID 是在客户端和服务器之间传递的,因此也存 在被窃取的风险。

但是一般虽然 Cookie 被盗取了,但是用户只泄漏了一个 Session ID,私密信息 暂时没有被泄露的风险

Session ID 便于服务端进行客户端有效性的管理,比如异地登录。 可以通过 HTTPS 和设置合适的 Cookie 属性(如 HttpOnly 和 Secure)来增强安 全性。

超时和失效:

Session 可以设置超时时间,当超过这个时间后,Session 会自动失效。

服务器也可以主动使 Session 失效,例如当用户登出时。

用途:

用户认证和会话管理

存储用户的临时数据(如购物车内容)

实现分布式系统的会话共享(通过将会话数据存储在共享数据库或缓存中)

实验测试session

Session工作流程

复制代码
1. 用户访问 /login
   ↓
2. 服务器创建Session对象(存储用户信息)
   ↓
3. 服务器生成唯一的sessionid
   ↓
4. 服务器通过Set-Cookie将sessionid发送给浏览器
   ↓
5. 浏览器保存Cookie
   ↓
6. 用户访问其他页面(如/index)
   ↓
7. 浏览器自动携带sessionid Cookie
   ↓
8. 服务器解析Cookie,获取sessionid
   ↓
9. 服务器通过sessionid查找Session对象
   ↓
10. 服务器识别用户身份
测试1:基本登录流程
  1. 打开浏览器访问 http://localhost:8080/login

  2. 观察服务器输出:创建了新用户和sessionid

  3. 浏览器会跳转到首页,显示用户信息

  4. 查看浏览器Cookie,应该能看到sessionid

测试2:Session自动携带
  1. 登录后,直接访问 http://localhost:8080/index

  2. 服务器应该能识别你的身份

  3. 这是因为浏览器自动携带了sessionid Cookie

测试3:Session隔离
  1. 用Chrome浏览器登录

  2. 用Edge浏览器登录

  3. 观察服务器输出:两个浏览器有不同的用户和sessionid

  4. 这说明Session是按浏览器隔离的

测试4:Session过期(服务器重启)
  1. 登录后,保持浏览器打开

  2. 重启服务器(Ctrl+C然后重新运行)

  3. 刷新浏览器页面

  4. 观察:服务器提示Session已过期

  5. 原因:服务器重启后内存中的Session丢失了

总结

1. Session的本质

  • Session是服务器端的会话管理机制

  • 客户端只持有Session ID,不存储敏感信息

  • 服务器通过Session ID识别用户身份

2. Cookie的作用

  • Cookie是Session的载体

  • 服务器通过Set-Cookie设置Cookie

  • 浏览器自动携带Cookie发送给服务器

3. 安全性考虑

  • Session ID应该足够随机,难以猜测

  • 可以使用HTTPS加密传输

  • 设置HttpOnly防止JavaScript访问Cookie

  • 设置Secure只在HTTPS下发送Cookie

4. Session的局限性

  • 服务器重启会导致Session丢失

  • 分布式系统需要共享Session(如使用Redis)

  • Session存储在内存中,有内存限制

5. 实际应用

  • 用户登录状态管理

  • 购物车功能

  • 权限控制

  • 用户行为跟踪

代码:

文件 说明
Session.hpp Session类和SessionManager类
HttpProtocol.hpp HTTP请求/响应解析类
HttpServer.hpp HTTP服务器核心逻辑
main.cpp 程序入口
Makefile 编译脚本

Session.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <memory>
#include <ctime>
#include <unordered_map>
#include <random>
#include <sstream>
#include <iomanip>

// Session类:存储用户会话信息
class Session {
public:
    Session(const std::string& username, const std::string& status)
        : _username(username), _status(status) {
        _create_time = time(nullptr);
    }

    ~Session() {}

    // 获取用户信息
    std::string GetUsername() const { return _username; }
    std::string GetStatus() const { return _status; }
    uint64_t GetCreateTime() const { return _create_time; }

private:
    std::string _username;
    std::string _status;
    uint64_t _create_time;
};

using session_ptr = std::shared_ptr<Session>;

// SessionManager类:管理Session的创建和获取
class SessionManager {
public:
    SessionManager() {
        // 初始化随机数种子
        std::random_device rd;
        _gen.seed(rd());
    }

    // 添加Session,返回sessionid
    std::string AddSession(session_ptr s) {
        // 生成唯一的sessionid:随机数+时间戳
        uint32_t random_id = _dist(_gen);
        std::stringstream ss;
        ss << std::hex << std::time(nullptr) << random_id;
        std::string session_id = ss.str();

        _sessions[session_id] = s;
        return session_id;
    }

    // 获取Session
    session_ptr GetSession(const std::string& session_id) {
        auto it = _sessions.find(session_id);
        if (it == _sessions.end()) {
            return nullptr;  // Session不存在或已过期
        }
        return it->second;
    }

    // 删除Session(用于登出)
    void RemoveSession(const std::string& session_id) {
        _sessions.erase(session_id);
    }

    ~SessionManager() {}

private:
    std::unordered_map<std::string, session_ptr> _sessions;
    std::mt19937 _gen;
    std::uniform_int_distribution<uint32_t> _dist;
};

HttpProtocol.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <sstream>
#include <vector>
#include <map>
#include <functional>

const std::string HttpSep = "\r\n";
const std::string BlankSep = " ";

// HTTP请求类
class HttpRequest {
public:
    HttpRequest() {}

    // 解析HTTP请求
    bool Parse(const std::string& request) {
        std::istringstream iss(request);
        std::string line;

        // 解析请求行:GET /path HTTP/1.1
        if (std::getline(iss, line)) {
            // 去除末尾的\r
            if (!line.empty() && line.back() == '\r') {
                line.pop_back();
            }
            std::istringstream line_ss(line);
            line_ss >> _method >> _url >> _http_version;
        }

        // 解析请求头
        while (std::getline(iss, line)) {
            // 去除末尾的\r
            if (!line.empty() && line.back() == '\r') {
                line.pop_back();
            }

            // 空行表示头部结束
            if (line.empty()) {
                break;
            }

            // 解析Cookie头
            if (line.find("Cookie: ") == 0) {
                std::string cookie_str = line.substr(8);  // 跳过"Cookie: "
                ParseCookie(cookie_str);
            }
        }

        return true;
    }

    // 获取请求方法
    std::string GetMethod() const { return _method; }

    // 获取请求URL
    std::string GetUrl() const { return _url; }

    // 获取sessionid
    std::string GetSessionId() const {
        auto it = _cookies.find("sessionid");
        if (it != _cookies.end()) {
            return it->second;
        }
        return "";
    }

    // 获取指定Cookie值
    std::string GetCookie(const std::string& name) const {
        auto it = _cookies.find(name);
        if (it != _cookies.end()) {
            return it->second;
        }
        return "";
    }

private:
    // 解析Cookie字符串
    void ParseCookie(const std::string& cookie_str) {
        std::istringstream iss(cookie_str);
        std::string token;

        while (std::getline(iss, token, ';')) {
            // 去除首尾空格
            size_t start = token.find_first_not_of(" \t");
            size_t end = token.find_last_not_of(" \t");

            if (start != std::string::npos && end != std::string::npos) {
                std::string cookie = token.substr(start, end - start + 1);
                size_t eq_pos = cookie.find('=');
                if (eq_pos != std::string::npos) {
                    std::string name = cookie.substr(0, eq_pos);
                    std::string value = cookie.substr(eq_pos + 1);
                    _cookies[name] = value;
                }
            }
        }
    }

    std::string _method;
    std::string _url;
    std::string _http_version;
    std::map<std::string, std::string> _cookies;
};

// HTTP响应类
class HttpResponse {
public:
    HttpResponse() : _status_code(200), _status_desc("OK") {}

    // 设置状态码
    void SetStatusCode(int code) {
        _status_code = code;
        switch (code) {
            case 200: _status_desc = "OK"; break;
            case 301: _status_desc = "Moved Permanently"; break;
            case 404: _status_desc = "Not Found"; break;
            case 500: _status_desc = "Internal Server Error"; break;
            default: _status_desc = "Unknown"; break;
        }
    }

    // 添加响应头
    void AddHeader(const std::string& key, const std::string& value) {
        _headers[key] = value;
    }

    // 设置响应体
    void SetBody(const std::string& body) {
        _body = body;
    }

    // 序列化为HTTP响应字符串
    std::string Serialize() const {
        std::ostringstream oss;

        // 状态行
        oss << "HTTP/1.1 " << _status_code << " " << _status_desc << "\r\n";

        // 响应头
        for (const auto& header : _headers) {
            oss << header.first << ": " << header.second << "\r\n";
        }

        // 空行分隔头部和体部
        oss << "\r\n";

        // 响应体
        oss << _body;

        return oss.str();
    }

private:
    int _status_code;
    std::string _status_desc;
    std::map<std::string, std::string> _headers;
    std::string _body;
};

HttpServer.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <memory>
#include <functional>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include "Session.hpp"
#include "HttpProtocol.hpp"

class HttpServer {
public:
    HttpServer(uint16_t port) : _port(port), _listen_fd(-1) {
        _session_manager = std::make_unique<SessionManager>();
    }

    ~HttpServer() {
        if (_listen_fd >= 0) {
            close(_listen_fd);
        }
    }

    // 初始化服务器
    bool Init() {
        // 创建socket
        _listen_fd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_fd < 0) {
            std::cerr << "创建socket失败" << std::endl;
            return false;
        }

        // 设置端口复用
        int opt = 1;
        setsockopt(_listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

        // 绑定地址
        struct sockaddr_in addr;
        memset(&addr, 0, sizeof(addr));
        addr.sin_family = AF_INET;
        addr.sin_port = htons(_port);
        addr.sin_addr.s_addr = INADDR_ANY;

        if (bind(_listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
            std::cerr << "绑定地址失败" << std::endl;
            close(_listen_fd);
            return false;
        }

        // 监听
        if (listen(_listen_fd, 5) < 0) {
            std::cerr << "监听失败" << std::endl;
            close(_listen_fd);
            return false;
        }

        std::cout << "HTTP服务器启动在端口 " << _port << std::endl;
        return true;
    }

    // 运行服务器
    void Run() {
        while (true) {
            // 接受连接
            struct sockaddr_in client_addr;
            socklen_t client_len = sizeof(client_addr);
            int client_fd = accept(_listen_fd, (struct sockaddr*)&client_addr, &client_len);

            if (client_fd < 0) {
                std::cerr << "接受连接失败" << std::endl;
                continue;
            }

            std::cout << "新连接: " << inet_ntoa(client_addr.sin_addr) << ":" << ntohs(client_addr.sin_port) << std::endl;

            // 处理请求
            HandleClient(client_fd);
            close(client_fd);
        }
    }

private:
    // 处理客户端请求
    void HandleClient(int client_fd) {
        // 读取请求
        char buffer[4096];
        memset(buffer, 0, sizeof(buffer));
        ssize_t bytes_read = recv(client_fd, buffer, sizeof(buffer) - 1, 0);

        if (bytes_read <= 0) {
            return;
        }

        std::string request(buffer, bytes_read);

        // 解析请求
        HttpRequest req;
        if (!req.Parse(request)) {
            SendResponse(client_fd, 400, "Bad Request", "请求解析失败");
            return;
        }

        std::cout << "请求: " << req.GetMethod() << " " << req.GetUrl() << std::endl;

        // 处理不同路由
        std::string url = req.GetUrl();

        if (url == "/login") {
            HandleLogin(client_fd, req);
        } else if (url == "/index" || url == "/") {
            HandleIndex(client_fd, req);
        } else if (url == "/logout") {
            HandleLogout(client_fd, req);
        } else {
            HandleNotFound(client_fd);
        }
    }

    // 处理登录请求
    void HandleLogin(int client_fd, const HttpRequest& req) {
        std::string session_id = req.GetSessionId();

        // 检查是否已经登录
        if (!session_id.empty()) {
            session_ptr session = _session_manager->GetSession(session_id);
            if (session) {
                // 已经登录,跳转到首页
                SendRedirect(client_fd, "/index");
                return;
            }
        }

        // 新用户登录,创建Session
        static int user_count = 0;
        std::string username = "user-" + std::to_string(user_count++);

        session_ptr new_session = std::make_shared<Session>(username, "online");
        session_id = _session_manager->AddSession(new_session);

        std::cout << "用户 " << username << " 登录,sessionid: " << session_id << std::endl;

        // 构建响应,设置Cookie
        HttpResponse resp;
        resp.SetStatusCode(200);
        resp.AddHeader("Content-Type", "text/html; charset=utf-8");
        resp.AddHeader("Set-Cookie", "sessionid=" + session_id + "; Path=/; HttpOnly");
        resp.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate");

        std::string body = R"(
<!DOCTYPE html>
<html>
<head>
    <title>登录成功</title>
    <meta charset="utf-8">
</head>
<body>
    <h1>欢迎, )" + username + R"(!</h1>
    <p>你已成功登录,Session ID: )" + session_id + R"(</p>
    <p><a href="/index">访问首页</a></p>
    <p><a href="/logout">退出登录</a></p>
</body>
</html>)";

        resp.SetBody(body);
        SendHttpResponse(client_fd, resp);
    }

    // 处理首页请求
    void HandleIndex(int client_fd, const HttpRequest& req) {
        std::string session_id = req.GetSessionId();

        if (session_id.empty()) {
            // 未登录,跳转到登录页
            SendRedirect(client_fd, "/login");
            return;
        }

        // 检查Session是否存在
        session_ptr session = _session_manager->GetSession(session_id);
        if (!session) {
            // Session已过期或不存在
            HttpResponse resp;
            resp.SetStatusCode(200);
            resp.AddHeader("Content-Type", "text/html; charset=utf-8");

            std::string body = R"(
<!DOCTYPE html>
<html>
<head>
    <title>Session已过期</title>
    <meta charset="utf-8">
</head>
<body>
    <h1>Session已过期</h1>
    <p>你的Session已过期或无效,请重新登录。</p>
    <p>原因可能是:服务器重启导致Session丢失,或Cookie中的sessionid已失效。</p>
    <p><a href="/login">重新登录</a></p>
</body>
</html>)";

            resp.SetBody(body);
            SendHttpResponse(client_fd, resp);
            return;
        }

        // 已登录,显示用户信息
        HttpResponse resp;
        resp.SetStatusCode(200);
        resp.AddHeader("Content-Type", "text/html; charset=utf-8");

        std::string body = R"(
<!DOCTYPE html>
<html>
<head>
    <title>首页</title>
    <meta charset="utf-8">
</head>
<body>
    <h1>欢迎回来, )" + session->GetUsername() + R"(!</h1>
    <p>用户状态: )" + session->GetStatus() + R"(</p>
    <p>Session ID: )" + session_id + R"(</p>
    <p>登录时间: )" + std::to_string(session->GetCreateTime()) + R"(</p>
    <p><a href="/logout">退出登录</a></p>
</body>
</html>)";

        resp.SetBody(body);
        SendHttpResponse(client_fd, resp);
    }

    // 处理登出请求
    void HandleLogout(int client_fd, const HttpRequest& req) {
        std::string session_id = req.GetSessionId();

        if (!session_id.empty()) {
            session_ptr session = _session_manager->GetSession(session_id);
            if (session) {
                std::cout << "用户 " << session->GetUsername() << " 登出" << std::endl;
            }
            _session_manager->RemoveSession(session_id);
        }

        // 清除Cookie,跳转到登录页
        // 先删除服务端Session,再通过Set-Cookie清除浏览器Cookie
        HttpResponse resp;
        resp.SetStatusCode(302);
        resp.AddHeader("Location", "/login");
        // 使用Expires比Max-Age更可靠,设置为过去的时间确保浏览器立即删除Cookie
        resp.AddHeader("Set-Cookie", "sessionid=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly");
        // 防止缓存,确保Set-Cookie头被浏览器处理
        resp.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate");
        resp.AddHeader("Pragma", "no-cache");

        std::string response_str = resp.Serialize();
        send(client_fd, response_str.c_str(), response_str.length(), 0);
    }

    // 处理404
    void HandleNotFound(int client_fd) {
        SendResponse(client_fd, 404, "Not Found", "页面不存在");
    }

    // 发送HTTP响应
    void SendHttpResponse(int client_fd, const HttpResponse& resp) {
        std::string response_str = resp.Serialize();
        send(client_fd, response_str.c_str(), response_str.length(), 0);
    }

    // 发送简单响应
    void SendResponse(int client_fd, int code, const std::string& desc, const std::string& body) {
        HttpResponse resp;
        resp.SetStatusCode(code);
        resp.AddHeader("Content-Type", "text/html; charset=utf-8");
        resp.SetBody("<h1>" + desc + "</h1><p>" + body + "</p>");
        SendHttpResponse(client_fd, resp);
    }

    // 发送重定向
    void SendRedirect(int client_fd, const std::string& url) {
        HttpResponse resp;
        resp.SetStatusCode(302);
        resp.AddHeader("Location", url);
        std::string response_str = resp.Serialize();
        send(client_fd, response_str.c_str(), response_str.length(), 0);
    }

    uint16_t _port;
    int _listen_fd;
    std::unique_ptr<SessionManager> _session_manager;
};

main.cpp

cpp 复制代码
#include <iostream>
#include "HttpServer.hpp"

int main(int argc, char* argv[]) {
    uint16_t port = 8080;

    if (argc > 1) {
        port = std::stoi(argv[1]);
    }

    HttpServer server(port);

    if (!server.Init()) {
        std::cerr << "服务器初始化失败" << std::endl;
        return 1;
    }

    std::cout << "=== HTTP Session 演示服务器 ===" << std::endl;
    std::cout << "访问 http://localhost:" << port << "/login 开始测试" << std::endl;
    std::cout << "按 Ctrl+C 停止服务器" << std::endl;

    server.Run();

    return 0;
}

makefile

cpp 复制代码
CXX = g++
CXXFLAGS = -std=c++14 -Wall -Wextra -g
TARGET = http_server

.PHONY: all clean

all: $(TARGET)

$(TARGET): main.cpp HttpServer.hpp HttpProtocol.hpp Session.hpp
	$(CXX) $(CXXFLAGS) -o $(TARGET) main.cpp -lpthread

clean:
	rm -f $(TARGET)

# 运行服务器
run: $(TARGET)
	./$(TARGET) 8080

# 测试说明
help:
	@echo "使用方法:"
	@echo "  make          # 编译项目"
	@echo "  make run      # 运行服务器"
	@echo "  make clean    # 清理编译文件"
	@echo ""
	@echo "测试步骤:"
	@echo "  1. 运行 make run 启动服务器"
	@echo "  2. 打开浏览器访问 http://localhost:8080/login"
	@echo "  3. 观察Session的创建和使用"
	@echo "  4. 尝试用不同浏览器登录,观察Session隔离"
	@echo "  5. 重启服务器后访问,观察Session丢失现象"

总结:

HTTP Cookie 和 Session 都是用于在 Web 应用中跟踪用户状态的机制。Cookie 是存 储在客户端的,而 Session 是存储在服务器端的。它们各有优缺点,通常在实际应用 中会结合使用,以达到最佳的用户体验和安全性。

相关推荐
时空无限2 小时前
ubuntu 修改 journal 日志保存目录
linux·运维·服务器·journal
Better Bench2 小时前
Ubuntu 22.04系统中解决运行CC-Switch-v3.16.1-Linux-x86_64.AppImage中文乱码
linux·ubuntu·claude·claude code·cc-switch
A_humble_scholar2 小时前
Linux (一)入门指南:历史、常用指令、权限与文件属性详解
linux·运维·服务器
skywalk81632 小时前
在Ubuntu安装言律并部署playground web网页
linux·运维·ubuntu
zzqssliu2 小时前
Taocarts库存锁定机制优化:彻底解决跨境代购商品超卖问题
java·linux·javascript·php
Peace2 小时前
【Ansible】
linux·运维·ansible
kebidaixu2 小时前
AD7606B 使用总结报告
linux
zhangfeng11332 小时前
htop命令根据实际Linux环境下的讲解,结合国家超算中心hpc
linux·运维·服务器
‎ദ്ദിᵔ.˛.ᵔ₎2 小时前
linux基础开发工具
linux