这篇文章可谓极尽详实地记叙了Session和JWT登录认证的各个方面与底层原理,看完这篇文章,哪怕你是登录认证的新手,我也会彻底让你读懂几乎所有Web登录认证都会用到的两大主流方案:Session和JWT,它们究竟是什么、都是如何实现的以及我们应该如何选择。

一、前言

        在正式讲解之前,相信很多同学跟我一样,其实不太了解各种登录校验方法直接都有什么显著区别,我们能想到的就是Cookie和Session像用钥匙开锁一样,数据库里有“钥匙”(用户信息),就能“开锁”(登录),一把钥匙配一把锁。又或者你了解JWT的原理,但却不知道为什么它的使用更为广泛,也不知道JWT是否比Session更高级。在这篇文章,我都会给出答案。

二、基本概念

1.Session+Cookie

        因为Session的底层就是基于Cookie的,所以我们这里只简单复习一遍Session的登录校验,原理很简单,核心就是「服务端保存状态,客户端保存凭证」。下面我们完整地梳理一遍Session 登录流程:

  1. 用户提交凭证:用户在登录页输入用户名和密码,浏览器将其发送到服务端。
  2. 服务端校验:服务端拿到用户名和密码后,与数据库中的用户信息进行比对。校验通过后,进入下一步。
  3. 创建Session:服务端为该用户生成一个sessionId,并将用户信息存储在数据库/Redis中。
  4. 下发Cookie:服务端通过响应头Set-Cookie将sessionId写入浏览器的Cookie中,并设置过期时间与作用域。
  5. 后续请求携带:浏览器在后续每次请求中,都会自动在请求头Cookie中带上该sessionId。
  6. 服务端校验Session:服务端根据请求携带的sessionId,在存储中查找对应的Session数据。若存在且未过期,则视为已登录,放行请求;否则返回未登录或跳转登录页。
  7. 退出登录:用户点击退出时,服务端删除对应的Session数据,同时浏览器清除Cookie,完成登出。

        整个过程可以用一句话概括:Session是服务端保存的「钥匙」,Cookie是客户端持有的「钥匙副本」,两者配合完成登录状态的维持。这是一种查表式的校验,sessionId只是编号,无任何用户信息,就像一把普通的钥匙,只用于验证锁是否可以开,上面不会标注锁关着的那扇门背后的信息,并且这份校验必须依赖数据库/Redis查询。

2.JWT

        JWT(JSON Web Token)与Session最大的不同在于:服务端不保存任何会话状态,用户信息直接编码在令牌里。这是一种自包含式的校验,令牌本身就是「钥匙」,上面直接标注了锁背后那扇门的信息。下面我们完整梳理一遍JWT登录的完整流程:

  1. 用户提交凭证:用户在登录页输入用户名和密码,浏览器将其发送到服务端。
  2. 服务端校验:服务端拿到用户名和密码后,与数据库中的用户信息进行比对。校验通过后,进入下一步。
  3. 生成JWT:服务端将用户信息作为载荷,生成一个JWT令牌。
  4. 下发令牌:服务端将JWT返回给客户端。客户端将其存储。
  5. 后续请求携带:浏览器后续每次请求都会在请求头Authorization:Bearer <token>中携带该JWT。
  6. 服务端验签:服务端使用密钥对 JWT 的签名进行校验,确认令牌未被篡改,并检查过期时间。若验签通过且未过期,则视为已登录,放行请求;否则返回未登录或跳转登录页。
  7. 退出登录:由于服务端不保存状态,退出登录只需客户端删除本地存储的JWT即可。

        整个过程也可以用一句话概括:JWT 是客户端持有的「自包含钥匙」,服务端通过验签即可确认身份,无需查表。这是一种自校验式的方案,其中的验签机制是关键,令牌本身携带用户信息,服务端只需验证签名即可完成登录状态的确认,无需查表。

三、具体差异

        基础概念说完了,我们来具体说说这两种登录认证方式有什么差异。明面上看,只有查不查表的区别,似乎都是先通过提交账密表单得到一张“通行证”,只不过在通过拦截时,Session的通行证需要调库查表校验通过,JWT的通行证则在拦截关卡当场就能校验通过。但实际上,它们的底层原理以及延伸出来的优劣势具有很大差异。

