Cookie 和 Session

HTTP cookie(web cookie、browser cookie)是服务器发送给用户 web 浏览器的一小段数据。浏览器可能会存储 cookie,并在以后的请求中将其发送回同一台服务器。通常,HTTP cookie 用于判断两个请求是否来自同一个浏览器 - 例如,保持用户登录。它为无状态 HTTP 协议记住有状态信息。

cookie 主要用于三个目的:

  • 会话管理:登录、购物车、游戏分数或其它服务器需要保存的数据。
  • 个性化:用户首选项、主体和其它设置。
  • 跟踪:记录和分析用户行为。

示例:当你登录一次哔哩哔哩软件时,使用完成之后关闭掉该网站,下次开启电脑再次访问该网站时,你的账号依然是已经登录的状态。这实际上就是使用 cookie 实现的,如下所示,点击浏览器地址栏左侧的锁标志,你就可以看到对于网站的各种 cookie 数据了。

你真的了解 Cookie 和 Session 吗?-LMLPHP

用户在通过身份验证并且服务器成功设置了 cookie 之后,服务器会将该 cookie 作为 HTTP 响应的一部分发送给浏览器。浏览器收到响应后会自动解析 cookie 的值,并将其保存在浏览器的 cookie 存储中。这样,用户的身份信息就会以加密的形式是保存在浏览器的 cookie 文件中。

当用户在之后的请i去中访问同一网站时,浏览器会自动将响应的 cookie 信息加到请求中的 cookie 标头中,并发送给服务器。服务器可以通过这些 cookie 信息来识别用户并提供个性化的服务,例如保持用户的登录状态。

你真的了解 Cookie 和 Session 吗?-LMLPHP

浏览器会自动管理 cookie 的生命周期和安全性。如果你删除该网站对应的 cookie 信息,那么下次再访问时,就需要重新进行账号登录了。

注意:要查看存储的 cookie(以及网页可以使用的其它存储),你可以开启开发人员哦工具中的存储检测器并从存储树中选择 cookie。

如果你存储在浏览器中的 cookie 信息被未经授权的用户盗取,那么该用户就可以使用你的 cookie 信息以你的身份来访问你之前访问过的网站。(例如:QQ被盗,身份泄密等)这种情况都称为 cookie 被盗取。

恶意链接是一种常见的网络攻击方式,黑客可以通过欺骗用户点击恶意链接来窃取其Cookie。以下是一个示例:假设您收到一封看似来自银行的电子邮件,邮件内容称您的账户存在异常,需要您点击链接以确认身份。然而,该链接实际上是一个恶意链接,当您点击时,黑客将会收到您的 Cookie 信息。一旦黑客获取了您的 Cookie,他们可以使用它来伪装成您,访问您的账户,进行各种恶意活动,如盗取个人信息、进行非法转账等。

  • Session 代表了服务器和客户端之间的一次会话过程。在 Web 应用程序中,当用户与服务器建立会话时,服务器会为该用户创建一个唯一的 Session 对象。该 Session 对象用于存储特定用户会话所需的属性和配置信息。
  • 在整个用户会话中,当用户在应用程序的不同 Web 页面之间跳转时,存储在 Session 对象中的变量和数据不会丢失,而是持续存在。这使得应用程序能够跟踪和管理用户的状态和数据,以提供个性化的服务和功能。
  • 当客户端关闭会话(例如关闭浏览器)或者 Session 超时失效时,会话结束,Session 对象中的数据也随之销毁。会话超时时间通常由应用程序的配置决定,以平衡用户体验和服务器资源的利用。
  • 通过使用 Session,应用程序可以在用户会话期间保持持久性数据,并确保用户的状态和信息不会再页面跳转或重新加载时丢失。这对于实现登录认证、购物车功能、个性化设置都非常有用。
  • 存储位置:Cookie 保存在客户端(浏览器)中,而 Session 保存在服务器端。
  • 存储内容:Cookie 只能保存 ASCII 字符,而 Session 可以存储任意数据类。通常情况下,我们可以在 Session 中保存一些常用的变量信息,如用户ID 等。
  • 有效期:Cookie 可以设置长时间保持,比如经常使用的默认登录功能。Session 的有效时间相对较短,通常在客户端关闭或 Session 超时后会失效。
  • 隐私策略:由于 Cookie 存储在客户端,相对容易收到非法获取的风险。在早期,有些网站将个人信息存储在 Cookie 中,导致信息被盗取。而 Session 数据存储在服务器中,相对来说安全性更高。
  • 存储大小:单个 Cookie 的存储容量有限,一般不超过 4KB。而 Session 可以存储的数据量远远超过 Cookie 的限制,因为它存储在服务器的内存或数据库中。

为什么需要使用 Cookie,这就要从浏览器说起,我们知道浏览器是没有状态的(HTTP 协议无状态),这意味着浏览器无法记住用户信息,这时就需要使用一个机制来告诉服务器,本次操作用户是否登录以及是哪一个用户等。而仅仅使用 Cookie 来存储用户数据存在安全风险,一旦 Cookie 文件泄漏,用户的隐私数据也会泄漏。因此,为了增加安全性,那这套机制的实现就需要 Cookie 和 Session 的配合。

你真的了解 Cookie 和 Session 吗?-LMLPHP

在用户首次请求服务器时,服务器会根据用户提交的相关信息生成一个对应的 Session,并将 Session 的唯一标识信息(SessionID)返回给浏览器,浏览器收到 SessionID 之后,会将其存储在 Cookie 中并记录此 SessionID 属于哪个域名。

