做到登录认证这一块的时候,我正好在重构手上的一个前后端分离项目。老项目用的还是Session那一套,用户在A服务登录了,请求打到B服务就查不到登录态。改了两周,踩了无数坑,我才真正理解课程里把JWT令牌和Filter放在一起讲的原因:这俩东西看着是一个生成令牌、一个拦截校验,合在一起才是完整的登录认证链路。这篇文章就是把我在实践中的理解、落地代码和踩坑记录整理出来,给同样在学Java Web后端、正在做登录认证的朋友做个参考。
如果你还没搞清楚Session和JWT的差别,或者写完Filter发现注入Redis是null、前端一直报跨域错误,这篇文章应该能帮上忙。我会先从“为什么不用Session”讲起,再拆JWT的生成与解析,然后是Filter拦截里最容易出问题的几个细节,最后补充一些生产环境才会遇到的问题,比如token刷新、主动失效和安全隐患。
1. 从Session到JWT:这一步到底绕开了哪些坑
1.1 面试常背的Session方案,在前后端分离下为什么不够用
传统的Session方案,大家应该都很熟:用户登录成功后,服务器在内存或者Redis里存一份用户信息,然后生成一个Session ID,通过Set-Cookie: JSESSIONID=xxx返回给浏览器。浏览器之后的每次请求都会自动带上这个Cookie,服务器拿着Session ID去查对应的用户信息。
这套机制在单体应用、前后端不分离的年代没有任何问题,但在现在的开发模式下,痛点一个比一个明显。
第一,App端没有Cookie机制。小程序、安卓、iOS这些客户端压根不是浏览器,Cookie的自动携带行为对它们并不友好。你让App开发者手动维护Session ID,还要处理过期刷新,纯属给自己找麻烦。
第二,分布式环境下Session不共享。一个请求先打到服务器A,登录态存在A的内存里,下一个请求被负载均衡转发到服务器B,B那边根本没有这个Session。用Spring Session把Session集中存到Redis能解决,但这是个额外的架构组件,也带来序列化、网络开销等一系列新问题。
第三,跨域场景下Cookie的处理非常棘手。前后端分离之后,前端页面在localhost:8080,后端接口在localhost:9090,浏览器发起跨域请求,Cookie默认不会携带,需要配置Access-Control-Allow-Credentials和withCredentials。一旦涉及SameSite策略,很多浏览器默认拦截跨站Cookie,排查起来头大。
所以你会发现,凡是做前后端分离的项目,但凡多部署几个节点,Session方案的维护成本就会指数级上升。JWT就是在这样的背景下成为登录认证的主流方案的。
1.2 无状态到底是省事还是添麻烦
JWT的思想其实非常直白:服务端不存登录态,把用户信息经过签名后生成一串令牌,返回给前端。前端后续请求把令牌放到请求头里,服务端只需要验证令牌的签名是否合法、是否过期,就知道请求来自谁。
你可以把它想象成游乐场的盖章通行证。游客买票进场时,工作人员在手背上盖一个荧光章,之后你去玩过山车、鬼屋,工作人员只需要扫一下章,就知道你是买过票的,不需要挨个翻台账核对。
这个方案天然适合分布式,因为校验逻辑只需要密钥,不需要共享Session存储。每个服务都能独立完成校验,微服务网关做统一鉴权也变得很自然。
但无状态不等于没有成本。服务端不存登录态,意味着服务端没法主动把某个token作废。用户改了密码,老token在有效期内照样能用;管理员封了某个账号,封禁操作并不能立刻让已经发出去的token失效。这些“后悔药”问题,后面我会单独讲怎么处理。
1.3 别把JWT当银弹:四个短板要提前知道
JWT虽然好用,但它不是万能的。在你决定把登录认证全部改成JWT之前,下面这几个短板最好先有数。
第一,Token没法主动失效。 这是无状态方案最大的痛点。用户点退出登录,前端把token删了,但token本身还是有效的,被别人截获了照样能访问接口。后面要讲的黑名单和版本号方案,本质上都是在JWT之外“补状态”。
第二,Payload只是Base64Url编码,不是加密。 任何拿到token的人都能直接解码看内容。你把手机号、身份证号写进payload,等于把这些敏感信息明文暴露给了客户端。Payload里只放非敏感的必要信息,比如用户ID、昵称、角色,密码之类的绝对不要放。
第三,Token体积偏大。 Header加Payload加签名,一个简单token大概300到500字节。放HTTP Header里还好,如果请求头里还带着其他业务数据,有些网关对请求头大小有上限,超过4KB或8KB就会出问题。权限多、角色多的时候,payload很容易膨胀。
第四,密钥管理不容小觑。 HS256算法是对称密钥,签发和校验用的同一个Secret。一旦Secret泄露,任何人都能伪造合法token,直接登入系统。生产环境要把密钥放到配置中心或环境变量里,代码仓库里不能出现明文密钥。
| 维度 | Session方案 | JWT方案 |
|---|---|---|
| 存储位置 | 服务端内存/Redis | 客户端持有 |
| 扩展性 | 分布式需集中存储 | 天然支持分布式 |
| 主动失效 | 删除Session即可 | 需要黑名单/版本号 |
| 跨端支持 | App端不友好 | 任意客户端 |
| 安全性 | CSRF风险较高 | Header携带可降低CSRF风险 |
| 性能开销 | 每次请求查存储 | 验签计算 |
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. JWT令牌长什么样:从三段结构到手写解析
2.1 Header、Payload、Signature分别存什么
JWT由三个部分组成,中间用英文句点分隔。一个真实token长这样:
code复制eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMDAxIiwidXNlcm5hbWUiOiJ6aGFuZ3NhbiIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcxMDAwMDAwMH0.8Wfq3l9P8rKx5L9HcF4y6dDgF2yVbQ
第一部分是Header,声明签名算法和令牌类型。Base64Url解码后大概是这样的:
json复制{
"alg": "HS256",
"typ": "JWT"
}
第二部分是Payload,承载业务数据。它包含标准字段(Registered Claims)和自定义字段(Custom Claims)。标准字段有iss(签发者)、sub(面向的用户)、exp(过期时间)、iat(签发时间)、jti(令牌唯一ID)等;自定义字段就随便定义了,username、role之类的都可以放。
第三部分是Signature,签名。以HS256为例,签名的计算方式是:
code复制HMACSHA256(base64url(Header) + "." + base64url(Payload), secret)
把前两部分的字符串拼接起来,用密钥做HMAC-SHA256运算,得到的结果再Base64Url编码,就是签名。签名的作用是防止token被篡改,Payload在Base64Url编码下是明文可读的,用户可以直接解码修改用户名,但是改完之后没有密钥,无法算出新的合法签名。服务端校验签名不匹配,直接拒绝。
2.2 签名算法怎么选:HS256还是RS256
很多人写JWT的时候不关注签名算法,看到一个教程用HS256就照着敲。实际项目里HS256和RS256的区别值得认真选一下。
HS256是对称签名,服务端用同一个Secret既签发又校验。好处是简单,只要一个密钥;坏处是密钥必须保密,而且所有需要校验token的服务都必须共享这个密钥。微服务场景下,密钥分发范围越大,泄露风险越高。
RS256是非对称签名,用私钥签发、公钥校验。认证中心持有私钥,其他服务只拿到公钥,各自独立校验token,不需要共享密钥。某个服务被攻破,攻击者拿到公钥也伪造不了token。代价是计算开销更大、密钥对管理更复杂。
| 对比项 | HS256 | RS256 |
|---|---|---|
| 密钥类型 | 对称密钥 | 私钥签名 |
