什么是 JWT
在现代 Web 开发中,身份认证与授权是保障系统安全的核心环节。随着前后端分离架构和微服务的普及,传统基于Session的认证方式面临着跨域、服务器存储压力大等问题。而 JWT(JSON Web Token)作为一种轻量级、无状态的认证方案,凭借其自包含、可跨域、易扩展的特性,逐渐成为主流选择。
JWT全称为JSON Web Token,是基于RFC 7519标准定义的一种紧凑、自包含的令牌格式,用于在不同系统间安全地传递结构化的JSON数据,是目前最流行的跨域认证解决方案
JWT 是一种基于 Token 的认证授权机制,从名字就能知道了,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。
其核心价值在于可验证性 与自包含性:
可验证:通过数字签名确保数据未被篡改,接收方可通过签名反向验证令牌合法性;
自包含:令牌本身携带用户身份、权限、有效期等关键信息,无需频繁查询数据库或缓存;
跨平台兼容:基于JSON格式和Base64编码,支持所有主流编程语言和框架;
轻量灵活:体积远小于XML格式的令牌(如SAML),可通过URL、HTTP头或POST参数轻松传输。
JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。
可以看出,JWT 更符合设计 RESTful API 时的「Stateless(无状态)」原则 。
下面是 RFC 7519 对 JWT 做的较为正式的定义。
JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted. ——JSON Web Token (JWT)
跨域认证的问题
JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案
在单服务器架构下,上述基于 Session 的认证流程能够稳定运行,但随着互联网服务的发展,分布式系统和多域名场景日益普遍,传统 Session 认证逐渐暴露出难以解决的跨域问题
互联网服务离不开用户认证。一般流程是下面这样。
1、用户向服务器发送用户名和密码。
2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
3、服务器向用户返回一个 session_id,写入用户的 Cookie。
4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
在单服务器架构下,上述基于 Session 的认证流程能够稳定运行,但随着互联网服务的发展,分布式系统和多域名场景日益普遍,传统 Session 认证逐渐暴露出难以解决的跨域问题
浏览器的 “同源策略” 规定:只有当两个页面的协议、域名、端口完全一致时,才算 “同源”,此时 Cookie 才能正常传递。若用户需要访问的服务分散在不同域名下(例如,用户在a.com登录后,还需访问b.com的资源),由于a.com服务器写入的 Session Cookie 无法被b.com的服务器读取,b.com无法通过 Session ID 识别用户身份,导致用户需要重复登录,严重破坏用户体验。
在分布式架构中,用户的请求可能被分发到不同的服务器节点(例如,通过负载均衡将请求分配给服务器 A 或服务器 B)。传统 Session 数据通常保存在单个服务器的内存中,若用户首次请求被分配到服务器 A 并创建 Session,下次请求若被分配到服务器 B,由于服务器 B 没有该用户的 Session 数据,会判定用户未登录,导致认证失效。
为解决 Session 共享问题,开发者常采用 “Session 集群”(如 Redis 集中存储 Session)等方案,但这些方案不仅增加了系统的复杂度和运维成本,还可能因集中存储节点的故障引发整个认证系统的可用性风险。
传统 Session 认证依赖 Cookie 实现 Session ID 的传递,但在移动端(如 iOS、Android 应用)中,Cookie 的支持并不完善,且移动端应用更倾向于通过 HTTP 请求头(而非 Cookie)传递认证信息。若强行在移动端使用 Session 认证,需要额外开发适配逻辑,增加了开发成本,同时也可能因 Cookie 的安全性问题(如 CSRF 攻击)给应用带来风险。
为解决传统 Session 认证在跨域、分布式、移动端场景下的缺陷,JSON Web Token(JWT)应运而生。JWT 是一种基于 JSON 的轻量级认证令牌,它将用户身份信息加密后存储在令牌中,通过客户端(如浏览器、移动端 App)主动携带令牌的方式实现认证,无需服务器存储 Session 数据,从根本上解决了跨域认证的核心痛点。
相比传统 Session 认证,JWT 的认证流程更简洁,且天然支持跨域场景,具体步骤如下:
- 用户首次登录:用户向服务器发送用户名、密码等认证信息;
- 服务器验证与生成 JWT:服务器验证用户信息通过后,根据用户 ID、角色等信息构建 JWT 的载荷,结合头部声明的算法和服务器密钥生成 JWT 令牌;
- 服务器返回 JWT:服务器将 JWT 令牌以 JSON 格式(如{“token”: “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…”})返回给客户端,无需写入 Cookie;
- 客户端存储 JWT:客户端(浏览器、移动端 App)收到 JWT 后,可将其存储在localStorage(浏览器)、SessionStorage或应用内存(移动端)中,而非依赖 Cookie;
- 后续请求携带 JWT:用户后续发起请求时,客户端需在 HTTP 请求头中携带 JWT 令牌
- 服务器验证 JWT:服务器收到请求后,提取请求头中的
JWT 令牌,按以下步骤验证:
- 拆分 JWT 的头部、载荷、签名三部分,对头部和载荷进行 Base64 解码;
- 检查头部声明的签名算法,使用服务器密钥对解码后的头部和载荷重新生成签名;
- 对比重新生成的签名与 JWT 中的原签名:若一致,说明令牌未被篡改;若不一致,直接拒绝请求;
- 验证载荷中的exp(过期时间)字段,若当前时间已超过exp,说明令牌已过期,拒绝请求;
- 验证通过后,从载荷中提取用户身份信息(如userId),基于该信息处理业务逻辑(如返回用户专属数据)。
JWT 解决跨域认证的核心优势
无状态性:服务器无需存储任何 Session 数据,所有用户身份信息都包含在 JWT 令牌中,降低了服务器的存储压力,同时使分布式系统中的各个节点无需同步 Session 数据,简化了系统架构;
跨域兼容性:JWT 通过 HTTP 请求头传递,不受浏览器同源策略限制,可轻松支持多域名、跨域接口调用场景(如a.com的前端调用api.b.com的接口);
移动端友好:无需依赖 Cookie,可在 iOS、Android 等移动端应用中灵活存储和传递,适配性更强;
安全性可控:通过签名机制保障令牌不被篡改,同时可通过exp字段设置令牌过期时间,降低令牌泄露后的安全风险(即使令牌被窃取,过期后也无法使用)。
并且, 使用 JWT 认证可以有效避免 CSRF 攻击,因为 JWT 一般是存在在 localStorage 中,也就是浏览器内存中,使用 JWT 进行身份验证的过程中是不会涉及到 Cookie 的。
JWT 的原理
JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。
1 | { |
以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。
服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。
JWT 的数据结构
JWT 本质上就是一组字串,通过(.)切分成三个为 Base64
编码的部分:
JWT 令牌的完整格式如下:
1 | [Header].[Payload].[Signature] |
它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT
内部是没有换行的
这三个部分分别承担不同的功能,且均采用 Base64URL 编码(一种适用于 URL 的 Base64 变体,替换了部分特殊字符),便于在网络中传输。
头部(Header)
头部用于描述 JWT 的基本信息,主要包含两个字段:
alg:声明签名算法(如 HS256、RS256 等)typ:声明令牌类型,固定为 “JWT”
示例头部的 JSON 结构:
1 | { |
经过 Base64URL 编码后,上述头部会转换为类似这样的字符串(作为 JWT 的第一部分):
1 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 |
载荷(Payload)
载荷是 JWT 的核心部分,用于存储需要传递的用户数据和元信息。它包含三种类型的声明(Claims):
- 标准声明(Registered
Claims):预定义的可选字段,用于描述令牌的基本属性,常见的有:
iss:令牌签发者(Issuer)sub:令牌主题(Subject),通常为用户 IDaud:令牌接收者(Audience)exp:令牌过期时间(Expiration Time),以 Unix 时间戳表示nbf:令牌生效时间(Not Before),在此时间前令牌无效iat:令牌签发时间(Issued At)jti:令牌唯一标识符(JWT ID),用于防止重放攻击
- 公共声明(Public Claims):JWT 签发方可以自定义的声明,也就是可以由开发者自定义的字段,但需注意避免与标准声明冲突,通常用于传递业务相关信息(如用户名、角色等)。
- 私有声明(Private Claims):JWT 签发方因为项目需要而自定义的声明,一般是由服务端和客户端协商定义的字段,仅在特定场景下使用。
示例载荷的 JSON 结构:
1 | { |
经过 Base64URL 编码后,上述载荷会转换为类似这样的字符串(作为 JWT 的第二部分):
1 | eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjQyNjIyfQ |
请注意,Base64URL 编码是可逆的,他不是安全加密,因此载荷中绝对不能存放敏感信息(如密码、银行卡号等),只能存储非敏感的身份标识或业务数据。
你可以在 jwt.io 这个网站上对其 JWT 进行解码,解码之后得到的就是 Header、Payload、Signature 这三部分。
签名(Signature)
签名是 JWT 的安全保障,用于验证 JWT令牌(主要是payload)在传输过程中是否被篡改,以及确保令牌确实由合法的服务器签发。
签名的生成步骤如下:
- 用 Base64URL 编码头部和载荷,得到两个字符串
- 将这两个字符串用句号拼接,形成
HeaderEncoded.PayloadEncoded - 使用头部中声明的签名算法(如 HS256),结合服务器端的密钥(Secret)对拼接后的字符串进行加密,生成签名
以 HS256 算法为例,签名的伪代码如下:
1 | HMACSHA256( |
生成的签名字符串作为 JWT 的第三部分,例如:
1 | SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c |
JWT 的三段式结构设计既满足了数据传输的简洁性(单一字符串便于在请求头中携带),又通过签名机制确保了数据的完整性和真实性。服务器在验证 JWT 时,只需重新计算签名并与令牌中的签名比对,即可判断令牌是否有效,无需查询数据库或缓存,这也是 JWT 适用于跨域和分布式场景的核心原因。
如何基于 JWT 进行身份验证?
这部分要求了解 Spring Security 的用户认证部分
在基于 JWT 进行身份验证的应用程序中,服务器通过 Payload、Header 和 Secret(密钥)创建 JWT 并将 JWT 发送给客户端。客户端接收到 JWT 之后,会将其保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。
所以说, JWT 进行身份验证的整体步骤涉及到一个 Token 下发和 Token 回传这两个回环步骤完成验证,简单来说就是
用户向服务器发送用户名、密码以及验证码用于登陆系统;
认证通过,服务端会根据用户信息返回已经签名的 Token,也就是 JWT;
客户端收到 Token 后自己保存起来,一般是浏览器的
localStorage因为你放 Cookie 那不和 Session 一样会引起 CSRF 了吗
客户端以后每次向后端发请求,都需要在 Header 中带上这个 JWT
请求服务端并携带 JWT 的常见做法是将其放在 HTTP Header 的
Authorization字段中服务端检查 JWT 来校验请求是否合法,并从中获取用户相关信息。
spring-security-jwt-guide 就是一个基于 JWT 来做身份认证的简单案例
如何防止 JWT 被篡改
有了签名之后,即使 JWT 被泄露或者截获,黑客也没办法同时篡改 Signature、Header、Payload。
但是上面说了,JWT 并不是不可见来进行保护的,Payload 是 Base64 编码可解码,而 Signature 是确保不被篡改的部分,那么是如何做到一旦 Token 的任何部分被修改,服务端验证签名时会直接失败的?
因为服务端拿到 JWT 之后,会解析出其中包含的 Header、Payload 以及
Signature ,然后通常会解码然后修改相对明文的 Payload
部分,例如把普通用户的role: "user"改成role: "admin",然后重新编码
Header 和 Payload,拼接成新的前两部分;
但是,服务端会根据 Header、Payload、密钥再次生成一个 Signature,你修改了其中的一部分,生成的 Signature 就肯定不一样。因为这本质上也可以看成公钥私钥对,对 Header 和 Payload 的组合进行加密生成的 Signature 签名串一定是唯一的。拿新生成的 Signature 和原先 JWT 中的 Signature 作对比,如果一样就说明 Header 和 Payload 没有被修改,但是很明显,上述修改的对比一定会失败。
签名的生成流程如下,以最常用的 HS256 为例
1 | 签名 = HMACSHA256( |
- Base64UrlEncode 是 JWT 专用的 Base64 编码
- Secret Key 是服务端的 核心机密,绝对不能泄露,一旦泄露,攻击者就能伪造签名、
然后当服务端收到 JWT 时,会执行以下步骤验证是否被篡改:
- 拆分 Token 为 Header、Payload、Signature 三部分;
- 对 Header 和 Payload 重新执行 Base64UrlEncode
并拼接(
Header编码.Payload编码); - 用自己保存的密钥,通过 Header 声明的算法(如 HS256)重新计算签名;
- 对比 重新计算的签名 和 Token 中的 Signature
所以说,如果服务端的秘钥 Secret Key 也被泄露的话,黑客就可以同时篡改 Signature、Header、Payload 了。黑客直接修改了 Header 和 Payload 之后,再重新生成一个 Signature 就可以了。
密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥
整个流程示例代码如下
1 | import io.jsonwebtoken.*; |
上面演示的是 HS256 这种对称加密算法,还有非对称加密算法(RS256/RS512),它更安全,微服务更优先考虑这种方案,只不过是生成签名用私钥(仅服务端持有),验证签名用公钥(可公开)
这样,攻击者即使拿到公钥,也无法生成私钥签名的 Token,安全性比对称加密更高;
如何防止 JWT 被截获和冒用
很多人担心 JWT 被截获后引发类似 Session 的 CSRF 问题
JWT 本身不会天然导致 CSRF,但如果 Token 被截获,攻击者可能冒充用户发起请求,这本质是令牌窃取风险,所以,如何防止 JWT 被截获和冒用
传统 CSRF 是 利用浏览器自动携带 Cookie,而 JWT 被截获是 攻击者拿到 Token 字符串后,手动添加到请求头发起请求
因此,我们的防护目标是:
- 防止 Token 被截获;
- 即使被截获,也无法被攻击者使用。
那么,思路有了,方案自然也有了
安全存储 JWT
这是避免 JWT 从客户端本地被窃取
JWT 最常见的存储位置是
localStorage/sessionStorage,但这两个位置容易被 XSS 攻击窃取这种情况下一般就有几种面对方案,在 https://www.ergoutreegal.cn/posts/36724.html?highlight=csrf,我也有说
最简单的方法是开启
HttpOnly,核心是防 XSS,效果还不错安全传输 JWT
这是防 中间人攻击 的核心,必须确保 Token 在网络中不被明文传输
- 强制使用 HTTPS 可以极好的解决这个问题,就是要搞证书,emmmm,所有请求必须走 HTTPS,自己就加密了
- Token 放在请求头,一般是
Authorization请求头,这样下的 HTTPS 加密传输更安全; - 缩短 Token 有效期,即使 Token 被截获,有效期越短,攻击者可利用的时间窗口越小,一般情况下,我是将访问令牌 Access Token 有效期控制在 3 小时内,刷新令牌 Refresh Token 有效期 7 天
Access Token + Refresh Token
典型的双令牌机制,Access Token 通常短期有效,Refresh Token 通常长期有效,它仅用于获取新的 Access Token,存储在更安全的位置,一般存储在数据库或者Redis中
Access Token 过期后,前端用 Refresh Token 请求新的 Access Token,服务端验证 Refresh Token 合法后,签发新的 Access Token,同时吊销旧的,这也是登录流程中记住我的核心机制
增加 Token 的使用限制
这部分就比较宽泛了,例如比较常见的就有
- 开启 Token 黑名单:服务端维护一个 Token 黑名单,这个下面细说
- 2FA:敏感操作除了 Token,还要求输入验证码二次认证,即使 Token 被截获,攻击者也无法完成敏感操作。
做好防护 XSS 攻击
JWT 的优势
相比于 Session 认证的方式来说,使用 JWT 进行身份认证主要有下面 4 个优势。
无状态
JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 JWT 信息,只需要解析它就可以。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。
不过,也正是由于 JWT 的无状态,也导致了它最大的缺点:不可控!
就比如说,我们想要在 JWT 有效期内废弃一个 JWT 或者更改它的权限的话,并不会立即生效,通常需要等到有效期过后才可以。再比如说,当用户 Logout 的话,JWT 也还有效。除非,我们在后端增加额外的处理逻辑比如将失效的 JWT 存储起来,后端先验证 JWT 是否有效再进行处理。具体的解决办法,我们会在后面的内容中详细介绍到,这里只是简单提一下
有效避免了 CSRF 攻击
CSRF(Cross Site Request Forgery) 一般被翻译为 跨站请求伪造,属于网络攻击领域范围。相比于 SQL 脚本注入、XSS 等安全攻击方式,CSRF 的知名度并没有它们高。但是,它的确是我们开发系统时必须要考虑的安全隐患。就连业内技术标杆 Google 的产品 Gmail 也曾在 2007 年的时候爆出过 CSRF 漏洞,这给 Gmail 的用户造成了很大的损失。
那么究竟什么是跨站请求伪造呢? 简单来说就是用你的身份去做一些不好的事情
CSRF 攻击需要依赖 Cookie ,Session 认证系统中 Cookie 中的
SessionID
是由浏览器发送请求时候携带然后发送到服务端的,只要发出请求,Cookie
就会被携带。借助这个特性,即使黑客无法获取你的
SessionID,只要让你误点攻击链接,拿到了其中自动携带的
SessionID,就可以达到攻击效果。所以说,很多时候,只要你打开了某个页面,CSRF
攻击就会发生,最常见的就是 Steam 的投票,送礼等盗号
那为什么 JWT 不会存在这种问题呢?
一般情况下我们使用 JWT 的话,在我们登录成功获得 JWT 之后,一般会选择存放在 localStorage 中。前端的每一个请求中都一般会在请求体里附带上这个 JWT,整个过程压根不会涉及到 Cookie,除非你存到 Cookie 中。
因此,即使你点击了非法链接发送了请求到服务端,这个非法请求也是不会携带 JWT 的,因为 JWT 它不是自动携带的,是需要手动处理才能带上的,所以这个请求将是非法的。
总结来说就一句话:使用 JWT 进行身份验证不需要依赖 Cookie,因此不会涉及到自动传递的问题,因此可以避免 CSRF 攻击。
不过,这样也会存在 XSS 攻击的风险。为了避免 XSS 攻击,你可以选择将
JWT 存储在标记为httpOnly 的 Cookie
中。但是,这样又导致了你必须自己提供 CSRF
保护,因此,实际项目中我们通常也不会这么做。
常见的避免 XSS 攻击的方式是过滤掉请求中存在 XSS 攻击风险的可疑字符串。
在 Spring 项目中,我们一般是通过创建 XSS 过滤器来实现的。
适合移动端
使用 Session
进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到
Cookie(需要 Cookie 保存
SessionId),所以不适合移动端。
但是,使用 JWT 进行身份认证就不会存在这种问题,因为只要 JWT 可以被客户端存储就能够使用,发送请求时候携带就可以,而且 JWT 还可以跨语言使用。
为什么使用 Session 进行身份认证的话不适合移动端 ?
- 状态管理: Session 基于服务器端的状态管理,而移动端应用通常是无状态的。移动设备的连接可能不稳定或中断,因此难以维护长期的会话状态。如果使用 Session 进行身份认证,移动应用需要频繁地与服务器进行会话维护,增加了网络开销和复杂性;
- 兼容性: 移动端应用通常会面向多个平台,如 iOS、Android 和 Web。每个平台对于 Session 的管理和存储方式可能不同,可能导致跨平台兼容性的问题;
- 安全性: 移动设备通常处于不受信任的网络环境,存在数据泄露和攻击的风险。将敏感的会话信息存储在移动设备上增加了被攻击的潜在风险。
而且 JWT 天生跨语言,JWT 有标准化的解析 / 验证库,所有语言都能统一处理,无需适配不同的 Session 逻辑。
天生适合微服务
微服务架构的核心是服务解耦和独立部署,Session 的 中心化状态管理 与微服务的设计理念完全冲突,而与 JWT 的 无状态 特性天然适配。
Session 的核心是 服务器存储 SessionId→用户信息的映射,在微服务中会遇到很多问题,包括会话共享困难,跨服务认证复杂,同步存在延迟等
单点登录友好
单点登录(SSO)的核心是一次登录,多系统访问
Session 的 Cookie 跨域限制和状态中心化,让 SSO 实现成本很高,而 JWT 几乎是为 SSO 量身定制的,牛大了
使用 Session 进行身份认证的话,实现单点登录,需要我们把用户的 Session 信息保存在一台电脑上,并且还会遇到常见的 Cookie 跨域的问题。但是,使用 JWT 进行认证的话, JWT 被保存在客户端,不会存在这些问题
JWT 不依赖 Cookiee,可通过 登录中心颁发 Token→各系统存储 Token→请求时携带 Token 的方式实现跨域认证,这样也自然能够跨系统
- 用户在登录中心登录,获取 JWT
- 其他系统通过前端存储等其他方式获取 JWT;
- 各系统请求时携带 JWT,无需依赖 Cookie,自然就突破了跨域限制;
实际开发中使用 JWT
依赖和配置
来到 JWT 官网找找依赖,我们用的比较多的就是右边这两个 Java 的,我们这次就用 jose4j
我们使用这个 jwt 的实现来作为依赖导入我们的项目
1 | <dependency> |
首先,我们需要在配置文件里写上关于 JWT 的一些信息
1 | # 这是生成 JWT 签名时使用的密钥 |
编写创建和解析 JWT 令牌的工具类
然后我们创建 JWT 工具类
在这个工具类中,我们一般会创建 JWT 令牌,验证 JWT 令牌,然后从 JWT 令牌中拿出来一些你需要的内容
1 | package net.onest.aries.util; |
完善业务
随便写一个 repository
1 | /** |
然后把用户服务的接口和实现类写好
1 | /** |
1 | package net.onest.aries.user.service.impl; |
如果你写了 DTO,请务必在登陆响应的 DTO 中把 token 放进去
编写控制器和 JWT 拦截器
接下来我们创建我们登录的控制器,也就是认证控制器,我们使用纯 JWT 验证安全情况
1 | package net.onest.aries.user.controller; |
我们再写一个JWT拦截器,用于验证请求中的JWT令牌,调用的还是上面 JwtUtil 的方法
1 | package net.onest.aries.config; |
接下来我们写一个配置类,标记一下都拦截什么地址
1 | /** |
写一个受保护的 api,看看 jwt 能否生效
1 | package net.onest.aries.user.controller; |
懒得搞数据库了,因为是从老项目上摘取然后改的,估计是差不多能对的
你可以创建一个 POST 请求试试
URL:
http://localhost:8080/api/auth/loginHeaders:
Content-Type: application/jsonBody (raw JSON):
1
2
3
4{
"username": "testuser",
"password": "password"
}
发送请求,如果成功,你将收到类似以下的响应:
1 | { |
保存返回的token,我们将在后续请求中使用它
然后创建一个GET请求:
- URL:
http://localhost:8080/api/users/me - Headers:
Content-Type: application/jsonAuthorization: Bearer eyJhbGciOiJIUzI1NiJ9...(使用上一步获取的token)
发送请求,如果JWT验证成功,你将收到用户信息
可以使用在线工具如 jwt.io 来解析和验证JWT令牌
JWT 身份认证常见问题及解决办法
注销登录等场景下 JWT 还有效
与之类似的具体相关场景有:
- 退出登录;
- 修改密码;
- 服务端修改了某个用户具有的权限或者角色;
- 用户的帐户被封禁/删除;
- 用户被服务端强制注销;
- 用户被踢下线;
等等,它们都是一个问题,用户退出认证了,Token 依旧有效
这个问题不存在于 Session 认证方式中,因为在 Session 认证方式中,遇到这种情况的话服务端删除对应的 Session 记录即可。
但是,JWT 一旦派发出去,如果后端不增加其他逻辑的话,它在失效之前都是有效的。
那我们如何解决这个问题呢?
Token入库
这个方法比较暴力,却是理解黑名单机制的基础,它是将有效的 JWT 存入 Redis 这种内存数据库中
如果需要让某个 JWT 失效就直接从 Redis 中删除这个 JWT 即可。
但是,这样会导致每次使用 JWT 都要先从 Redis 中查询 JWT 是否存在的步骤,大量请求下肯定影响性能,而且违背了 JWT 的无状态原则。
看看就好
黑名单机制
黑名单机制是解决这个问题最成熟、最通用的方案,它本质是在服务端维护一份 已失效但未过期的 JWT 列表,验证 Token 时先检查是否在黑名单中,再验证签名和有效期。
首先,JWT 为什么不能撤回,因为服务端无状态,而黑名单机制是再其基础上增加一个过期了的状态
使用内存数据库比如 Redis 维护一个黑名单,如果想让某个 JWT 失效的话就直接将这个 JWT 加入到 黑名单 即可。然后,每次使用 JWT 进行请求的话都会先判断这个 JWT 是否存在于黑名单中
虽然这种方案都违背了 JWT 的无状态原则,但是一般实际项目中我们通常还是会使用这种方案。
首先,黑名单只存储需要提前失效的 JWT,例如注销下号,更换账号密码等对用户认证状态或者认证信息出现更改的时候,才需要加入,大部分正常业务的 JWT 无需记录
而且,JWT 本身有过期时间,黑名单无需永久存储,要有到期自动清理的特性
为什么是 Redis,因为 Redis 的键值对 + 过期时间特性,完美匹配黑名单的需求
flowchart TD
A[客户端携带JWT发起请求] --> B[服务端提取JWT]
B --> C{检查JWT是否在黑名单中?}
C -->|是| D[拒绝请求,返回401]
C -->|否| E{验证JWT签名+有效期?}
E -->|否| D
E -->|是| F[正常处理请求]
G[触发失效场景:注销/改密码/封禁] --> H[将该用户的JWT加入黑名单]
H --> I[设置黑名单过期时间=JWT剩余有效期]
简单看一下之前写过的一个代码
1 | /** |
别忘了在 Spring Security 的过滤器中,把黑名单检查这一步集成进去
1 |
|
针对注销登录、改密码、封禁账号等场景,也需要调用加入黑名单方法
1 | // 场景1:注销登录 |
修改密钥
我们为每个用户都创建一个专属密钥,如果我们想让某个 JWT 失效,我们直接修改对应用户的密钥即可。
但是这种我们只是说一下,没有人真这么干,因为这后果太严重
如果服务是分布式的,则每次发出新的 JWT 时都必须在多台机器同步密钥。为此,你需要将密钥存储在数据库或其他外部服务中,这样和 Session 认证就没太大区别了。
如果用户同时在两个浏览器打开系统,或者在手机端也打开了系统,如果它从一个地方将账号退出,那么其他地方都要重新进行登录,这是不可取的。
保持令牌的有效期限短并经常轮换
唯一真神
为什么你呆着呆着网站就掉了,大约就是这种情况
所以它是很简单的一种方式。但是,会导致用户登录状态不会被持久记录,而且需要用户经常登录。
另外,对于修改密码后 JWT 还有效问题的解决还是比较容易的。说一种我觉得比较好的方式:使用用户的密码的哈希值对 JWT 进行签名或者再次签名。这样,如果密码更改,则任何先前的令牌将自动无法验证
JWT 的续签问题
JWT 有效期一般都建议设置的不太长,那么 JWT 过期后如何认证,如何实现动态刷新 JWT,避免用户经常需要重新登录?
我们先来看看在 Session 认证中一般的做法:假如 Session 的有效期 30 分钟,如果 30 分钟内用户有访问,就把 Session 有效期延长 30 分钟。
JWT 认证的话,我们应该如何解决续签问题呢?
类似于 Session 认证中的做法
假设服务端给的 JWT 有效期设置为 30 分钟,服务端每次进行校验时,如果发现 JWT 的有效期马上快过期了,服务端就重新生成 JWT 给客户端。客户端每次请求都检查新旧 JWT,如果不一致,则更新本地的 JWT。
比较好用
每次请求都返回新 JWT
这种方案的思路很简单,但是,开销会比较大,尤其是在服务端要存储维护 JWT 的情况下。
JWT 有效期设置到凌晨
你怎么知道我凌晨没在干活?
而且一般网络安全大案都发生在休息日的夜晚或凌晨
用户登录返回两个 JWT
唯一真神
就是上面提到的双 JWT 做法,一个是 AccessToken,另一个是 RefreshToken
AccessToken 过期时间短,另外一个是 RefreshToken 它的过期时间更长一点比如为 1 天。RefreshToken 只用来获取 AccessToken ,这样既保证了使用体验,又保证了安全性,而且是记住我这个业务的一个基础。







