HTTP Cookie深入解析:Web会话追踪的秘密

当我们登录了B站过后,为什么下次访问B站就不需要登陆了?

  • 问题:B 站是如何认识我这个登录用户的?
  • 问题:HTTP 是无状态,无连接的,怎么能够记住我?

定义

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

工作原理

  • 用户首访触发 Cookie 下发: 当用户首次访问网站时,服务器会在响应的 HTTP 头部添加 Set-Cookie 字段,以此向用户浏览器发送 Cookie 信息。例如,Set-Cookie: user=zhangsan ,此操作可让服务器为用户标记特定身份或存储相关状态。
  • 浏览器接收并本地存储 Cookie: 浏览器成功接收到服务器发送的 Cookie 后,会将其保存到本地。通常情况下,浏览器会依据域名对 Cookie 进行分类存储,以便后续针对不同域名的网站快速调用相应的 Cookie 信息。
  • 后续请求自动携带 Cookie: 在用户后续访问该网站时,浏览器会自动在 HTTP 请求头部添加 Cookie 字段,将之前存储的 Cookie 信息发送给服务器。这样,服务器就能根据接收到的 Cookie 信息识别用户身份,提供个性化的服务和内容。

分类

  • 会话 Cookie(Session Cookie): 会话 Cookie 的有效期与浏览器的会话周期绑定,当用户关闭浏览器时,会话 Cookie 就会自动失效。

  • 持久 Cookie(Persistent Cookie): 持久 Cookie 与会话 Cookie 不同,它附带了明确的过期日期或指定了持续时间。这使得持久 Cookie 能够在多个浏览器会话之间持续存在,即使浏览器关闭后重新打开,只要未到过期时间,持久 Cookie 依然有效。

持久 Cookie 的存储形式与查看方式

持久 Cookie 以文件的形式存储在与浏览器相关的特定目录下。不过,直接打开这些文件时,你可能会看到乱码或者无法直接读取的内容,这是因为 Cookie 文件通常采用二进制或 SQLite 格式进行存储。若你需要查看持久 Cookie 的内容,无需直接去查看这些文件,只需在浏览器相应的设置选项中就能方便地进行查看。

类似于下面这种方式:

安全性

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

用途

  • 用户认证和会话管理(最重要)
  • 跟踪用户行为
  • 缓存用户偏好等
  • 比如在 chrome 浏览器下,可以直接访问:link

HTTP 协议提供了一个名为 Set-Cookie 的报头选项,专门用于服务器向浏览器设置 Cookie 值。当服务器处理客户端(如浏览器)的请求时,会在 HTTP 响应 报头中添加 Set-Cookie 字段,客户端获取到响应后,会自行解析并保存这些 Cookie 信息。

当客户端(如浏览器)首次向服务器请求资源时,服务器可能会在 HTTP 响应 中添加一个或多个 Set-Cookie 头部。这些 Set-Cookie 头部的作用是指示客户端存储特定的信息,也就是我们所说的 Cookie。每个 Set-Cookie 头部包含了 Cookie 的名称、值以及一些可选属性,具体如下:

  • 过期时间(Expires/Max-Age):用于指定 Cookie 的有效期限。
  • 作用域(Path):定义了 Cookie 生效的路径范围。
  • 安全性要求(Secure):表示该 Cookie 是否只能通过安全的 HTTPS 连接传输。
  • 跨站策略(SameSite):控制 Cookie 在跨站请求时的发送规则。
  • 是否只能通过 HTTP 接口访问(HttpOnly):设置该属性后,Cookie 只能通过 HTTP 请求访问,无法通过 JavaScript 脚本获取,增强了 Cookie 的安全性。

浏览器接收到包含 Set-Cookie 头部的 HTTP 响应后,会对这些头部信息进行解析,并按照其中的指令将 Cookie 存储到本地。存储的 Cookie 不仅包含名称和值,还会记录所有相关的属性。浏览器会根据 Cookie 的过期时间和其他属性来决定何时删除这些 Cookie,以确保 Cookie 数据的有效性和安全性。

当浏览器再次向同一服务器(或符合 Cookie 作用域的其他服务器)发送请求时,会自动检查是否存在与该请求相关的 Cookie。如果有,浏览器会将这些 Cookie 附加到 HTTP 请求的 Cookie 头部,并发送给服务器。服务器接收到请求后,可以从 Cookie 头部中读取这些 Cookie 信息,并根据业务需求进行相应的处理。

基本格式

在这里插入图片描述

完整的 Set-Cookie 示例

在这里插入图片描述

时间格式需严格遵循 RFC 1123 标准,具体格式示例为: Tue, 01 Jan 2030 12:34:56 GMT 或者 UTC(推荐采用 UTC 格式)。

