基于session的会话管理实践

本文最后更新于:8 个月前

分别探讨了前后端同源和跨域条件下的session cookie会话管理

基于session的会话管理实践(含cookie跨域解决方案)

之前做的项目中,使用的都是基于token的会话管理方式,以致于对更为传统的、据说现在依然在广泛使用的基于session的会话管理机制没有太多了解。所以特意通过一次实践,来深入了解掌握基于session的会话管理方法

项目仓库:基于sessionID cookie的会话管理实践(含cookie跨域解决方案),使用express-session (github.com)



一、理论基础

1.什么是会话管理

会话管理基于一个背景,即http是无状态的,在一次请求结束后,下一次请求服务端不知道请求是由哪个用户发送过来的。
采用各种方法,让服务器记住用户的身份,就是会话管理

2.基于session的会话管理

会话管理有三种方式:(1)基于server的session,(2)cookie-based,(3)token-based

基于session的会话管理,是目前仍在大量使用的方式,适合单体服务网站使用。

用户在服务端完成身份认证后,由服务端生成和保存一份session记录,其中保存了用户相关的信息,如身份、权限等。在返回认证响应式,将session ID保存到cookie中返回给客户端(实际上是添加set-cookie请求头,通知浏览器把session ID保存到cookie中)。之后,浏览器再发送请求时,会自动将cookie携带。服务端可以获取cookie中携带的 session ID,读取保存在服务端的数据,从而识别出用户身份。

优点:是有状态的会话管理,服务端可以决定会话继续会终止

缺点:(1)cookie可能存在跨域的问题需要解决;(2)存在CSRF的风险;(3)占用服务端资源;(4)如果服务端是分布式的,需要在多台服务器设备之间共享session



二、实践工具

后端:

前端:

  • 纯HTML
  • XHR库:Axios(通过<script src="https://cdn.jsdelivr.net/npm/axios@1.1.2/dist/axios.min.js"></script>引入html)
  • 响应式框架:petite-vue(提供页面响应式机制,编写更方便)


三、搭建服务端

新建项目

1
express session_based

安装中间件

1
npm i express-session connect-mongo

要解决的几个关键问题:(1)怎么生成sessionID cookie?(2)怎么将会话信息写入session?(3)怎么读取session保存的会话信息?

express-session配置指南

1
2
3
4
5
6
7
8
9
10
// 为每个请求设置一个sessionID cookie,会自动将sessionID cookie发送给客户端
app.use(session({
secret: SESSION_SECRET,

// 是否将 sessionID cookie保存回客户端,即使cookie没有发生修改,默认值为true(官方不推荐使用)
resave: false,

// 布尔值,强制将“未初始化”的会话保存到存储中。false减少服务器储存使用量。默认为true
saveUninitialized: false,
}))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 登陆时,重新生成一个会话,会自动将sessionID cookie发送给客户端
router.post('/login', function (req, res, next) {

// 重新生成一个新的会话
req.session.regenerate(function (err) {
if (err) next(err)

// 写入会话信息到session
req.session.username = req.body.username

// 保存session
req.session.save(function (err) {
if (err) return next(err)
res.send(req.session.username)
})
})
})
1
2
3
4
5
6
router.get('/userinfo', isAuthenticated, function (req, res, next) {

// 读取session信息
res.send(`您的用户名为:${req.session.username}`)

})


四、搭建前端页面

前端只需要搭建一个简单的页面,能够分别发送:不用携带cookie的请求、获取cookie的请求、需要携带cookie用于鉴权的请求几种即可。

首先搭建一个同源的网页。

1
2
// app.js
app.use(express.static(path.join(__dirname, 'public')));

Express框架搭建了静态托管服务,(因为不会写jade),所以在/public目录下创建一个index.html文件,用来实现上述几种请求

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
<body>
<div v-scope>
<h2>基于session的会话管理演示</h2>
<ul @click="togglePage">
<li id="home">首页</li>
<li id="login">登陆</li>
<li id="userinfo">获取用户信息</li>
</ul>

