jwt与token+redis,哪种方案更好用?

企业应用开发与运维数据库

大家好,我是苏三,又跟大家见面了。

前言

今天我们来聊聊一个非常经典的话题:JWT和Token+Redis两种认证方案,到底哪种更好用?

有些小伙伴在工作中可能会纠结于选择哪种方案,今天我就从底层原理到实际应用,给大家做一个全面的剖析。

希望对你会有所帮助。

最近建了一些工作内推群,各大城市都有,欢迎各位HR和找工作的小伙伴进群交流,群里目前已经收集了不少的工作内推岗位。

扫码加苏三的微信:li_su223,备注:所在城市,即可进群。

picture.image

一、认证与授权

在深入讨论之前,我们先明确两个基本概念:

  1. 认证(Authentication) :你是谁?验证用户身份的过程
  2. 授权(Authorization) :你能做什么?验证用户权限的过程

无论是JWT还是Token+Redis,都是用来解决这两个问题的技术方案。

picture.image

二、JWT方案

2.1 JWT是什么?

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为JSON对象。JWT由三部分组成:

  
header.payload.signature  

  • Header :包含令牌类型和签名算法
  • Payload :包含声明(用户信息、过期时间等)
  • Signature :用于验证消息在传输过程中没有被篡改

2.2 JWT的工作流程

让我们通过一个完整的登录流程来理解JWT的工作原理:

picture.image

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的优点和缺点

优点:

  1. 无状态 :服务端不需要存储会话信息
  2. 跨域友好 :适合分布式系统和微服务架构
  3. 自包含 :令牌中包含所有必要信息
  4. 扩展性好 :可以轻松添加自定义声明

缺点:

  1. 无法主动失效 :一旦签发,在到期前一直有效
  2. 令牌大小 :包含的信息越多,令牌越大
  3. 安全性依赖 :完全依赖签名,密钥泄露后果严重

三、Token+Redis方案

3.1 Token+Redis是什么?

Token+Redis方案使用随机生成的令牌作为用户会话的标识,将会话数据存储在Redis中。

这种方案本质上是有状态的,服务端需要维护会话状态。

3.2 Token+Redis的工作流程

picture.image

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的优点和缺点

优点:

  1. 主动控制 :可以随时使特定令牌失效
  2. 信息量小 :令牌只是一个标识符,不会太大
  3. 灵活性高 :可以存储复杂的会话状态
  4. 安全性好 :令牌泄露可以立即撤销

缺点:

  1. 有状态 :服务端需要存储会话信息
  2. Redis依赖 :Redis成为单点故障源
  3. 网络开销 :每次请求都需要查询Redis
  4. 扩展性挑战 :需要处理Redis集群和数据同步

四、深度对比分析

4.1 性能对比

从性能角度,两种方案有显著差异:

| 方面 | JWT | Token+Redis | | --- | --- | --- | | 认证速度 | 快(本地验证) | 慢(需要Redis查询) | | 网络开销 | 小 | 大(每次请求都需要访问Redis) | | 服务端压力 | 小 | 大(Redis需要处理大量查询) | | 扩展成本 | 低 | 高(需要维护Redis集群) |

4.2 安全性对比

安全性是认证方案的核心考量因素:

JWT安全性考虑:

  1. 密钥管理 :签名密钥需要严格保护,定期轮换
  2. 令牌泄露 :无法主动失效,只能等待自动过期
  3. 算法选择 :需要选择安全的签名算法(如HS256、RS256)

Token+Redis安全性考虑:

  1. Redis安全 :需要保证Redis实例的安全性
  2. 令牌随机性 :令牌必须足够随机,防止猜测
  3. 传输安全 :需要HTTPS防止令牌被窃听

4.3 适用场景对比

不同的业务场景适合不同的方案:

适合JWT的场景:

  1. 分布式系统和微服务架构
  2. 需要跨域认证的单页应用(SPA)
  3. 无状态API服务
  4. 移动应用后端

适合Token+Redis的场景:

  1. 需要精细控制会话的企业应用
  2. 需要实时吊销权限的系统
  3. 会话信息复杂的传统Web应用
  4. 对安全性要求极高的金融系统

五、混合方案

有些小伙伴在工作中可能会想:能不能结合两种方案的优点?

答案是肯定的!

下面介绍一种混合方案:

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 混合方案工作流程

picture.image

六、实际项目选型建议

根据我多年的工作经验,给大家一些实用的选型建议:

6.1 选择JWT当以下情况成立时:

  1. 系统是分布式架构,需要无状态认证。
  2. 需要支持跨域认证(如多个前端应用共享后端)。
  3. API消费者主要是第三方应用或移动端。
  4. 团队有能力管理好密钥和令牌安全。

6.2 选择Token+Redis当以下情况成立时:

  1. 系统是单体或少量服务的架构。
  2. 需要精细的会话控制和实时权限管理。
  3. 有专业的运维团队维护Redis集群。
  4. 对安全性要求极高,需要即时吊销能力。

6.3 选择混合方案当以下情况成立时:

  1. 既需要JWT的无状态特性,又需要主动注销能力。
  2. 系统对用户体验要求高(避免频繁登录)。
  3. 有能力处理稍复杂的令牌管理逻辑。
  4. 需要平衡安全性和便利性。

总结

通过上面的详细分析,JWT和token+redis这两种方案,各有优缺点和适用场景。

我们可以得出以下结论:

  1. 没有绝对的最好方案 :只有最适合具体业务场景的方案。
  2. JWT优势在无状态和扩展性 :适合分布式系统和API优先的架构。
  3. Token+Redis优势在控制和灵活性 :适合需要精细会话管理的企业应用。
  4. 混合方案取长补短 :适合大多数现代Web应用。

有些小伙伴在工作中可能会盲目追求技术的新颖性,或者过度设计认证方案。

我的建议是:从实际业务需求出发,选择最简单可靠的方案

对于大多数应用来说,我推荐采用混合方案:

  • 使用短期JWT保证API的无状态特性。
  • 使用刷新令牌机制优化用户体验。
  • 使用Redis黑名单提供主动注销能力。
  • 使用HTTPS和严格的密钥管理保证安全性。

无论选择哪种方案,都要记住:安全不是一个功能,而是一个过程。

希望这篇文章能帮助大家在技术选型时做出更明智的决策。

最后欢迎加入苏三的星球,你将获得:SaaS点餐系统(DDD+多租户)、100万QPS短链系统(超过并发)、复杂的商城微服务系统(分布式)、苏三AI项目、刷题吧小程序、秒杀系统、商城系统、秒杀系统、代码生成工具等8个项目的源代码、开发教程和技术答疑。

系统设计、性能优化、技术选型、底层原理、Spring源码解读、工作经验分享、痛点问题、面试八股文等多个优质专栏。

还有1V1免费修改简历、技术答疑、职业规划、送书活动、技术交流。

扫描下方二维码,可以优惠30元:

picture.image

只有20张优惠券, 数量有限,先到先得。

目前星球已经更新了5800+篇优质内容,还在持续爆肝中.....

星球已经被官方推荐了3次,收到了小伙伴们的一致好评。戳我加入学习,已有2000+小伙伴加入学习。

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
DevOps 在字节移动研发中的探索和实践
在日益复杂的APP工程架构下,如何保证APP能高效开发,保障团队效能和工程质量?本次将结合字节内部应用的事件案例,介绍DevOps团队对移动研发效能建设的探索和思考。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论