1.编码差异

        Session的通行证是比对那串sessionId,它本质上后端随机生成的字符串,例如:3F2504E0-4F89-11D3-9A0C-0305E82C3301,没有固定格式规范,也可以是简单随机数字、字母组合,只是普通的ASCII字符,单纯的索引,并具有唯一性。

        相比之下,JWT的token通行证就要复杂得多,因为需要当场校验,所以需要存储的信息自然会更多更复杂。它由固定的三段结构组成,用 . 分隔。

        如上图所示,前两段header和payload使用Base64编码,第三段关键签名则使用HMAC/RSA算法对header.payload算出签名再做编码,这样下来,整个JWT就是自包含的完整凭证,编码后的字符串内部就能自带用户信息,即载荷内置业务信息。这样的编码方式之所以能够实现令牌校验,是因为签名部分使用了服务端持有的密钥进行加密,客户端无法伪造,当服务端收到令牌时,会用同样的密钥对header.payload重新计算签名,并与令牌携带的签名进行对比。只要两者一致,就说明令牌在传输过程中没有被篡改,且确实由服务端签发,从而在无需查询数据库的情况下,仅凭验签即可确认用户身份。

2.痛点差异

        相较于JWT这种“现拦现验不查库”的机制,Session登录校验对于调库查询的依赖引出了一系列短板。首先就是最关键的集群痛点,因为服务器集群分布的场景与负载均衡Nginx的独特机制,使得用户的请求可能被分发到集群里任意一台机器,假如第一次登录请求打到了服务器A,那么sessionId就会存入A的本地内存;等下一次请求被随机分配到服务器B时,因为B没有这个sessionID,就会判定用户未登录,从而要求用户重新登陆,因此为了解决这个问题,我们给出一个方案:不用本机内存存Session,而是抽离到公共的中间件Redis。这是集群Session最基础的改造,但这样仅仅解决了服务器之间会话不互通的问题,并且引出了一个更大的痛点:Redis因此变成了整个登录体系的单点核心!

        什么意思呢?意思就是集群中所有的后端机器在每次用户请求时都必须访问Redis读取会话数据,所有登录相关的鉴权请求全部压到Redis上面,这使得Redis成为了整个系统登录功能的唯一核心,我们知道,Redis在高并发下由于其单线程模型会引发响应延迟,一旦Redis宕机或者网络发生抖动,那么所有的用户全部都会无法登录,所有带登录态的接口也会全部都鉴权失败而无法访问。并且在这种高频请求下会产生大量Redis IO,存在较大的性能开销,加上Session一般都会设置超时自动过期,因此很多项目每次访问还要刷新Session过期时间,额外增加Redis写操作,会话共享也会带来额外的网络开销与运维成本(目的是保障Redis的高可用:主从、哨兵和集群)。

        总结来说,Session的身份认证是强依赖外部存储的,在集群环境下,为了使多台机器读到同一份会话,必须把会话抽离到公共Redis,这就导致了Redis变成整个登录态的单点,每次请求都需要远程查询,带来性能、可用性和额外运维成本。

        那么JWT就一定更好吗?我们来看看JWT有什么痛点。首先,JWT令牌本身没有状态,它更像是一张一经发行就“锁死”的固定门票,初步的令牌校验因此不需要Session那样的查库校验、也不会因此引发同一系列的痛点;但也正因“锁死”,令牌无法即时失效(原生JWT),这就是JWT最核心的痛点。其实这点很好理解,Session 依靠后端存储会话换取实时可控性,代价是存储 IO 带来并发压力;原生 JWT 通过令牌自带信息、免存储查询获得更高并发性能,但牺牲了令牌即时作废的能力。这种即时性的缺失不仅体现在登录上,更体现在Token本身携带的信息上。因为JWT载荷(即payload)是固定的,用户在旧签发的Token过期前修改昵称、修改权限,是无法同步到JWT中的载荷的,这是令牌“锁死”特点所带来的局限性。也正因如此,就连令牌的过期时间都是硬编码在Token内部的,不能进行动态调整,有效期是签发时就写死在payload里的,运行中无法临时修改(原生JWT)。

        除此之外,JWT还具有其它延伸出来的痛点,例如因为令牌“锁死”的特点使得登录信息不像Session将身份信息更安全地存储在数据库中,而是放在载荷中直接用Base64编码存储,任何人拿到这串JWT都可以解码查看,一旦携带了敏感信息就会直接泄露,因此密钥只用来校验签名和防止篡改,不用于保护载荷内容。同时JWT还有一个巨大隐患,那就是一旦用于加密的密钥泄露并没有及时处理,攻击者就能伪造任意用户的合法JWT并进行登录,因为JWT的验签是严格依赖固定密钥的,相较于Session的sessionId泄露仅仅是一个用户的对话泄露,密钥的泄露危害范围是极大的。

        总结来说,原生 JWT 身份认证强依赖令牌本身与签名密钥,集群环境下无需共享会话存储,多台服务器只要共用同一密钥即可独立验签。但原生 JWT 不支持令牌即时作废,载荷可直接解码,不可存储敏感数据;密钥一旦泄露,可批量伪造用户令牌,风险面更广;令牌内信息签发后固定,用户权限、资料变更无法实时生效。

