LiteHub之会话管理
Qingyh Lv2

会话管理理论

为什么进行会话管理

HTTP协议的特性与局限性

Web应用程序的基础是HTTP(Hypertext Transfer Protocol)协议,该协议在设计时具有以下核心特点:

  1. 请求/响应模式

    • 每次HTTP交互都遵循”客户端发起请求→服务器返回响应”的固定模式
    • 例如:用户访问电商网站时,点击商品页、加入购物车、结算等都是独立的请求
    • 各请求之间没有内在关联,服务器无法自动识别这些请求是否来自同一用户
  2. 无状态性(Stateless)

    • 协议本身不保存任何历史交互信息
    • 每个请求都被视为全新的交互,服务器不会”记住”之前的请求
    • 实际案例:刷新网页后,登录状态、表单填写内容等都将丢失
  3. 无连接特性

    • 每次TCP连接只处理一个请求/响应
    • 请求完成后立即断开连接以节省资源
    • 导致的问题:无法维持长期对话,如在线聊天、多步骤表单等场景难以实现

现实应用的需求矛盾

虽然HTTP的这些特性使其简单高效,但现代Web应用需要:

  • 用户登录状态保持(如保持7天免登录)
  • 购物车商品跨页面保存
  • 多步骤表单数据暂存
  • 个性化内容推荐(基于历史浏览)

因此需要引入会话管理机制来:

  1. 识别同一用户的连续请求
  2. 在服务器端存储用户特定数据
  3. 维持应用的状态连续性

常见解决方案包括:

  • Cookie技术
  • Session会话
  • Token令牌(如JWT)
  • URL重写技术

在这个项目中,我们使用的就是Session会话和Cookie技术结合使用来记录用户登录状态。

什么是Session

  • 服务器为每个用户浏览器创建一个会话对象(session对象),一个浏览器只能产生一个session
  • 当新建一个窗口访问服务器时,还是原来的那个session。session中默认保存的是当前用户的信息。因此,在需要保存其他用户数据时,我们可以自己给session添加属性。
  • session(会话)可以看为是一种标识,通过带session的请求,可以让服务器知道是谁在请求数据。

Session与cookie的区别与联系

  • session是由服务器创建的,并保存在服务器上的。在session创建好之后,会把sessionId(会话的唯一标识符)放在cookie中返回(response)给客户端。客户端将cookie是保存在客户端的。
  • 以后的每次请求都携带cookie,cookie中的内容是sessionId值。
  • session的过期和超时与cookie的过期无直接联系,都是可以分别进行设置的。当session或cookie中任意一方过期,那么用户就需要重新登录了

注意:虽然 Cookie 是最主流的方式,但如果用户禁用 Cookie,服务器还可以通过其他方式传递 Session ID:
URL 重写: 将 Session ID 作为查询参数附加到每个 URL 后面 (如 ?sessionid=abc123xyz)。这种方式不太安全(容易泄露)且不美观。
隐藏表单域: 将 Session ID 放在 HTML 表单的隐藏字段中。仅适用于表单提交。

所以sessionid和cookie的更准确描述是:Session 机制通常利用 Cookie 用于在客户端存储和传递标识服务器端 Session 数据的 Session ID。Cookie 是 Session ID 的载体,而非 Session 生成了 Cookie。 服务器端的 Session 管理代码负责生成 Session ID 并指示浏览器(通过 Set-Cookie)存储它。

第六章 会话管理(Session)

会话管理代码实现

会话管理中共实现了四个类实现:

  • Session(会话):表示一个会话。
  • SessionManager(会话管理器):用于管理多个会话的声明周期。
  • SessionStorage(会话存储):会话存储实现的抽象类。
  • MemorySessionStorage(内存会话存储):继承SessionStorage,具体的会话存储实现类。

Session类实现

  • Session 构造函数,用于初始化会话实例,记录 sessionId、设置最长有效时间(默认值为 1 小时)并关联会话管理器。
  • isExpired(),判断当前会话是否过期
  • refresh(),刷新过期时间,当前时间加上最长有效时间(默认为一小时)
  • setValue(),以键值对形式存储会话数据
  • getValue(),根据传入key获取相应的会话数据(value)
  • remove(),根据传入key删除相应的会话数据(value)
  • clear(),清空所有会话数据

总结,session类实现了记录会话唯一标识符,维持会话过期时间,更新会话数据功能。

SessionManager类实现

  • SessionManager 构造函数,用于配置会话存储对象(负责会话存储)和随机数生成器(用于生成随机的会话ID)
  • getSession()函数会从请求中提取cookie字段以获取sessionid,并返回相应的会话;若cookie不存在或者会话已过期,则创建一个新会话。
  • generateSessionId(),生成一个唯一的会话标识符
  • destroySession(),从存储中移除会话
  • getSessionIdFromCookie(),从请求中的cookie字段获取sessionId
  • setSessionCookie(),在响应中设置cookie

这几个函数比较重要,放这里注释一下,便于理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
//从req请求中解析出sessionId
std::string SessionManager::getSessionIdFromCookie(const HttpRequest& req)
{
std::string sessionId;
std::string cookie = req.getHeader("Cookie");//找到Cookie字段

if (!cookie.empty())
{
//格式一般为
//Cookie: sessionId=1dd3ce798f86bf092595840ac8ecadc8\r\n
size_t pos = cookie.find("sessionId=");
if (pos != std::string::npos)
{
pos += 10; // 跳过"sessionId="
size_t end = cookie.find(';', pos);
if (end != std::string::npos)
{
sessionId = cookie.substr(pos, end - pos);
}
else
{
//从pos开始截取到字符串结束
sessionId = cookie.substr(pos);
}
}
}

return sessionId;
}