时间格式详细解释

  • Tue:这是英文中“星期二”(Tuesday)的缩写形式,用于表示星期几。
  • 逗号(,):作为分隔符,在格式中起到清晰划分不同时间部分的作用。
  • 01:代表日期,采用两位数表示,如这里的“01”即表示该月的第 1 天。
  • Jan:是英文月份“January”(一月)的缩写。
  • 2030:表示年份,以四位数呈现,精确到具体年份。
  • 12:34:56:表示具体时间,依次为小时、分钟和秒,精确记录时刻。
  • GMT:全称为格林威治标准时间(Greenwich Mean Time),是一种时区缩写。

GMT 与 UTC 的说明

GMT 和 UTC 在国际时间体系中都曾占据重要地位或至今仍有重要意义。不过,鉴于地球自转存在不规则性,而原子钟的计时具有极高精确性,如今 UTC(协调世界时)已成为全球通用的标准时间。相对而言,GMT 更多地作为历史和地理层面的时间参考被提及。

其他可选属性详解

过期时间(expires)

expires=<date> 用于设定 Cookie 的过期日期和时间。若未明确指定该属性,此 Cookie 将默认作为会话 Cookie 处理。也就是说,当用户关闭浏览器时,该 Cookie 便会自动过期失效。

路径限制(path)

path=<some_path> 这一属性能够对 Cookie 可发送至服务器的路径范围加以限制。若未进行额外设置,其默认值为设置该 Cookie 时所在的路径。这意味着,只有当请求的路径与该设置路径相匹配时,浏览器才会在请求中携带此 Cookie。

域名指定(domain)

domain=<domain_name> 用于指定可以接收该 Cookie 的主机范围。若未明确指定,默认情况下,只有设置该 Cookie 的主机能够接收到它。通过合理设置该属性,可以实现让同一域名下的多个子域名共享 Cookie。

安全传输(secure)

secure 属性的作用是确保 Cookie 仅在使用 HTTPS 协议时才会被发送。由于 HTTPS 采用了加密传输技术,能够有效防止 Cookie 在不安全的 HTTP 连接中被恶意截获,从而大大提高了 Cookie 传输的安全性。

防止脚本访问(HttpOnly)

HttpOnly 属性用于标记 Cookie,使其不能被客户端脚本(如 JavaScript)访问。在 Web 应用中,跨站脚本攻击(XSS)是一种常见的安全威胁,攻击者可能通过注入恶意脚本获取用户的 Cookie 信息。而设置 HttpOnly 属性后,能够有效阻止此类攻击,增强了 Cookie 的安全性。

以下是对 Set-Cookie 头部字段的简洁介绍

注意事项

  • 在设置 Cookie 时,每个 Cookie 属性之间需使用分号(;)和空格进行分隔,以此明确区分不同的属性信息。
  • Cookie 的名称和值之间采用等号(=)进行分隔,清晰界定名称与对应的值。
  • 若 Cookie 的名称或值中包含特殊字符,像空格、分号、逗号等,为保证数据的正确传输和解析,需要对其进行 URL 编码处理。
  • 当为 Cookie 设置了 expires 属性时,该 Cookie 将在指定的日期和时间之后过期失效。
  • 若未设置 expires 属性,此 Cookie 将被默认视为会话 Cookie,其生命周期与浏览器会话绑定,即当用户关闭浏览器时,该 Cookie 自动过期。

安全性考虑

  • 在 Cookie 中使用 secure 标志,能够确保 Cookie 仅在使用 HTTPS 连接时进行发送。由于 HTTPS 具备加密传输机制,可有效防止 Cookie 在传输过程中被窃取,从而显著提升 Cookie 的安全性。
  • 添加 HttpOnly 标志可以阻止客户端脚本(如 JavaScript)对 Cookie 进行访问。这一机制能有效防范跨站脚本攻击(XSS),因为攻击者无法通过注入恶意脚本获取 Cookie 信息。
  • 通过合理规划和设置 Set-Cookie 的格式及属性,能够全方位保障 Cookie 的安全性、有效性和可访问性,进而满足不同 Web 应用程序的多样化需求。

测试 cookie 的关键性完整代码全部附在最后。

  • 测试 cookie 写入到浏览器
    resp.AddHeader("Set-Cookie: username=zhangsan;"); //响应中添加一行报头即可

  • 测试自动提交

  • 测试写入过期时间
    • 这里要由我们自己形成 UTC 统一标准时间:
    //时间格式如: expires=Thu, 18 Dec 2024 12:00:00 UTC
    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) // 秒级别的未来UTC时间
    {
        time_t timeout = time(nullptr) + t;
        struct tm *tm = gmtime(&timeout); // 这里不能用localtime,因为localtime是默认带了时区的. gmtime获取的就是UTC统一时间
        char timebuffer[1024];

        //时间格式如: expires=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;
    }

在这里插入图片描述

  • 测试路径 path

提交到非/a/b 路径下