<p>当前位置:{{curPage}}</p>

<div v-if="curPage=='home'">
{{homeText}}
</div>

<div v-else-if="curPage=='login'">
<div>
<label for="username">用户名:</label>
<input type="text" name="username" required v-model="username">
</div>
<div>
<label for="password">密码:</label>
<input type="password" name="password" required v-model="pwd">
</div>
<button @click="handleLogin">
登陆
</button>
</div>

<div v-else>
{{userinfoText}}
</div>
</div>

<script src="https://unpkg.com/petite-vue@0.2.2/dist/petite-vue.iife.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@1.1.2/dist/axios.min.js"></script>
<script type="module">
PetiteVue.createApp({
......
}).mount()
</script>
</body>
</html>

接下来,输入:http://localhost:3000/,即可打开网页

image-20230504172507639

直接访问“获取用户信息”,由于没有cookie,所以失败401

image-20230504172722125

登陆之后,查看响应头,可以看到Set-Cookie字段,这个就是express-session生成并发送过来的

image-20230504172811267

然后发送userinfo请求的时候,可以看到浏览器自动将cookie携带上了,服务端由此可以获取sessionID,进而读取服务端本地的session信息,进行身份识别。

image-20230504172932930
同源条件下的session会话管理至此就完成了,实现起来还是比较简单。下面继续探究跨域条件下的session会话管理该如何操作


五、跨域情况下的session会话管理

为了模拟出一个跨域环境,我们在后端项目之外,新建一个HTML,内容与上面那个html文件一致,这里不再重复

此时,为了解决跨域请求的问题,我们在服务端进行CORS配置,这里采用现成的解决方案 cors - npm (npmjs.com)

在Express后端项目中,安装cors

1
npm i cors

在app.js文件中引入

1
2
3
4
// app.js
var cors = require('cors')

app.use(cors())

这里只是为了从简处理,直接放行了所有跨域请求的origin源。实际生产环境下强烈不建议这么做!

这里的表现与同源条件下稍有不同!

  • login的响应头中依然携带了Set-Cookie字段
  • 但是这次userinfo请求却没有携带cookie,通过检查cookie发现,浏览器并没有保存cookie(cookie为空)
image-20230504174634819 image-20230504174857017

原因:跨站点的cookie,Set-Cookie标头没有指定 SameSite 属性!!默认为 Lax,不能被浏览器保存。关于SameSite属性,更多的介绍,可以看这篇文章

微信图片编辑_20230505105611

遇到新的问题:当设置SameSite:None时,需要搭配设置Secure属性(只能在https协议下携带cookie)

微信图片编辑_20230505110514

至此,后面的探究路基本是断了,因为:

  • html是直接在本地打开的,协议为 file
  • 后端服务直接在本地运行的,协议为http

解决方案有两种:

  • 第一种,通过Nginx为网页架设正向代理,让跨域变成同源,也就不存在cookie跨域问题了
  • 第二种,为了让前后端都能通过https协议访问,需要将两者部署到服务器上,然后设置https协议的路径,继续向下探究

接下来,分别对两种解决方案进行实践。



六、cookie跨域方案一:正向代理服务器

网页静态资源部署到服务器A上,同时在服务器A上部署一个Nginx服务器,在Nginx设置一个代理,通过这个代理去访问真正的后端url。这个代理就称为网页的正向代理

对于网页来说,接收的数据,包括cookie,来自于Nginx中的正向代理。而这个代理与网页是同源的,因为它们都在同一台服务器上(本质是因为使用相同的协议、域名、端口),所以就不会构成跨域了

而对于代理来说,因为它并不是浏览器,所以也就不存在跨域问题一说了。

分别将 网页静态html 和 后端项目 上传到服务器。

这里最核心的步骤是Nginx的配置