当用户再次访问服务器时,浏览器会自动判断此域名是否存在 Cookie 信息,并在请求中自动发送 Cookie 信息给服务器。服务器会从 Cookie 中获取 SessionID,并根据 SessionID 查找对应的 Session 信息。如果没有找到 Session,说明用户没有登录或登录失效;如果找到了 Session,就标识用户已登录,服务器可以根据 Session 中的信息执行后续的操作。

安全是相对的,没有绝对的安全性。无论是使用账号密码直接发送到网络中,还是使用 SessionID 进行身份认证,都存在一定的风险。即使使用加密等安全措施,也无法完全消除所有的潜在威胁。

安全性评估通常会考虑破解成本与收益之间的关系。如果破解某个信息的成本非常高,远远大于攻击获取信息带来的收益,那么这个信息就是相对安全的。因此,在设计安全系统时,我们尽量提高攻击者获取有价值信息的成本,以增加安全性。

对于使用 Session 的方式,尽管 SessionID 可能会被盗取,但相比直接在每次请求中发送账号和密码信息,它降低了账号密码被泄漏的风险。

当浏览器访问服务器时,若服务器给浏览器的 HTTP 响应中包含 Set-Cookie 字段,那么浏览器再次访问该服务器时就会携带上该 Cookie 字段。

如下所示,当浏览器访问服务器时,我们给响应的报头中添加上一个 Set-Cookie 字段,测试当浏览器第二次访问该服务器时会不会携带上该 Cookie 字段。

#define CRLF "\r\n"
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define HOME_PAGE "index.html"
#define ROOT_PATH "wwwroot"

string getPath(string http_request)
{
    size_t pos = http_request.find(CRLF);
    if (pos == string::npos)
        return "";
    string request_line = http_request.substr(0, pos);
    // GET /a/b/c http/1.1
    size_t first = request_line.find(SPACE);
    if (first == string::npos)
        return "";
    size_t second = request_line.rfind(SPACE);
    if (second == string::npos)
        return "";

    string path = request_line.substr(first + SPACE_LEN, second - (first + SPACE_LEN));
    if (path.size() == 1 && path[0] == '/')
        path += HOME_PAGE;

    return path;
}

string readFile(const string &recource)
{
    ifstream in(recource, std::ifstream::binary);

    if (!in.is_open())
        return "404";
    string content;
    string line;
    while (getline(in, line))
        content += line;
    in.close();

    return content;
}

void handlerHttpRequest(int sock)
{
    cout << "---------------------------------------------------" << endl;
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof buffer);
    if (s > 0)
        cout << buffer;

    string path = getPath(buffer);
    std::string recource = ROOT_PATH;
    recource += path;

    string html = readFile(recource);
    size_t pos = recource.rfind(".");
    string suffix = recource.substr(pos);
  
    // 开始响应
    std::string response;
    response = "HTTP/1.0 200 OK\r\n";
    if (suffix == ".jpg")
        response += "Content-Type: image/jpeg\r\n";
    else
        response += "Content-Type: text/html\r\n";
    response += ("Content-Length: " + to_string(html.size()) + "\r\n");
    response += "Set-Cookie: This is my cookie content;\r\n";
    response += "\r\n";
    response += html;

    send(sock, response.c_str(), response.size(), 0);
}

class ServerTcp
{
public:
    ServerTcp(uint16_t port, const std::string &ip = "")
        : port_(port), ip_(ip), listenSock_(-1)
    {
        quit_ = false;
    }

    ~ServerTcp()
    {
        if (listenSock_ >= 0)
            close(listenSock_);
    }

public:
    void init()
    {
        // 1. 创建socket
        listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
        if (listenSock_ < 0)
        {
            exit(1);
        }

        // 2. bind
        // 2.1 填充服务器信息
        struct sockaddr_in local; // 用户栈
        memset(&local, 0, sizeof local);
        local.sin_family = PF_INET;
        local.sin_port = htons(port_);
        ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
        // 2.2 本地socket信息,写入sock_对应的内核区域
        if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
        {
            exit(2);
        }

        // 3. 监听socket,为何要监听呢?tcp是面向连接的!
        if (listen(listenSock_, 5) < 0)
        {
            exit(3);
        }
    }

    void loop()
    {
        signal(SIGCHLD, SIG_IGN);
        while (!quit_)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 4. 获取连接,accept的返回值是一个新的socket fd
            int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
            if (quit_)
                break;
            if (serviceSock < 0)
            {
                cerr << "accept error..." << endl;
                // 获取连接失败,继续获取
                continue;
            }
            // 4.1 获取客户端基本信息
            uint16_t peerPort = ntohs(peer.sin_port);
            std::string peerIp = inet_ntoa(peer.sin_addr);
            
            pid_t id = fork();
            assert(id != -1);
            if (id == 0)
            {
                close(listenSock_); // 建议
                if (fork() > 0)
                    exit(0);
                handlerHttpRequest(serviceSock);
                exit(0);
            }
            close(serviceSock);
            wait();
        }
    }

    bool quitServer()
    {
        quit_ = true;
        return true;
    }

private:
    int listenSock_;
    uint16_t port_;
    std::string ip_;
    bool quit_; // 安全退出
};

运行服务器并使用浏览器访问,此时我们通过 Fiddler 可以看到服务器给浏览器的 HTTP 响应报头中共包含了 Set-Cookie 字段。

你真的了解 Cookie 和 Session 吗?-LMLPHP

总结

Cookie 和 Session 是 Web 开发中常用的数据存储和传递技术。Cookie 将数据存储在客户端浏览器,通过 HTTP 请求自动发送给服务器;而 Session 将数据信息存储在服务器中,通过 Cookie 或 URL 重写将 SessionID 发送给客户端。它们存储位置、数据容量、安全性、传输方式、生命周期和应用场景等方面都具有明显差异。

11-21 14:24