3.状态模型差异

        Session是有状态(stateful)的认证模型。所谓会话状态,指登录之后产生的用户会话信息,是保存在服务端一侧的。当用户登录成功,服务端生成唯一的 SessionId,同时在后端存储中开辟一条记录,保存本次会话对应的用户身份、登录时间等会话状态。 前端只持有这个简短的 SessionId,它本身不携带任何完整用户信息,仅仅是一个索引编号。每一次后续请求,前端把 SessionId 传给服务端,服务端依靠这个索引,去后端存储中读取预先保存好的会话状态,以此识别当前用户是谁。 整个会话的 “状态本体” 托管在服务器,前端只持有状态的引用,不拥有状态本身。

        原生 JWT 是无状态(stateless)的认证模型。这里的无状态,含义是服务端不需要在本地保存这份会话状态。用户登录成功后,服务端把本次登录需要用到的身份信息,打包封装进 JWT 令牌内部,加上数字签名后下发给前端。 令牌本身就承载了全部会话状态,前端持有这份完整的状态载体。后续请求,前端把令牌传回后端,后端不需要查询外部存储获取会话记录,只需要校验签名,就能直接从令牌内部提取出会话状态。 会话状态随同令牌保存在客户端,服务端不维护、不存储这份会话记录,服务端本身不保存会话的状态本体。

        状态模型是最核心概念差异,也是前文所述差异在概念上的根因,两种不同的认证模型导向了两种截然不同的登录模式,也产生了各自的优劣,不存在一定的孰强孰弱,只有在不同应用场景下的取舍选择。

四、适用场景

        既然说到在不同应用场景下的取舍选择,那我们就具体展开说说到底什么场景与什么选型适配以便我们进一步理解各自的优劣性。

1.Session选型

        Session的核心优势是会话状态由后端全权管控,支持会话即时失效、用户信息实时生效。而它的短板来自每次请求都要访问存储,高并发下存储会成为瓶颈。当业务满足下面任意一类特征,我们就优先考虑使用Session。

        第一,业务存在大量账号管控操作,需要支持立刻踢下线、账号冻结、强制登出。比如后台管理系统、电商、二手交易平台这类系统。管理员可以封禁用户账号,或者用户修改密码后,需要让该用户其他设备上已登录的会话马上失效。Session 只需要在 Redis 删除对应的会话记录,下一次请求就直接拦截,实现简单且稳定。如果用原生JWT,其本身是不具备这个能力的,需要额外开发黑名单或者版本号逻辑,增加开发工作量。
        第二,系统权限需要实时变更,要求权限修改后马上生效。例如后台管理员修改用户角色、增减接口权限。Session每次请求都会读取后端存储里的会话信息,权限变更后,下一次请求就能拿到最新权限。而JWT令牌内的权限信息在签发时就固定,不重新登录就无法更新。对于权限变动频繁的业务,Session 的实时性优势非常明显。
        第三,项目体量为中小型,并发压力不算极端,追求开发简单、维护成本低。中小型项目不需要应对每秒上万请求的超高并发,Redis的查询开销完全可以接受。Spring Session集成Redis开箱即用,代码简单,坑少,不需要自己处理令牌签发、验签、刷新令牌、黑名单等复杂逻辑。团队维护成本更低,出现问题更容易排查。
        第四,业务主要是 Web 网页端,以浏览器访问为主,对多端适配没有强需求。Session依靠Cookie传递SessionId,浏览器原生支持Cookie,开发简单。如果项目主要用户都通过网页访问,不需要对接 APP、小程序,那么Session则更天然适配网页场景。

