HTTP Cookie深入解析:Web会话追踪的秘密
目录
- ` HTTP Cookie`
- `定义`
- `工作原理`
- `分类`
- `安全性`
- `用途`
- `认识 cookie`
- `基本格式`
- `实验测试 cookie`
当我们登录了B站过后,为什么下次访问B站就不需要登陆了?
- 问题:B 站是如何认识我这个登录用户的?
- 问题:HTTP 是无状态,无连接的,怎么能够记住我?
HTTP Cookie
定义
HTTP Cookie
(也称为 Web Cookie、浏览器 Cookie 或简称 Cookie)是服务器发送到用户浏览器并保存在浏览器上的一小块数据
,它会在浏览器之后向同一服务器再次发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态、记录用户偏好等。
工作原理
- 当用户第一次访问网站时,服务器会在响应的 HTTP 头中设置
Set-Cookie
字段(如Set-Cookie : user = zhangsan),用于发送 Cookie 到用户的浏览器。 - 浏览器在接收到 Cookie 后,会将其保存在本地(通常是按照域名进行存储)。
- 在之后的请求中,浏览器会自动在 HTTP 请求头中携带 Cookie 字段,将之前保存的 Cookie 信息发送给服务器。
分类
-
会话 Cookie(Session Cookie):在浏览器关闭时失效。
-
持久 Cookie(Persistent Cookie):带有明确的过期日期或持续时间,可以跨多个浏览器会话存在。
-
如果 cookie 是一个持久性的 cookie,那么它其实就是浏览器相关的,特定目录下的一个文件。但直接查看这些文件可能会看到乱码或无法读取的内容,因为 cookie 文件通常以二进制或 sqlite 格式存储。一般我们查看,直接在浏览器对应的选项中直接查看即可。
-
类似于下面这种方式:
安全性
- 由于 Cookie 是存储在客户端的,因此存在被篡改或窃取的风险。
用途
用户认证和会话管理
(最重要)- 跟踪用户行为
- 缓存用户偏好等
- 比如在 chrome 浏览器下,可以直接访问:link
认识 cookie
- HTTP 存在一个报头选项:
Set-Cookie
, 可以用来进行给浏览器设置 Cookie值。 - 在
HTTP 响应
报头中添加,客户端(如浏览器)
获取并自行设置并保存Cookie。
服务器发送Cookie
:
- 当客户端(如浏览器)
首次
请求服务器资源时,服务器可能会在HTTP响应中
包含一个或多个Set-Cookie头部。这些Set-Cookie头部指示客户端存储特定的信息(即Cookie)。 - 每个Set-Cookie头部都包含了Cookie的名称、值以及可选的属性,如过期时间(Expires/Max-Age)、作用域(Path)、安全性要求(Secure)、跨站策略(SameSite)以及是否只能通过HTTP接口访问(HttpOnly)等。
客户端接收并保存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
(推荐)。
关于时间解释
- Tue: 星期二(星期几的缩写)
- , : 逗号分隔符
- 18: 日期(两位数表示)
- Thu: 月份的缩写
- 2024: 年份(四位数)
- 12:34:56: 时间(小时、分钟、秒)
- GMT: 格林威治标准时间(时区缩写)
GMT 和 UTC 都曾是或现在是国际上重要的时间标准,但由于地球自转的不规则性和原子钟的精确性,UTC 已经成为了全球性的标准时间,而 GMT 则更多被用作历史和地理上的参考。
关于其他可选属性的解释
expires=<date>
:设置 Cookie 的过期日期/时间。如果未指定此属性,则Cookie 默认为会话 Cookie
,即当浏览器关闭时过期。path=<some_path>
:限制 Cookie 发送到服务器的哪些路径。默认为设置它的路径。- domain=<domain_name>:指定哪些主机可以接受该 Cookie。默认为设置它的主机。
- secure:仅当使用 HTTPS 协议时才发送 Cookie。这有助于防止Cookie 在不安全的 HTTP 连接中被截获。
- HttpOnly:标记 Cookie 为 HttpOnly,意味着该 Cookie 不能被客户端脚本(如 JavaScript)访问。这有助于防止跨站脚本攻击(XSS)。
以下是对 Set-Cookie 头部字段的简洁介绍
注意事项
- 每个 Cookie 属性都以分号(;)和空格( )分隔。
- 名称和值之间使用等号(=)分隔。
- 如果 Cookie 的名称或值包含特殊字符(如空格、分号、逗号等),则需要进行 URL 编码。
Cookie 的生命周期
- 如果设置了 expires 属性,则 Cookie 将在指定的日期/时间后过期。
- 如果没有设置 expires 属性,则 Cookie 默认为会话 Cookie,即当浏览器关闭时过期。
安全性考虑
- 使用 secure 标志可以确保 Cookie 仅在 HTTPS 连接上发送,从而提高安全性。
- 使用 HttpOnly 标志可以防止客户端脚本(如 JavaScript)访问 Cookie,从而防止 XSS 攻击。
- 通过合理设置 Set-Cookie 的格式和属性,可以确保 Cookie 的安全性、有效性和可访问性,从而满足 Web 应用程序的需求。
实验测试 cookie
测试 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 路径下
- 比如:http://8.137.19.140:8888/a/x
- 比如:http://8.137.19.140:8888/
- 比如:http://8.137.19.140:8888/x/y
单独使用 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;
};