会话管理理论
为什么进行会话管理
HTTP协议的特性与局限性
Web应用程序的基础是HTTP(Hypertext Transfer Protocol)协议,该协议在设计时具有以下核心特点:
请求/响应模式:
- 每次HTTP交互都遵循”客户端发起请求→服务器返回响应”的固定模式
- 例如:用户访问电商网站时,点击商品页、加入购物车、结算等都是独立的请求
- 各请求之间没有内在关联,服务器无法自动识别这些请求是否来自同一用户
无状态性(Stateless):
- 协议本身不保存任何历史交互信息
- 每个请求都被视为全新的交互,服务器不会”记住”之前的请求
- 实际案例:刷新网页后,登录状态、表单填写内容等都将丢失
无连接特性:
- 每次TCP连接只处理一个请求/响应
- 请求完成后立即断开连接以节省资源
- 导致的问题:无法维持长期对话,如在线聊天、多步骤表单等场景难以实现
现实应用的需求矛盾
虽然HTTP的这些特性使其简单高效,但现代Web应用需要:
- 用户登录状态保持(如保持7天免登录)
- 购物车商品跨页面保存
- 多步骤表单数据暂存
- 个性化内容推荐(基于历史浏览)
因此需要引入会话管理机制来:
- 识别同一用户的连续请求
- 在服务器端存储用户特定数据
- 维持应用的状态连续性
常见解决方案包括:
- 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
| std::string SessionManager::getSessionIdFromCookie(const HttpRequest& req) { std::string sessionId; std::string cookie = req.getHeader("Cookie");
if (!cookie.empty()) { size_t pos = cookie.find("sessionId="); if (pos != std::string::npos) { pos += 10; size_t end = cookie.find(';', pos); if (end != std::string::npos) { sessionId = cookie.substr(pos, end - pos); } else { sessionId = cookie.substr(pos); } } } return sessionId; }
std::string SessionManager::generateSessionId() { std::stringstream ss; std::uniform_int_distribution<> dist(0, 15);
for (int i = 0; i < 32; ++i) { ss << std::hex << dist(rng_); } return ss.str(); }
void SessionManager::setSessionCookie(const std::string& sessionId, HttpResponse* resp) { std::string cookie = "sessionId=" + sessionId + "; Path=/; HttpOnly"; resp->addHeader("Set-Cookie", cookie); }
std::shared_ptr<Session> SessionManager::getSession(const HttpRequest& req, HttpResponse* resp) { std::string sessionId = getSessionIdFromCookie(req); std::shared_ptr<Session> session; if (!sessionId.empty()) { session = storage_->load(sessionId); }
if (!session || session->isExpired()) { sessionId = generateSessionId(); session = std::make_shared<Session>(sessionId, this); setSessionCookie(sessionId, resp); } 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方式将用户登录的username和password上传到服务器(这里是明文传输,这是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) { 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; } return -1; } void LoginHandler::handle(const http::HttpRequest &req, http::HttpResponse *resp) { try { json parsed = json::parse(req.getBody()); std::string username = parsed["username"]; std::string password = parsed["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 { return; } } catch (const std::exception &e) { return; } }
|
上述代码中的 server_->getSessionManager()->getSession(req, resp)负责返回会话,或者新建会话,具体为:
- 如果当前用户存在会话并且会话有效,则直接返回会话
- 否则创建新的会话(这里是首次登录,所以默认就是这种情况)
并在会话中存储如"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) { std::string cookie = "sessionId=" + sessionId + "; Path=/; HttpOnly"; resp->addHeader("Set-Cookie", cookie); }
|
![image]()
服务器响应设置了cookie字段,以后的客户端的每次请求都将携带这个字段。
5.客户端的下一次请求
![image]()
![image]()
从上图可知,设置cookie之后的每一次请求,都会带上cookie这个字段。
if (hexo-config('comment') && hexo-config('comment.enable') == true && hexo-config('comment.use')) {
if (hexo-config('comment.use') == "valine") {
@import "./valine.styl"
}
else if (hexo-config('comment.use') == "gitalk") {
@import "./gitalk.styl"
}
else if (hexo-config('comment.use') == "twikoo") {
@import "./twikoo.styl"
}
else if (hexo-config('comment.use') == "waline") {
@import "./waline.styl"
}
}
.comments-container {
display inline-block
width 100%
margin-top var(--component-gap)
.comment-area-title {
width 100%
color var(--text-color-3)
font-size 1.38rem
line-height 2
i {
color var(--text-color-3)
}
+keep-tablet() {
font-size 1.2rem
}
}
.configuration-items-error-tip {
display flex
align-items center
margin-top 1rem
color var(--text-color-3)
font-size 1rem
i {
margin-right 0.3rem
color var(--text-color-3)
font-size 1.2rem
}
}
.comment-plugin-fail {
display none
flex-direction column
align-items center
justify-content space-around
width 100%
padding 2rem
.fail-tip {
color var(--text-color-3)
font-size 1.1rem
}
.reload {
margin-top 1rem
}
}
.comment-plugin-loading {
flex-direction column
padding 1rem
color var(--text-color-3)
.loading-icon {
color var(--text-color-4)
font-size 2rem
}
.load-tip {
margin-top 1rem
color var(--text-color-4)
font-size 1.1rem
}
}
}