// 生成唯一的会话标识符,确保会话的唯一性和安全性
std::string SessionManager::generateSessionId()
{
std::stringstream ss;
std::uniform_int_distribution<> dist(0, 15);

// 生成32个字符的会话ID,每个字符是一个十六进制数字
for (int i = 0; i < 32; ++i)
{
ss << std::hex << dist(rng_);//生成一个0~15之间的随机数
}
return ss.str();
}
//在响应中添加cookie字段
void SessionManager::setSessionCookie(const std::string& sessionId, HttpResponse* resp)
{
// 设置会话ID到响应头中,作为Cookie
std::string cookie = "sessionId=" + sessionId + "; Path=/; HttpOnly";
resp->addHeader("Set-Cookie", cookie);
}

std::shared_ptr<Session> SessionManager::getSession(const HttpRequest& req, HttpResponse* resp)
{
//从请求中的cookie字段中取出sessionid
std::string sessionId = getSessionIdFromCookie(req);

std::shared_ptr<Session> session;

if (!sessionId.empty())//如果sessionId存在
{
session = storage_->load(sessionId); //根据sessionId获取对应会话
}

if (!session || session->isExpired())//如果sesseion不存在或者说session过期,重新创建一个
{
sessionId = generateSessionId();//生成唯一的sessionId
session = std::make_shared<Session>(sessionId, this);//传入sessionid和SessionManager构建一个会话
setSessionCookie(sessionId, resp);//在响应resp中setCookie中添加sessionId
}
else
{
session->setManager(this); // 为现有会话设置管理器
}

session->refresh(); //刷新会话的过期时间,因为当前有新的访问,过期时间需要重新被设置
storage_->save(session); // 保存会话
return session;
}

MemorySessionStorage类实现

SessionStorage定义了抽象类提供了save()load()remove()接口,而MemorySessionStorage对重写了这些函数。
MemorySessionStorage是以<std::string, std::shared_ptr<Session>>构造的无序键值对保存的。

  • save()函数,保存会话
  • load()函数,根据sessionId找到对应的会话,如果会话过期则删除会话;否则返回会话
  • remove()函数,通过sessionId删除会话

会话管理抓包分析

1.首次访问网页

此时还没登录(此时还不需要维护用户的登录状态),这时的请求中还没有cookie字段。
image
这是发起的请求是为了请求网页,
image
服务器返回的响应,可以看到,此时服务器也没有set-cookie字段;此时服务器返回前端渲染需要的网页(响应体中)
image

2.点击登录时

image
这时可以看到请求的报文,是以POST方式将用户登录的usernamepassword上传到服务器(这里是明文传输,这是HTTP的缺点,在网络传输中容易被抓包导致密码和账户泄露;后面这个项目看能不能扩展成HTTPS协议)
image

3.服务器处理

此时通过点击登录按钮,将登录请求发送到服务器,服务器根据相应的路由,转发到专门用于登录处理的函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 根据账号密码,查找数据库是否有该账号密码
int LoginHandler::queryUserId(const std::string &username, const std::string &password)
{
// 前端用户传来账号密码,查找数据库是否有该账号密码
// 使用预处理语句, 防止sql注入
std::string sql = "SELECT id FROM users WHERE username = ? AND password = ?";
sql::ResultSet* res = mysqlUtil_.executeQuery(sql, username, password);
if (res->next())
{
int id = res->getInt("id");
return id;
}
// 如果查询结果为空,则返回-1
return -1;
}
void LoginHandler::handle(const http::HttpRequest &req, http::HttpResponse *resp)
{
// ...其他代码

// JSON 解析使用 try catch 捕获异常
try
{
json parsed = json::parse(req.getBody());
std::string username = parsed["username"];
std::string password = parsed["password"]; //从请求体中解析得到username 和password
// 验证用户是否存在,是否注册过
int userId = queryUserId(username, password);
if (userId != -1)
{
// 获取会话
auto session = server_->getSessionManager()->getSession(req, resp);

// 在会话中存储用户信息
session->setValue("userId", std::to_string(userId));
session->setValue("username", username);
session->setValue("isLoggedIn", "true");

//其他代码
}
else // 账号密码错误,请重新登录
{
// 封装json数据,返回401未认证状态码
return;
}
}
catch (const std::exception &e)
{
//...其他代码,返回错误信息
return;
}
}

上述代码中的 server_->getSessionManager()->getSession(req, resp)负责返回会话,或者新建会话,具体为:

  1. 如果当前用户存在会话并且会话有效,则直接返回会话
  2. 否则创建新的会话(这里是首次登录,所以默认就是这种情况)

并在会话中存储如"userId""username""isLoggedIn"字段。

4.服务器返回响应

在getSession(req, resp)—>>>setSessionCookie(sessionId, resp);设置响应报文

1
2
3
4
5
6
void SessionManager::setSessionCookie(const std::string& sessionId, HttpResponse* resp)
{
// 设置会话ID到响应头中,作为Cookie
std::string cookie = "sessionId=" + sessionId + "; Path=/; HttpOnly";
resp->addHeader("Set-Cookie", cookie);
}

image
服务器响应设置了cookie字段,以后的客户端的每次请求都将携带这个字段。

5.客户端的下一次请求

image
image
从上图可知,设置cookie之后的每一次请求,都会带上cookie这个字段。

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 31k