2.原生JWT选型

        原生JWT的核心优势是服务端无会话状态、无需存储、集群零依赖、跨端适配性强,而短板则是无法即时作废、数据不实时、存在密钥泄露风险。因此原生 JWT 更适合追求高并发稳定性、多端统一鉴权、对会话实时管控容忍度较高的业务场景。当业务满足下面任意一类特征,我们则可以优先考虑使用原生JWT。

        第一,高并发、海量用户的分布式/微服务业务,追求极致接口吞吐与稳定性。原生JWT鉴权完全基于本地算法验签,无需远程查询Redis、数据库,没有网络IO开销。在秒杀、资讯浏览、首页推荐、公共接口等高频访问场景中,不会出现Session方案的Redis查询瓶颈,能极大提升系统并发上限。同时多服务集群无需共享会话存储,所有微服务只需统一密钥即可完成鉴权,大幅降低分布式系统的运维与耦合成本。
        第二,多端统一接入的业务,需要同时适配浏览器、APP、小程序、第三方客户端。Session依赖Cookie传递会话ID,受浏览器同源策略、跨域限制、Cookie禁用等问题制约,无法适配全终端场景。而JWT通过Header自定义令牌传输,不依赖Cookie,因此跨域兼容性极强,天然就适配多端统一登录、统一鉴权的业务架构,是移动端、小程序项目的首选基础方案。
        第三,前后端完全分离项目,无服务端页面渲染,纯接口交互模式。现代前后端分离架构中,前端独立部署、域名独立,跨域是常态需求。而Session的Cookie机制需要复杂的跨域配置、证书配置,适配繁琐且容易出现兼容方面的bug。原生JWT脱离既Cookie体系,其鉴权逻辑纯粹基于令牌与签名,因此完美契合前后端分离的架构设计,开发适配更简洁、坑点也会更少。
        第四,业务对会话实时性要求低,极少需要踢下线、改权限的轻量化场景。例如资讯网站、博客系统、普通展示型会员系统。这类业务几乎不需要强制用户下线、实时冻结账号、动态修改权限,令牌短暂过期滞留不会产生安全与业务风险。原生JWT无需额外开发黑名单、版本号等增强逻辑,用最简代码即可完成登录鉴权,兼顾性能与开发效率。

3.增强型JWT(黑名单/版本号机制)选型

        在说明适用场景之前,先简单介绍一下增强型JWT,其本质就是在保留原生JWT高并发、无状态、跨端优势的前提下,通过少量服务端状态补偿,修复原生JWT无法即时失效的致命缺陷。它主动放弃了“完全无状态”的极致简洁,通过Redis黑名单或数据库版本号(这里对具体原理感兴趣的同学可以搜索相关信息,文章内便不过多赘述了),换取会话可管控、可强制下线、状态可实时校验的能力,是工程中最常用、最均衡的折中方案。

        第一,前后端分离 + 高安全业务并存的场景。项目架构需要适配前后端分离、跨域、多端登录,不能用传统Cookie-Session体系;但业务上又属于交易类平台,涉及用户资产、订单、商品,必须支持改密码踢全部设备、账号封禁即时失效、盗号风险紧急下线。原生JWT安全性不足、可控性太差,纯Session又不适合前后端分离架构,此时增强JWT是唯一最优解。
        第二,微服务分布式架构,不想维护全局会话Redis,但又需要账号实时管控的纯Session方案需要所有微服务接入统一会话Redis,耦合度高、存在单点瓶颈;原生JWT完全无状态无法管控账号风险;增强JWT仅在鉴权时做一次轻量校验(版本号/黑名单),不需要维护海量会话数据,既保留了分布式解耦优势,又补齐了安全短板。
        第三,用户权限、账号状态可变,要求数据不能严重滞后。对于会员系统、用户中心、后台权限系统,管理员可以随时封禁用户、调整权限、限制功能。原生JWT因其签发后信息固定,用户被封禁后依旧能操作直到过期,存在安全漏洞。 而增强JWT每次请求都会校验账号状态/Token版本号/黑名单状态,可以保证账号变更、权限变更快速生效,规避原生 JWT 的数据滞后问题。 

五、总结归纳

        综上所述,三种认证方案的选型,其本质是在性能、实时性、安全性、架构适配性与开发成本之间做取舍,并不存在绝对的固定最优方案,只有综合考量下的相对适配选择。三种认证模型的核心差异本质是会话状态的存储位置与管控方式不同:Session将状态留存服务端,以性能代价换实时安全;原生JWT将状态下放客户端,以可控性代价换并发性能;增强型JWT通过状态补偿,实现了性能、安全、架构的动态平衡。在实际开发中,技术选型永远需要贴合业务场景,摒弃“新技术更优”的刻板认知,根据项目体量、并发压力、安全需求、终端形态选择适配方案,才是后端认证架构设计的核心思路。

        技术更新迭代,但需求千变万化,别让技术的浪潮席卷独立的思考。

Logo

一站式 AI 云服务平台

更多推荐