Web 开发中,Session 是经常用到的概念,但是在日常交流中,似乎又经常引起误解。

在我看来,引发误解的原因主要有两个:

  1. 大量使用简称,导致混淆了 数据 与 索引
  2. 不同 语言/框架 对 Session 做了不同形式的封装,导致特征不同

下面,我就尝试着捋一下整个问题,看看能不能尽量消除这些误会。

注:其它通讯领域也有使用 Session 这个词汇,本文仅探讨 Web 开发中的 Session 使用。

TL;DR

本文确实冗长无比,此处列出简要观点方便『太长不看』(Too Long; Didn't Read.)的读者。

  • 除了常见的 Server Side Session,应当注意还有 Client Side Session 的存在
  • Session 是虚拟概念,应当注意区分 Session-ID 和 Session-Data
  • Session 的具体实现,与 Cookies 没有绑定关系
  • JWT 和 Session 压根不在一个维度,不应该放在一起讨论

Session 是什么

名词约定

由于大家使用的『简称』含义往往差别很大,在此我们先 约定 一些称呼。

这些称呼可能和你惯用的不太一样,但统一定义是高效讨论的重要基础,还请不要抗拒。

Session

Session 经常被翻译为 『会话』,其实还是很贴切的,它代表了『一次』相互沟通,这次会话中,可能包括了多次通信。

需要注意的是:此处 Session 是一个整体性的 虚拟概念 ,不代指任何实体或数据。

Session-Data

看到 Data 后缀就知道,这个词代表了一次 Session 中,暂存或使用 的一些数据。

具体是什么数据取决于业务实现,例如可能存储了一些用户的 ID 信息或短效配置等。

需要注意的是,此处指的是数据的『实体』,而不是『索引』。

Session-ID

上面说到的数据实体 Session-Data ,未必会直接放在程序内部,而是可能存储在某一个具体的位置(后文会详细讨论)。

在大部分 Server Side Session 实现中(即 Session-Data 存储在服务端时),为了方便取用,就需要记录一个『索引』,用来找到具体的 Session-Data ,我们将这个『索引』称为 Session-ID 。

需要注意的是:在某些实现中,可能并没有 Session-ID 的概念(例如 Client Side Session)。

存储的差异

正如我前面所强调的,在不同的实现中,Session-Data 可能存储在不同的位置:可能在客户端,也可能在服务端。

Server Side Session

在大部分实现中,并不会直接传输 Session-Data ,而是传输它的索引:Session-ID ,服务端程序收到 Session-ID 后,从存储着 Session-Data 的地方取出具体的 Session-Data 来使用。

在这种模式下,更容易被我们接触到的,其实是存储在 Cookies 里的 Session-ID ,而许多人存在的一个误区,便是将 Session-ID 直接等同于 Session 本身,无视了 Session-Data 的存在,这其实是不正确的。

上图表示了这种模式的大致运行方式,绿色的 Session 是一个老用户会话, Alice 在访问时发送了自己持有的 Session-ID AA01 来标记当前会话,服务器程序根据相应的 Session-id 查询出了相应的 Session Data ,并在程序中应用。

对于新用户,因为自己没有存储 Session-id,只好不发送,服务器为了标记本次会话,生成了新的 Session-ID NN02 并返回给客户端,这样就开启了一个新的会话,图中用红色 Session 表示。

Client Side Session

除了上面提到的常见模式,其实还存在一种实现被许多人忽视,服务端并不存储 Session-Data,而是直接将 Session-Data 存储在客户端(浏览器)的 Cookies 里。

在这种情况下,你甚至找不到 Session-ID 的存在,许多人也会因此而搞不清 Session 究竟存储在哪里。

上图表示了这种模式的大致运行方式,与上面那种方式不同的地方在于,客户端直接告诉服务端,我自己是 Alice ,我的基本信息是 xxx ,于是服务端使用这些数据返回了相应的页面。

对于新用户,本地一丁点儿信息都没有,于是服务端返回了一个空的 Session-Data 用来初始化,在后续的通信中,这个 Session-Data 会被继续填充上有意义的数据。

很显然,这里存在着巨大的安全问题:我直接修改存储在本地的 Session-Data,声称我自己是管理员怎么办?

为此,Client side session 需要加入加密机制签名机制来进行校验,以确保所有的 Session-Data 都是由服务端生成的,具体会在下文讨论。

和 Cookies 的关系

提到 Session 就不能不提 Cookies,毕竟很多人会条件反射的把这二者弄混(苦笑脸)。

Cookies 自身就有非常多的玩儿法,抛开那些权限相关的内容,此处我们先只它最基础的几个特性:

  • 在客户端(浏览器)内存储一些数据(而不是在服务端)
  • 这些信息,可以由服务端设置,也可以由客户端设置
  • 当用户发起 HTTP 请求时,匹配的 Cookies 会嵌入请求头部(Header)一起发送

正是这些特征,让 Cookies 成为存储 Session-Data / Session-ID 的绝佳场所,在几乎所有的常见实现中,均默认使用 Cookies 来存储 Session 相关信息。

但是需要注意,Session-Data / Session-ID 选择 Cookie 作为容器只是因为『方便』,但这并不是必然情况

我们完全可以实现一个 Web 框架,它使用 LocalStorage 来存储 Session-Data / Session-ID ,在发起 XHR 请求时,主动将其加进 Header 内(甚至 URL 内),服务端则从相应位置读取信息完成业务逻辑,在这套框架体系内这是完全没有问题的。

实际上,许多框架都支持 Session with out Cookies 的实现,虽然存在各种各样的小问题或限制,但是确实可用。

因此,需要再次强调:对于 Session 机制来说,Session-Data / Session-ID 才是本体,Cookies 只是一个存储容器,这二者没有捆绑关系

Session 机制的实现

对于 Session 机制,之所以总是产生种种误解, 和各大 语言 / 框架 的实现有着很大的关联,由于大部分人都更熟悉自己常用的框架,导致很容易被当前框架的思路带进去。

那么接下来,就看一下各种常用 语言 / 框架 在 Session 机制上的实现与差异。

注:以下只是各个 语言 / 框架 默认支持的实现,实际中我们完全可以自定义任何形式的实现。

PHP

原生 PHP

PHP 不愧是为 Web 而生的语言,在语言层面就提供了 Session 机制的支持,按默认配置就可以使用 $_SESSION 这个超全局变量。

原生 PHP 默认使用的 Session-Data 存储方式为 Server Side Session ,使用 Session-ID 进行索引,Session-ID 默认存储在 Cookies 的 PHPSESSID 字段。

Session-Data 默认以文件形式存储在 session_save_path 所配置的路径中, PHPSESSID字段的内容就是文件名,文件内容就是序列化后的 Session-Data 数据。

通过配置 session.save_handler 字段,我们可以将存储方式切换为 redis 或者 memcache

redis 为例,Session-Data 会被存储在PHPREDIS_SESSION:PHPSESSID 中,PHPREDIS_SESSION:为默认的统一前缀。

除此之外,还有一些其它的配置可以参考官方文档,原生 PHP 也支持其它的相关扩展,不再一一赘述。

Laravel

Laravel 果然也很强,不但默认提供了常规的 Server Side Session ,也支持配置为 Client Side Session 形式。

通过配置 config/session.phpdriver 字段,可以切换 filecookiedatabasememcachedredisarray 这 6 种不同的存储方式。

这里面, filememcachedredis 都是比较常见的存储方式,就不再赘述。

database 可以将 Session-Data 存储在你定义的数据库内,但是你需要先建好一张表,里面配置好要存储的字段。

array 是一种比较奇怪的方式,只是用在开发阶段,它并没有任何持久化功能,随着 PHP 脚本的运行结束而消失。

cookie 则是比较少见的 Client Side Session 了,Laravel 使用 EncryptedStore.php 对 Session-Data 进行 『加密』后存储于客户端浏览器的 Cookies 中。

这里需要注意,Laravel 在实现 Client Side Session 的时候使用的是 『加密』方式来保障信息安全。

CodeIgniter

接下来看看老牌框架 CI 怎么样,翻看 配置文档 可以看到,CI4 默认支持 filedatabasememcachedredis 这几种 Server Side Session,看起来似乎平平无奇,都是常见套路。

但是文档上有一行备注:

In previous CodeIgniter versions, a different, “cookie driver” was the only option and we have received negative feedback on not providing that option. While we do listen to feedback from the community, we want to warn you that it was dropped because it is unsafe and we advise you NOT to try to replicate it via a custom driver.

这就很有意思了,看起来 CI 框架以前也支持 Client Side Session 嘛?翻出了 CI2 的文档 ,果然里面默认使用 Cookies 存储 Session-Data ,也提供了加密的选项。

那为什么 CI2 当时会因为这个被锤呢?我找到一篇相关的文章:(IN)secure session data in CodeIgniter - Websec ,文章中的观点来看:

  1. 虽然提供了对 Cookies 进行加密的选项,但默认配置是关闭的,这就导致了 Session-Data 会以明文展示,可能会泄漏信息。
  2. 虽然 CI2 在加密的基础上,还对明文 Session-Data 进行了签名,但是在用户配置了弱密钥的情况下,签名密钥可能会被暴力破解,导致 Session-Data 被篡改。

其实这两个问题都更应该归咎于用户的配置不当,但是毕竟和框架的默认配置有关,被锤也就在情理之中了。

可能也是因为这件事,才导致了 CI 现在只提供 Server Side Session 实现吧。

其它 PHP 框架

翻看了一下 SymfonyYiiCakePHPThinkPHP 等热门框架关于 Session 的文档,都使用了比较常规的 Server Side Session 方式,也都支持将 Session-Data 存储在文件、数据库、内存数据库等,在此就不一一介绍了,有需要的可以点链接查看相应文档。

Java

Tomcat

Tomcat 也是 Server Side Session 一派,默认在 Cookies 中使用 JSESSIONID 字段存储 Session-ID,Session-Data 默认存储在 内存中 ,翻阅 org.apache.catalina.session.ManagerBase 的源码,可以看到它使用了一个ConcurrentHashMap 结构来存储所有的 Session-Data 实例。

Tomcat 默认使用 StandardManager  来管理 Session-Data,当容器退出时,在退出时将所有的数据进行持久化;你也可以选择使用 PersistentManager 来管理,它可以更加灵活的执行持久化(但是 Session-Data 还是会存储在你的内存里)。

Jetty

Jetty 也比较中规中矩,参照文档来看,支持将 Session-Data 配置为存储在 内存、文件、JDBC 等位置。

比较不同的是,Jetty 支持为 Session-Data 配置 L1L2 两级缓存,在对性能有需求的场景下还是挺友好。

Spring Session

果然是家大业大的框架,Spring Session 自己就是个完整的组件了,毫无意外的支持把 Session-Data 储存在 redismongodbjdbc 等地方,方便扩展。

不知如此,它还有一些特有的特性:

在旧一些的版本中,Spring Session 还支持 在一个浏览器会话中,维护多个 Session ,不过在新版本中,这个特性被删除了,也许以后才会加回来。

这个巨无霸框架其实还支持其它一大堆特性,这里就不太多介绍了。

Python

Django

作为一个功能完善的重量级框架,Django 默认直接将 Session-Data 存储在了数据库,当然,你也可以配置为其它的存储方式,不再赘述。

这些存储方式中,Client Side Session 的代表 Cookies 再次出现,Session-Data 在序列化并『签名』后,存储在客户端 Cookies 中。

这里需要注意,此处是『签名』而不是『加密』,也就是说,客户端虽然不能篡改 Session-Data,但是可以自己反序列化后查看内容。

另外,Django 的 Session-data 还支持使用 Pickle 进行序列化,对于某些场景会非常有用(同时也带来了一些潜在风险)。

Flask

翻下文档,专注于轻量的框架 Flask ,毫无疑问的选择了实现起来最简单的方案:直接用 Client Side Session 就行了。

把 Session-Data 序列化并『签名』后,存储在客户端 Cookies 中就搞掂啦。这里同样需要注意,是『签名』而不是『加密』。

不过这毕竟是个 Python 框架,人家包多啊,使用 Flask-Session 就可以获得更多功能,支持将 Session-Data 存储在Cookies、文件、Redis、数据库等各种地方。

Tornado

这个已经接近凉凉的框架表示:不好意思,我们没有现成的 Session 模块,想要用?那你自己写去~

Why doesn't Tornado have session - Stack Exchange

Node.js

Express

Express 自身是最小化实现,Session 相关的功能由各种中间件来实现,此处主要提两个中间件。

express-session 是典型的 Server Side Session 实现,依靠强大的包数量支持很多种存储后端

比较有意思的是,虽然是 Server Side Session 实现,express-session 依然不放心,给 Cookies 里的 Session-ID 加了一层『签名』做校验,可以说非常心细了。

cookie-session 相对就比较普通,就是一个常规的 Client Side Session 实现, Session-Data 在序列化并『签名』后,存储在客户端的 Cookies 内。

Koa.js

包多就是好啊,koa 也有一大堆 Session 相关的中间件,在此就不一一介绍了。

Go

Gin

翻了下 Gin 比较常用的中间件是 gin-contrib/sessions ,同时支持 Server / Client Side Session ,也支持多种储存后端。

这个中间件默认支持在一个浏览器会话中,维护多个 Session ,对特定的需求会方便一些。

Echo

Echo 的文档中,默认使用了 gorilla/sessions 这个中间件,也是同时支持 Server / Client Side Session ,可以扩展支持其它存储后端

这个中间件也默认支持在一个浏览器会话中,维护多个 Session多个 Session ,对特定的需求会方便一些。

Beego

按照 Beego 的英文文档来看,默认只支持 Server Side Session,但是奇怪的是中文文档上却又写着支持的后端引擎包括 Cookie ,可能是文档写错了。

Ruby

Rails

Rails 默认使用 Client Side Session 实现,Session-Data 『加密』并『签名』后存储在客户端浏览器的 Cookies 中。也支持配置为 Server Side Session 实现,可以使用多种后端存储。

在其它许多框架的 Session 实现中,都可以看到一个叫做 Flash 的特殊数据,用于实现轻量的通知反馈等,这个功能应该是 Rails 首创的。

另外,Rails 关于 Session 安全的文档写的非常非常细致,强烈建议阅读。

Session 机制的使用

不同实现方式的对比

Session-Data 存储在何处

Sever Side Session

Sever Side Session 是目前最常见的 Session 实现,以至于不少人会误以为它是唯一一种 Session 实现。

优点:

  • 数据存储在服务端,相对来说安全性更强
  • 请求时只需要传递 Session-ID,减小流量开销
  • 可以方便的吊销 Session,管控 Session 策略

劣势:

  • Session-Data 集中管理,不利于并行化,需要专门解决 Session 共享问题
  • Session-Data 需要占用服务端内存 / 存储,对服务端存在压力
Client Side Session

Client Side Session 相对小众一些,但也可以看到许多框架都保留了对它的支持,Rails 甚至在文档中专门写了一大段来描述为什么自己默认这么用。

优点:

  • 实现简单,无须过多考虑 Session 存储、查询
  • 将存储压力转移到了客户端,这样可以减小服务端的资源消耗
  • 多机并行时,不需要考虑 Session 共享问题,完美支持高并发

劣势:

  • Cookies 默认有 4KB 限制,不能存储太多内容
  • Cookies 会在每次请求时被默认携带,存储较多内容时,增大了流量消耗
  • 存在重放攻击的风险,客户端可能会将数据替换为合法的旧数据
  • 实现 Session 数据拉黑、强制失效等功能时比较复杂
  • 数据需要『签名』或『加密』后才能确保安全,这需要开发者正确配置
  • 部分实现只对数据进行了『签名』,客户端可以直接查看到数据内容,存在安全风险

Session-ID 如何进行传递

对于 Server Side Session 来说,绝大部分实现都会选择将 Session-ID 放在 Cookies 中,在发起请求时自动带上,这样也是很符合直觉的做法,但是凡事都有例外,还是有一些特殊的实现。

重写进 URL 中

例如,PHP 默认就支持抛开 Cookies,使用 URL 传递 Session-ID,相关的配置为 use_cookiesuse_only_cookies 字段。

在 Tomcat 6.0 中,也支持类似的功能,但似乎是通过当前会话是否存在 Cookies 来判断的,也可以通过配置 disableURLRewriting 来关闭。

当然也存在不同的声音,比如 Django 就明确拒绝这么做,为此还在文档中专门写了原因 ,其一是 make URLs ugly ,其二是不安全。

除此之外,在 WAP 时代,由于早期手机浏览器很多不支持 Cookie,这种行为也非常常见。

必须要说的是,这样确实很不安全,比如可以参考这篇 9 年前的文章:浅谈WAP网站安全 - 空虚浪子心

放进 Header 中的其它字段

其实前面在说到 Spring 的时候就有提到,它支持将 Session-ID 放进 Header 中的X-Auth-Token 字段中。

实际上,只要前后端协调一致,其实放在任何一个字段都是没问题的。

放进隐藏的表单字段

这种处理方式我只在资料中看到过,大意就是在输出页面时,为每一个 Form 都补一个隐藏的 Session-ID 字段。

<form name="anyform" action="/xxx">
<input type="hidden" name="jsessionid" value="ByOK2vjFD43aPnrF6C2HmdnV6QZcEbzWoWiBYEnLerjQ22zWpBng!-1582381342">
<input type="text">
</form>

这种方式存在非常多的弊端,也难怪现在基本上看不到了。

Session-Data 的安全如何保障

对于 Client Side Session 来说,因为不需要索引,只需要看好 Session-Data 就好,在这件事上,大家也有不一样的思路。

大部分实现都选择只对 Session-Data 做『签名』处理,这样就算客户端知道 Session-Data 内部的数据是什么,也无法简单的篡改。

但是并不是所有的开发者都会注意这些,保不齐会有人存敏感信息在里面,所以部分框架不但做了签名,还做了『加密』,看也不让看。

需要注意的是,这样仍然存在重放攻击的风险,用户可以将其替换为旧的合法 Session-Data,此时服务端是无法鉴别出差异的。

另外,Rails 关于 Session 安全的文档写的非常非常细致,再次强烈建议阅读。

Session 相关的误解

Session 的概念并不复杂,洋洋洒洒写了这么多,其实还是因为讨论时遇到的各种误解,这里我们复盘一下。

Session ID 就是 Session 嘛?

不能等同,Session-ID 只是 Server Side Session 实现中,常用的索引信息。

Cookies 就是 Session 嘛?

Cookies 经常用于承载 Session 的信息(ID 或 Data),但并不是必然情况。

Session 是不是认证方式?Session 和 Token 的关系是什么?

这是经常被误解的一点,在此说下我个人的理解。

Token 并不是一个以技术手段定义的词汇,它的定义来源于『用途』。

但凡用于认证鉴权的 凭据,我认为都可以称作 Token ,与这个凭据的生成方式无关。

基于此,如果 Session-ID 或 Session-Data 被用作认证,那么我认为这条数据就可以被称为 Token。

但是需要注意,Session 自身是个虚拟概念,与 Token 毫不相关。

Session 和 JWT 的关系是什么?

但凡提到 Session,就不可避免的会有人搬出 JWT ,也会有很多人把 Session 和 JWT 摆到对立面来讨论,但是这两个压根就不在一个维度。

先来看看 JWT 到底是什么,它的全名是 JSON Web Token ,从名字就可以看出,它是特定的一种 Token 生成方式。

JWT 的生成方式大致是这样:将数据按特定格式进行序列化,标记过期时间,对数据进行『签名』后编码为 URL Safe 的 Base64URL 。

诶?好像有点眼熟?这怎么和 Client Side Session 那边对 Session-Data 的处理那么像呢?

除此之外,这里有一篇蛮有名的文章:Stop using JWT for sessions 也可以一起看看。

看完文章会发现,作者提到的那些将 JWT 用做 Session 实现的人,实际上说的都是 Client Side Session 啊。

那么问题变的比较清晰了:

  • JWT 只是一种处理数据的手法,它通过签名保证了信息的不可篡改,特定的格式也具备其它一些小特性。
  • JWT 的特性,让它具备成为 Client Side Session 的数据处理方式的潜力,也确实有人这么做了。

而那些关于 JWT 与 Session 优劣的争论,实际上大部分都是在争论 Sever Side Session 和 Client Side Session 的优劣。

总结复盘

扯了冗长的上万字,其实就是为了说明一个『定义』的问题,Session 本身并不复杂,只要不把各种简称搞混。

很多时候由于大量使用简称,或者模糊定义,导致我们对概念本身失去了掌握,也经常让讨论变的低效。

另一方面,不能把自己所熟悉的那些实现当作全部,时不时的看看其它语言或框架,也许能发现许多好玩儿的东西。

最后,如果本文有哪些地方出现了疏漏或错误,还请不吝赐教,一起交流进步。