1
2
3
4
5
6
7
8
9
location /session_base {
alias /var/session_base/frontend/;
index index.html;
}

// 后端api配置反向代理
location /session_base_api {
proxy_pass http://127.0.0.1:3001;
}
1
2
3
4
// 网页中访问的是 代理url,而非直接访问后端url
const instance = axios.create({
baseURL: 'https://timegogo.top/session_base_api',
})

响应头上携带上了set-cookie

image-20230506002057160

浏览器顺利保存了cookie

image-20230506002117223

发送请求时,顺利携带上了cookie

image-20230506002234416

七、cookie跨域方案二:SameSite + Secure

  • SameSite,用来防止CSRF攻击,有三个值:
    • Lax(默认值),不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外
    • Strict,完全禁止第三方(即跨源的) Cookie
    • None,关闭这个属性,前提是同时设置Secure属性为true
  • Secure,指定浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器

为了让前后端都能通过https协议访问,需要将两者部署到服务器上。并配置https协议的路径

1
2
3
4
5
6
7
8
9
location /session_base {
alias /var/session_base/frontend/;
index index.html;
}

// 后端api配置反向代理
location /session_base_api {
proxy_pass http://127.0.0.1:3001;
}

等等,这时前后端不就已经同源了吗?那还费劲去配置 SameSiteSecurewithcredentials干什么??

结束~



八、express-session配置

官方链接:express-session - npm (npmjs.com)

属性 说明
cookie 设置sessionID cookie的对象属性,支持设置的属性见下一章 cookie属性
默认值:{ path: '/', httpOnly: true, secure: false, maxAge: null }
genid 生成新会话ID的函数,默认为使用uid安全库生成ID的函数
name session ID cookie的名称,用来在服务端读取它,默认值:connect.sid
proxy true、false、undeined,设置secure为ture时信任反向代理,默认为undefined(继承自Express)
resave 是否将 sessionID cookie保存回客户端,即使cookie没有发生修改,默认值为true(官方不推荐使用)
rolling 布尔值,为true时将快速过期会话。默认为false
saveUninitialized 布尔值,强制将“未初始化”的会话保存到存储中。false减少服务器储存使用量。默认为true(官方不推荐使用)
secret(必须设置) 字符串or字符串数组,cookie签名的密钥,最好用环境变量设置(不要公开到仓库)
store 服务端session保存的位置,默认保存在内存中
unset 默认值:keep,存储中的会话将被保留,但在请求过程中所做的修改将被忽略,不会被保存


九、cookie属性

下面介绍的虽然只是express-session的cookie配置项,但它与原生cookie的属性基本是一致的
属性 说明
domain 设置cookie的域,用来解决子域名cookie跨域问题,默认不设置
设置了domain,子域名会携带当前cookie;反之不携带
expires 接受Date对象,用来设置cookie的到期时间,默认不设置,官方不推荐使用!
expires和maxAge都存在时,以后面声明的那个为准(在原生cookie中Max-Age比Expires优先级高)
httpOnly 布尔值,指定cookie无法通过JavaScript脚本拿到,默认为true
maxAge 时间戳毫秒数,设置从现在开始的有效时长,默认为 null(cookie是session cookie,只在本次会话期间保存)
path 指定发送请求时,哪些路径要携带cookie,只要是路径以path开头,就会携带cookie,默认为/
sameSite 布尔值or字符串,(尚未完全标准化),用来防止 CSRF 和 用户追踪,取值如下:
(1)true:等同于strict (2)false:不设置该属性
(3)’strict‘:完全禁止第三方cookie (4)’lax‘:除了get请求外,不携带第三方cookie (5)’none‘:关闭该属性
secure 布尔值,只有在https协议下,才携带cookie,默认为false


基于session的会话管理实践
http://timegogo.top/2023/05/04/前端工程化/会话管理:基于session的会话管理实战/
作者
丘智聪
发布于
2023年5月4日
更新于
2023年7月16日
许可协议