在这里插入图片描述

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

  • 我们写入的是测试数据,如果写入的是用户的私密数据呢?比如,用户名密码,浏览痕迹等。
  • 本质问题在于这些用户私密数据在浏览器(用户端)保存,非常容易被人盗取,更重要的是,除了被盗取,还有就是用户私密数据也就泄漏了。

Cookie测试代码

#pragma once

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

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

// 可以配置的
const std::string homepage = "index.html";
const std::string wwwroot = "./wwwroot";

class HttpRequest
{
public:
    HttpRequest() : _req_blank(HttpSep), _path(wwwroot)
    { }
    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); // \r\n
        str.erase(0, pos + HttpSep.size());
        return true;
    }
    bool Deserialize(std::string &request)
    {
        std::string line;
        bool ok = GetLine(request, &line);
        if (!ok)
            return false;
        _req_line = line;

        while (true)
        {
            bool ok = GetLine(request, &line);
            if (ok && line.empty())
            {
                _req_content = request;
                break;
            }
            else if (ok && !line.empty())
            {
                _req_header.push_back(line);
            }
            else
            {
                break;
            }
        }

        return true;
    }
    ~HttpRequest()
    {}
private:
    // http报文自动
    std::string _req_line; // method url http_version
    std::vector<std::string> _req_header;
    std::string _req_blank;
    std::string _req_content;

    // 解析之后的内容
    std::string _method;
    std::string _url; //    /dira/dirb/x.html   /dira/dirb/XX?usrname=100&&password=1234 /dira/dirb
    std::string _http_version;
    std::string _path;   // "./wwwroot"
    std::string _suffix; // 请求资源的后缀
};

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

class HttpResponse
{
public:
    HttpResponse() : _http_version("HTTP/1.0"), _status_code(200), _status_code_desc("OK"), _resp_blank(LineSep)
    {
    }
    void SetCode(int code)
    {
        _status_code = code;
    }
    void SetDesc(const std::string &desc)
    {
        _status_code_desc = desc;
    }
    void MakeStatusLine()
    {
        _status_line = _http_version + BlankSep + std::to_string(_status_code) + BlankSep + _status_code_desc + LineSep;
    }
    void AddHeader(const std::string &header)
    {
        _resp_header.push_back(header+LineSep);
    }
    void AddContent(const std::string &content)
    {
        _resp_content = content;
    }
    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:
    std::string _status_line;
    std::vector<std::string> _resp_header;
    std::string _resp_blank;
    std::string _resp_content; // body

    // httpversion StatusCode StatusCodeDesc
    std::string _http_version;
    int _status_code;
    std::string _status_code_desc;
};

class Http
{
private:
    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) // 秒级别的未来UTC时间
    {
        time_t timeout = time(nullptr) + t;
        struct tm *tm = gmtime(&timeout); // 这里不能用localtime,因为localtime是默认带了时区的. gmtime获取的就是UTC统一时间
        char timebuffer[1024];

        //时间格式如: expires=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;
    }
public:
    Http(uint16_t port)
    {
        _tsvr = std::make_unique<TcpServer>(port, std::bind(&Http::HandlerHttp, this, std::placeholders::_1));
        _tsvr->Init();
    }
    std::string ProveCookieWrite() // 证明cookie能被写入浏览器
    {
        return "Set-Cookie: username=zhangsan;";
    }
    // resp.AddHeader(ProveCookieWrite()); //测试cookie被写入与自动提交
    std::string ProveCookieTimeOut()
    {
        return "Set-Cookie: username=zhangsan; expires=" + ExpireTimeUseRfc1123(60) + ";"; // 让cookie 1min后过期
    }
    std::string ProvePath()
    {
        return "Set-Cookie: username=zhangsan; path=/a/b;";
    }
    std::string ProveOtherCookie()
    {
        return "Set-Cookie: passwd=1234567890; path=/a/b;";
    }

    std::string HandlerHttp(std::string request)
    {
        HttpRequest req;
        req.Deserialize(request);
        req.DebugHttp();
        lg.LogMessage(Debug, "%s\n", ExpireTimeUseRfc1123(60).c_str());
        HttpResponse resp;
        resp.SetCode(200);
        resp.SetDesc("OK");
        resp.AddHeader("Content-Type: text/html");

        // resp.AddHeader(ProveCookieWrite()); //测试cookie被写入与自动提交
        // resp.AddHeader(ProveCookieTimeOut()); //测试过期时间的写入
        // resp.AddHeader(ProvePath()); // 测试路径
        resp.AddHeader(ProvePath());
        resp.AddHeader(ProveOtherCookie());

        resp.AddContent("<html><h1>helloworld</h1></html>");
        return resp.Serialize();
    }
    void Run()
    {
        _tsvr->Start();
    }
    ~Http()
    {}
private:
    std::unique_ptr<TcpServer> _tsvr;
};

原文阅读