会话管理理论
为什么进行会话管理
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(会话):表示一个会话。
- 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 | //从req请求中解析出sessionId |
MemorySessionStorage类实现
SessionStorage
定义了抽象类提供了save()
、load()
、remove()
接口,而MemorySessionStorage
对重写了这些函数。MemorySessionStorage
是以<std::string, std::shared_ptr<Session>
>构造的无序键值对保存的。
- save()函数,保存会话
- load()函数,根据sessionId找到对应的会话,如果会话过期则删除会话;否则返回会话
- remove()函数,通过sessionId删除会话
会话管理抓包分析
1.首次访问网页
此时还没登录(此时还不需要维护用户的登录状态),这时的请求中还没有cookie
字段。
这是发起的请求是为了请求网页,
服务器返回的响应,可以看到,此时服务器也没有set-cookie字段;此时服务器返回前端渲染需要的网页(响应体中)
2.点击登录时
这时可以看到请求的报文,是以POST方式将用户登录的username
和password
上传到服务器(这里是明文传输,这是HTTP的缺点,在网络传输中容易被抓包导致密码和账户泄露;后面这个项目看能不能扩展成HTTPS协议)
3.服务器处理
此时通过点击登录按钮,将登录请求发送到服务器,服务器根据相应的路由,转发到专门用于登录处理的函数中
1 | // 根据账号密码,查找数据库是否有该账号密码 |
上述代码中的 server_->getSessionManager()->getSession(req, resp)负责返回会话,或者新建会话,具体为:
- 如果当前用户存在会话并且会话有效,则直接返回会话
- 否则创建新的会话(这里是首次登录,所以默认就是这种情况)
并在会话中存储如"userId"
,"username"
,"isLoggedIn"
字段。
4.服务器返回响应
在getSession(req, resp)—>>>setSessionCookie(sessionId, resp);设置响应报文
1 | void SessionManager::setSessionCookie(const std::string& sessionId, HttpResponse* resp) |
服务器响应设置了cookie字段,以后的客户端的每次请求都将携带这个字段。
5.客户端的下一次请求
从上图可知,设置cookie之后的每一次请求,都会带上cookie这个字段。