jwt(json web token)与token+redis是两种常见的用户认证方案,它们在设计原理、性能和安全特性上存在显著差异。jwt为无状态认证,适合分布式系统但无法主动失效;token+redis依赖redis存储会话,支持实时吊销但运维复杂,两者各有优劣,混合方案结合短期jwt与redis黑名单实现安全与便利平衡,推荐根据业务场景灵活选择。
一、认证与授权
在深入讨论之前,我们先明确两个基本概念:
- 认证(authentication):你是谁?验证用户身份的过程
- 授权(authorization):你能做什么?验证用户权限的过程
无论是jwt还是token+redis,都是用来解决这两个问题的技术方案。
二、jwt方案
2.1 jwt是什么?
jwt是一种无状态的身份验证机制,通过将用户信息编码在令牌中实现验证。其核心优势在于分布式友好和性能高,适合微服务架构和跨域sso场景。但由于无法主动失效token,存在信息泄露风险,且权限更新需要重新生成token。
jwt(json web token)是一种开放标准(rfc 7519),用于在各方之间安全地传输信息作为json对象。jwt由三部分组成:
header.payload.signature
- header:包含令牌类型和签名算法
- payload:包含声明(用户信息、过期时间等)
- signature:用于验证消息在传输过程中没有被篡改
2.2 jwt的工作流程
让我们通过一个完整的登录流程来理解jwt的工作原理:
2.3 jwt的java实现示例
下面是一个简单的jwt工具类实现:
import io.jsonwebtoken.*; import io.jsonwebtoken.security.keys; import java.security.key; import java.util.date; public class jwtutil { // 密钥,实际项目中应从配置中读取 private static final key key = keys.secretkeyfor(signaturealgorithm.hs256); // 过期时间:2小时 private static final long expiration_time = 2 * 60 * 60 * 1000; /** * 生成jwt */ public static string generatetoken(string userid, string username, list<string> roles) { return jwts.builder() .setsubject(userid) .claim("username", username) .claim("roles", roles) .setissuedat(new date()) .setexpiration(new date(system.currenttimemillis() + expiration_time)) .signwith(key) .compact(); } /** * 验证并解析jwt */ public static claims parsetoken(string token) { try { return jwts.parserbuilder() .setsigningkey(key) .build() .parseclaimsjws(token) .getbody(); } catch (expiredjwtexception e) { thrownew runtimeexception("token已过期", e); } catch (jwtexception e) { thrownew runtimeexception("token无效", e); } } /** * 刷新token */ public static string refreshtoken(string token) { claims claims = parsetoken(token); return generatetoken(claims.getsubject(), claims.get("username", string.class), claims.get("roles", list.class)); } }
2.4 jwt的优点和缺点
优点:
- 无状态:服务端不需要存储会话信息
- 跨域友好:适合分布式系统和微服务架构
- 自包含:令牌中包含所有必要信息
- 扩展性好:可以轻松添加自定义声明
缺点:
- 无法主动失效:一旦签发,在到期前一直有效
- 令牌大小:包含的信息越多,令牌越大
- 安全性依赖:完全依赖签名,密钥泄露后果严重
三、token+redis方案
3.1 token+redis是什么?
该方案通过redis集中存储会话信息,具有完全控制会话和动态权限管理的优势,可实时吊销token或绑定设备属性增强安全性。但性能依赖redis集群,运维复杂度较高。实现时通常将生成的token存入redis并设置过期时间,结合拦截器进行双重验证(签名校验和redis查询)。
token+redis方案使用随机生成的令牌作为用户会话的标识,将会话数据存储在redis中。
这种方案本质上是有状态的,服务端需要维护会话状态。
3.2 token+redis的工作流程
3.3 token+redis的java实现示例
import org.springframework.data.redis.core.redistemplate; import org.springframework.stereotype.component; import java.util.uuid; import java.util.concurrent.timeunit; @component public class redissessionmanager { private final redistemplate<string, object> redistemplate; // 会话过期时间:2小时 private static final long session_expire_time = 2 * 60 * 60; public redissessionmanager(redistemplate<string, object> redistemplate) { this.redistemplate = redistemplate; } /** * 创建会话 */ public string createsession(user user) { string token = generatetoken(); sessioninfo sessioninfo = new sessioninfo(user.getid(), user.getusername(), user.getroles()); redistemplate.opsforvalue().set( getrediskey(token), sessioninfo, session_expire_time, timeunit.seconds ); return token; } /** * 获取会话信息 */ public sessioninfo getsession(string token) { return (sessioninfo) redistemplate.opsforvalue().get(getrediskey(token)); } /** * 删除会话 */ public void deletesession(string token) { redistemplate.delete(getrediskey(token)); } /** * 刷新会话有效期 */ public void refreshsession(string token) { redistemplate.expire(getrediskey(token), session_expire_time, timeunit.seconds); } /** * 生成随机token */ private string generatetoken() { return uuid.randomuuid().tostring().replace("-", ""); } /** * 获取redis key */ private string getrediskey(string token) { return "session:" + token; } /** * 会话信息类 */ @data @allargsconstructor public static class sessioninfo { private string userid; private string username; private list<string> roles; private long createtime; public sessioninfo(string userid, string username, list<string> roles) { this.userid = userid; this.username = username; this.roles = roles; this.createtime = system.currenttimemillis(); } } }
3.4 token+redis的优点和缺点
优点:
- 主动控制:可以随时使特定令牌失效
- 信息量小:令牌只是一个标识符,不会太大
- 灵活性高:可以存储复杂的会话状态
- 安全性好:令牌泄露可以立即撤销
缺点:
- 有状态:服务端需要存储会话信息
- redis依赖:redis成为单点故障源
- 网络开销:每次请求都需要查询redis
- 扩展性挑战:需要处理redis集群和数据同步
四、深度对比分析
4.1 性能对比
从性能角度,两种方案有显著差异:
方面 | jwt | token+redis |
认证速度 | 快(本地验证) | 慢(需要redis查询) |
网络开销 | 小 | 大(每次请求都需要访问redis) |
服务端压力 | 小 | 大(redis需要处理大量查询) |
扩展成本 | 低 | 高(需要维护redis集群) |
4.2 安全性对比
安全性是认证方案的核心考量因素:
jwt安全性考虑:
- 密钥管理:签名密钥需要严格保护,定期轮换
- 令牌泄露:无法主动失效,只能等待自动过期
- 算法选择:需要选择安全的签名算法(如hs256、rs256)
token+redis安全性考虑:
- redis安全:需要保证redis实例的安全性
- 令牌随机性:令牌必须足够随机,防止猜测
- 传输安全:需要https防止令牌被窃听
4.3 适用场景对比
不同的业务场景适合不同的方案:
适合jwt的场景:
- 分布式系统和微服务架构
- 需要跨域认证的单页应用(spa)
- 无状态api服务
- 移动应用后端
适合token+redis的场景:
- 需要精细控制会话的企业应用
- 需要实时吊销权限的系统
- 会话信息复杂的传统web应用
- 对安全性要求极高的金融系统
五、混合方案
有些小伙伴在工作中可能会想:能不能结合两种方案的优点?
答案是肯定的!
下面介绍一种混合方案:
5.1 短期jwt + redis黑名单
这种方案使用短期有效的jwt,配合redis黑名单实现主动注销:
public class hybridauthmanager { private final jwtutil jwtutil; private final redistemplate<string, object> redistemplate; // jwt短期有效期:15分钟 private static final long short_expiration = 15 * 60 * 1000; // 刷新令牌有效期:7天 private static final long refresh_expiration = 7 * 24 * 60 * 60 * 1000; /** * 生成访问令牌和刷新令牌 */ public authresponse generatetokenpair(user user) { // 生成短期访问令牌 string accesstoken = jwtutil.generatetoken( user.getid(), user.getusername(), user.getroles(), short_expiration); // 生成长期刷新令牌 string refreshtoken = uuid.randomuuid().tostring(); // 存储刷新令牌到redis storerefreshtoken(refreshtoken, user.getid()); returnnew authresponse(accesstoken, refreshtoken); } /** * 刷新访问令牌 */ public string refreshaccesstoken(string refreshtoken) { // 验证刷新令牌有效性 string userid = validaterefreshtoken(refreshtoken); if (userid == null) { thrownew runtimeexception("刷新令牌无效"); } // 获取用户信息 user user = userservice.getuserbyid(userid); // 生成新的访问令牌 return jwtutil.generatetoken( user.getid(), user.getusername(), user.getroles(), short_expiration); } /** * 注销令牌 */ public void logout(string accesstoken, string refreshtoken) { // 将访问令牌加入黑名单(剩余有效期内) claims claims = jwtutil.parsetoken(accesstoken); long expiration = claims.getexpiration().gettime() - system.currenttimemillis(); if (expiration > 0) { redistemplate.opsforvalue().set( "blacklist:" + accesstoken, "logout", expiration, timeunit.milliseconds ); } // 删除刷新令牌 if (refreshtoken != null) { redistemplate.delete("refresh_token:" + refreshtoken); } } /** * 验证令牌是否在黑名单中 */ public boolean istokenblacklisted(string token) { return redistemplate.haskey("blacklist:" + token); } }
5.2 混合方案工作流程
六、实际项目选型建议
根据我多年的工作经验,给大家一些实用的选型建议:
6.1 选择jwt当以下情况成立时:
- 系统是分布式架构,需要无状态认证。
- 需要支持跨域认证(如多个前端应用共享后端)。
- api消费者主要是第三方应用或移动端。
- 团队有能力管理好密钥和令牌安全。
6.2 选择token+redis当以下情况成立时:
- 系统是单体或少量服务的架构。
- 需要精细的会话控制和实时权限管理。
- 有专业的运维团队维护redis集群。
- 对安全性要求极高,需要即时吊销能力。
6.3 选择混合方案当以下情况成立时:
- 既需要jwt的无状态特性,又需要主动注销能力。
- 系统对用户体验要求高(避免频繁登录)。
- 有能力处理稍复杂的令牌管理逻辑。
- 需要平衡安全性和便利性。
总结
通过上面的详细分析,jwt和token+redis这两种方案,各有优缺点和适用场景。
我们可以得出以下结论:
- 没有绝对的最好方案:只有最适合具体业务场景的方案。
- jwt优势在无状态和扩展性:适合分布式系统和api优先的架构。
- token+redis优势在控制和灵活性:适合需要精细会话管理的企业应用。
- 混合方案取长补短:适合大多数现代web应用。
有些小伙伴在工作中可能会盲目追求技术的新颖性,或者过度设计认证方案。
我的建议是:从实际业务需求出发,选择最简单可靠的方案。
对于大多数应用来说,我推荐采用混合方案:
- 使用短期jwt保证api的无状态特性。
- 使用刷新令牌机制优化用户体验。
- 使用redis黑名单提供主动注销能力。
- 使用https和严格的密钥管理保证安全性。
无论选择哪种方案,都要记住:安全不是一个功能,而是一个过程。
到此这篇关于java实现认证与授权的jwt与token+redis,哪种方案更好用?的文章就介绍到这了,更多相关java实现认证与授权的jwt与token+redis内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论