虾皮面试:深入探讨JWT身份认证的优缺点及常见解决方案,助力开发者做出明智选择
JWT 的优势
与传统的 Session 认证方式相比,使用 JWT 进行身份认证主要有以下四个显著优势。
无状态
JWT 本身就包含了身份验证所需的所有信息,因此我们的服务器不必储存 Session 信息。这显著提升了系统的可用性和可扩展性,同时减轻了服务端的压力。
然而,JWT 的无状态特征也带来了一个显著的缺点:不可控性!例如,如果我们想在 JWT 有效期内撤销某个 JWT 或者更改它的权限,变更通常不会立即生效,往往需要等到有效期结束后才能生效。此外,当用户登出时,JWT 仍然处于有效状态。除非在后端增加额外的处理逻辑,例如将失效的 JWT 存储起来,以便在后端验证 JWT 的有效性,我们将在后面的内容中详细探讨这个问题。
有效防止 CSRF 攻击
CSRF(跨站请求伪造) 是一种网络攻击形式,尽管比 SQL 注入、XSS 等攻击手法知名度低,但却是开发系统时需要重视的安全隐患。即使技术巨头 Google 的产品 Gmail 也曾在 2007 年爆出过 CSRF 漏洞,造成用户损失。
CSRF 攻击是如何发生的? 简而言之,就是黑客利用你的身份发起恶意请求(例如恶意转账)。比如,小壮登录某网上银行,看到一个帖子中有链接写着“科学理财,年盈利率过万”,好奇点开后却发现账户少了 10000 元。这是因为黑客在链接中藏了一个请求,利用小壮的身份向银行发起了转账请求。
CSRF 攻击依赖于 Cookie,通常使用 Session 认证时,Cookie 中的 SessionID
会被浏览器发送到服务器,只要发出请求,Cookie 就会被带上。因此,即使黑客无法获取你的 SessionID
,只需让你误点击攻击链接便可实施攻击。
然而,使用 JWT 进行身份验证时,登录后用户会将 JWT 存储在 localStorage 中。每次请求都会附带这个 JWT,整个过程与 Cookie 完全无关。因此,若用户点击了恶意链接并发起请求,非法请求将不会携带 JWT,因此被视为无效请求。
总结一句话:使用 JWT 进行身份验证不依赖于 Cookie,从而有效避免了 CSRF 攻击。
但这也增加了 XSS 攻击的风险。为了防范 XSS 攻击,可以选择将 JWT 存储在标记为 httpOnly
的 Cookie 中,然而,这样又需要自己提供 CSRF 保护。因此,在实际项目中,我们通常不采用这种方式。避免 XSS 攻击的常用方法是过滤掉请求中有风险的可疑字符串。在 Spring 项目中,我们一般通过创建 XSS 过滤器来实现。
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class XSSFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
XSSRequestWrapper wrappedRequest =
new XSSRequestWrapper((HttpServletRequest) request);
chain.doFilter(wrappedRequest, response);
}
// 其他方法
}
适合移动端应用
使用 Session 进行身份认证时,必须在服务器端保存一份信息,并依赖 Cookie(保存 SessionId
),这使得它不适合移动端应用。而使用 JWT 进行身份认证则不会有此问题,客户端只需将 JWT 存储即可使用,且 JWT 具备跨语言的优越性。
单点登录友好
在使用 Session 进行身份认证时,实现单点登录需要将用户的 Session 信息保存在一台设备上,同时还会遇到 Cookie 跨域问题。相比之下,使用 JWT 进行认证时,JWT 存储在客户端,不存在这类问题。
JWT 身份认证常见问题及解决方案
注销登录等场景下 JWT 仍然有效
相关场景包括但不限于:
- 退出登录;
- 修改密码;
- 服务端修改某个用户的权限或角色;
- 用户帐户被封禁或删除;
- 用户被服务端强制注销;
- 用户被踢下线;
- 等等。
这个问题在 Session 认证方式中不会出现,因为只需在服务器上删除对应的 Session 记录即可。然而,采用 JWT 认证方式时,JWT 一旦发出,如果后端不增加其他逻辑,它在失效之前依然有效。
如何解决这个问题呢?经过多方资料查阅,我简单总结了以下四种方案:
1. 将 JWT 存入内存数据库
可以使用 Redis 内存数据库存储 JWT,若需要让某个 JWT 失效,只需从 Redis 中删除该 JWT。然而,这样会导致每次使用 JWT 发送请求时都需先查询数据库中的 JWT,违背了 JWT 的无状态原则。
2. 黑名单机制
与上述方案类似,可以使用内存数据库(如 Redis)维护一个黑名单,任何想要失效的 JWT 都可以直接加入黑名单。每次请求时,需要判断该 JWT 是否存在于黑名单中。
尽管这两种方案违背了 JWT 的无状态原则,但在实际项目中我们通常会采用这两种方案。
3. 修改密钥 (Secret)
为每个用户创建一个专属密钥,若想让某个 JWT 失效,只需修改对应用户的密钥。然而,此方案相较于前两种引入内存数据库,带来的风险更大:
- 如果服务是分布式的,发出新的 JWT 时必须在多台机器中同步密钥,这样会使 JWT 认证与 Session 认证没有本质差别。
- 如果用户在两个浏览器中同时打开系统,或在手机端也打开系统,若他从一个地方退出登录,其他地方也需重新登录,这显然不符合用户习惯。
4. 缩短令牌的有效期限并频繁轮换
这种方式简单易行,但会导致用户需要频繁重新登录。
此外,对于修改密码后 JWT 继续有效的问题,我们可以采取一种较好的方式:使用用户密码的哈希值对 JWT 进行签名,这样若密码被更改,则任何先前的令牌将无法验证。
JWT 的续签问题
JWT 的有效期一般建议设置短一些,若 JWT 过期后该如何认证?如何实现动态刷新 JWT,以避免用户频繁重新登录?
在 Session 认证中,一般的做法是:假设 Session 有效期为 30 分钟,如果用户在 30 分钟内有访问,就将 Session 的有效期延长 30 分钟。
对于 JWT,我们应如何解决续签问题呢?以下是四种总结方案:
1. 类似于 Session 认证中的做法
这种方案适用于大部分场景。假设服务端给的 JWT 有效期设置为 30 分钟,服务端每次进行校验时,如果发现 JWT 的有效期即将到达,便重新生成 JWT 给予客户端。客户端每次请求时,检查新旧 JWT 是否一致,如不一致则更新本地 JWT。该做法的问题在于,仅在接近过期时请求才会更新 JWT,对客户端并不友好。
2. 每次请求都返回新 JWT
该方案思路简单,但开销较大,尤其在存储和维护 JWT 的情况下。
3. 将 JWT 有效期设置到半夜
该方案为折衷方案,保证大部分用户在白天可以正常使用,适合对安全性要求不高的系统。
4. 用户登录时返回两个 JWT
一个是 accessJWT,有效期为半小时;另一个是 refreshJWT,有效期为一天。客户端在登录后,将 accessJWT 和 refreshJWT 存储在本地,每次访问时发送 accessJWT。服务端校验 accessJWT 的有效性,若过期则发送 refreshJWT 进行校验;若有效,则生成新的 accessJWT。若无效,客户端需重新登录。
该方案的不足在于:
- 需客户端配合;
- 用户注销时需确保两个 JWT 同时失效;
- 在重新请求获取 JWT 的过程中,可能会出现短暂的 JWT 不可用情况(可通过在客户端设置定时器来提前请求新的 accessJWT)。
总结
JWT 其中一个重要的优势是其无状态特性,但在实际项目中合理使用 JWT 时,仍需保存 JWT 信息。
需要强调的是,JWT 并非解决所有问题的“银弹”,存在许多缺陷。我们在选择 JWT 还是 Session 方案时,需根据项目的具体需求进行权衡,切忌盲目推崇 JWT,而轻视其他身份认证方案。
此外,直接使用普通的 Token(随机生成,不包含具体信息)结合 Redis 进行身份认证也是可行的。Sa-Token 项目是一个比较完善的基于 JWT 的身份认证解决方案,支持自动续签、强制下线、账号封禁、同端互斥登录等功能,有兴趣的朋友可以深入了解。
参考资料
- JWT 超详细分析:https://learnku.com/articles/17883
- 如何在使用 JWT 时注销:https://medium.com/devgorilla/how-to-log-out-when-using-jwt-a8c7823e8a6
- 使用 JSON Web JWTs 进行 CSRF 保护:https://medium.com/@agungsantoso/csrf-protection-with-json-web-jwts-83e0f2fcbcc
- 使 JSON Web JWTs 无效:[https://stackoverflow.com/questions/21978658/invalidating-json-web-jwts](