JAVA后端开发—— JWT(JSON Web Token)实践
1. 什么是HTTP请求头 (Request Headers)?
当你的浏览器或手机App向服务器发起一个HTTP请求时,这个请求并不仅仅包含你要访问的URL(比如 /logout)和可能的数据(请求体),它还附带了一堆“元数据(Metadata)”,这些元数据就是请求头。
请求头是一些键值对(Key-Value pairs),它们向服务器提供了关于本次请求的各种上下文信息,比如:
Host: api.example.com (我想访问哪个服务器)
User-Agent: Mozilla/5.0 ... (我是用什么浏览器或设备发起的请求)
Accept: application/json (我希望你返回给我JSON格式的数据)
Content-Type: application/json (如果我发送了数据,那么这些数据的格式是JSON)
Authorization: Bearer eyJhbGciOiJIUzI1Ni... (这是身份凭证)
2. 为什么需要 Authorization 请求头?
HTTP协议本身是无状态的(Stateless)。这意味着服务器默认情况下,每一次收到请求,都把它当作一个全新的、陌生的请求来对待。它不会记得你上一次是谁,或者你是否已经登录过。
为了解决这个问题,我们需要一种机制来让客户端在每次请求时都“自报家门”。Authorization 请求头就是目前最主流的实现方式,特别是配合 Token-based Authentication(基于令牌的认证)
Token认证机制具有安全性。
核心凭证只传一次: 用户的账号密码只在登录那一次通过HTTPS安全地传输到服务器。
暴露的是“临时工”: Token是一个临时的、可控的凭证。它本质上是服务器对“我信任这个客户端”的一次授权声明。即使Token在传输过程中被截获,它的危害也是有限的:
有时效性: Token通常被设置为在几小时或几天后自动过期。攻击者拿到一个过期的Token是完全没用的。
可被吊销: 服务器可以建立一个“黑名单”机制。一旦发现某个Token泄露,可以立刻将其加入黑名单,使其立即失效。
权限可控: Token的Payload(载荷)部分可以包含用户的角色和权限信息。服务器可以签发一个权限受限的Token(例如,一个只读Token),即使泄露,攻击者也无法执行写操作。
3. 典型的登录认证流程
登录: 用户提交用户名和密码到一个登录接口(如 /login)。
获取令牌 (Token): 服务器验证用户名和密码成功后,生成一个加密的、有时效性的字符串,这个字符串就是令牌(Token)。服务器会将这个Token返回给客户端。
客户端存储令牌: 客户端(浏览器或App)收到Token后,会将其安全地存储起来(比如在浏览器的 localStorage 或 sessionStorage 中)。
后续请求携带令牌: 从此以后,客户端向服务器发起的每一个需要认证的请求,都必须在 Authorization 请求头中附带上这个Token。通常,Token前面还会加上一个Bearer的前缀,表示“持有者认证”。
服务器验证令牌: 服务器收到请求后,会先检查 Authorization 请求头。如果存在,它会取出Token,对其进行解密和验证(检查签名是否正确、是否过期等)。验证通过后,服务器就知道了“哦,原来是张三发来的请求”,然后才继续处理业务逻辑。
4. Token的生命周期
“生成”阶段: 每一次独立的登录(Login)行为,都会触发服务器生成一个全新的、不一样的Token。
“使用”阶段: 在某一次登录成功后,直到这个Token过期或用户手动登出之前,客户端在发起每一次业务请求(Request)时,都会重复使用这同一个Token。
5.JWT的“三段式”结构
JWT Token而是由 三个部分 通过 . 连接而成的:
Header.Payload.Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
第一部分:Header (头部) eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
内容: 描述这个JWT元信息的一个JSON对象,经过Base64Url编码而成。
解码后: {"alg": "HS256", "typ": "JWT"}
含义:
alg: "HS256" (HMAC using SHA-256),声明了签名部分(Signature)使用的加密算法。
typ: "JWT",声明了这是一个JWT。
第二部分:Payload (载荷) eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
Payload 的设计初衷是用来存放与“用户身份和权限”相关的、相对稳定的信息,这些信息在用户的一次登录会话中是不会改变的。
内容: 包含了我们想传递的实际业务数据的一个JSON对象,也经过Base64Url编码。
解码后: {"sub": "1234567890", "name": "John Doe", "iat": 1516239022}
含义 (这里是一些标准字段):
sub: Subject (主题),通常存放用户的唯一ID。—— 这就是“判断这个Token是谁的”关键!
name: 用户名。
iat: Issued At (签发时间),是一个时间戳。
exp: Expiration Time (过期时间),是一个时间戳。—— 这就是“检查是否过期”的关键!
注意: 任何人都可以对Header和Payload进行Base64Url解码,看到里面的内容。所以,绝对不能在Payload中存放敏感信息,如密码!
第三部分:Signature (签名) SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
内容: 这是一个加密后的字符串,是JWT安全性的核心。
生成过程: 它是通过以下公式,使用在**服务器端秘密保存的一个密钥(Secret Key)**生成的:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secretKey )含义: 这个签名就像是这份“声明文件”(Header + Payload)的“数字指纹”或“防伪印章”。
6. 服务器验证Token的全过程
假设服务器收到一个HTTP请求,请求头里有 Authorization: Bearer [一个JWT字符串]。在若依这类框架中,通常会有一个过滤器(Filter),比如 JwtAuthenticationTokenFilter,它会在请求到达你的Controller之前,自动执行以下验证流程:
第1步:检查请求头,取出Token
这步很简单,就是代码层面的字符串操作。
第2步:解密和验证(核心步骤)
这一步通常会由一个专门的JWT工具类(比如使用 jjwt 库)来完成。validateToken(jwtToken) 方法内部会做以下事情:
a. 拆分Token
将收到的 jwtToken 字符串,按 . 分割成三部分:headerPart, payloadPart, signaturePart。
b. 验证签名(Signature)—— 最关键的安全校验
服务器端有一个只有自己知道的、绝对保密的 secretKey。这个密钥在项目启动时就加载到内存中了。
服务器会完全忽略收到的 signaturePart。
它会用收到的 headerPart 和 payloadPart,以及自己保存在内存中的 secretKey,重新计算一次签名。
newSignature = HMACSHA256(headerPart + "." + payloadPart, mySecretKey)然后,它将 newSignature 与收到的 signaturePart 进行逐位比较。
如果两者完全一致: 这证明了这个Token确实是由我(或拥有相同密钥的其他可信服务)签发的,并且 Header 和 Payload 在传输过程中没有被任何人篡改过。因为只有持有 secretKey 才能生成出正确的签名。—— 验证通过!
如果两者不一致: 这说明Token要么是伪造的,要么内容被篡改了。—— 验证失败,立即拒绝请求!
c. 验证载荷(Payload)—— 业务规则校验
在签名验证通过后,服务器才会信任 Payload 里的内容。
它会对 payloadPart 进行Base64Url解码,得到一个JSON对象。
然后,它会检查Payload中的标准声明(Claims):
检查过期时间 (exp): if (currentTime > payload.getExp()) { ... }
获取exp字段的值(一个时间戳)。
与当前服务器时间进行比较。
如果当前时间已经超过了exp时间,说明Token已过期。—— 验证失败,拒绝请求!
还可以检查其他声明,比如nbf (Not Before,生效时间)等。
第3步:判断这个Token是谁的,并建立会话上下文
在签名和有效期都验证通过后,现在服务器可以完全信任这个Token了。
a. 提取用户信息
服务器会从已经解码的 Payload 中,提取出用户的唯一标识,通常是 sub (Subject) 字段。
String userId = payload.get("sub");它还可能提取出角色、权限等其他信息。
List<String> roles = payload.get("roles");
b. 建立安全上下文 (Security Context)
现在服务器知道了:“这个合法请求是用户 userId 发来的,他拥有 roles 这些角色。”
框架(如Spring Security)会创建一个认证对象(Authentication),把这些用户信息封装进去。
然后,将这个认证对象存入一个**与当前线程绑定的“安全上下文”**中(SecurityContextHolder)。
c. 放行请求
过滤器的工作到此完成,它会将请求放行,继续传递给后续的过滤器,最终到达Controller方法。当请求最终到达Controller时,不再需要关心Token了。只需要从那个“安全上下文”中获取用户信息即可。