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


👉 欢迎加入小哈的星球,你将获得: 专属的项目实战(多个项目) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新项目:《Spring AI 项目实战》正在更新中..., 基于 Spring AI + Spring Boot 3.x + JDK 21;

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17..., 点击查看项目介绍;演示地址:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2期已完结,演示链接:http://116.62.199.48/;

  • 专栏阅读地址:https://www.quanxiaoha.com/column

截止目前,累计输出 100w+ 字,讲解图 4013+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,解锁全部项目,已有3900+小伙伴加入

图片

前言

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

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

希望对你会有所帮助。

一、认证与授权

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

  1. 认证(Authentication):你是谁?验证用户身份的过程

  2. 授权(Authorization):你能做什么?验证用户权限的过程

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

二、JWT方案

2.1 JWT是什么?

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

优点:

  1. 无状态:服务端不需要存储会话信息

  2. 跨域友好:适合分布式系统和微服务架构

  3. 自包含:令牌中包含所有必要信息

  4. 扩展性好:可以轻松添加自定义声明

缺点:

  1. 无法主动失效:一旦签发,在到期前一直有效

  2. 令牌大小:包含的信息越多,令牌越大

  3. 安全性依赖:完全依赖签名,密钥泄露后果严重

三、Token+Redis方案

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

优点:

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

六、实际项目选型建议

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

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和严格的密钥管理保证安全性。

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

👉 欢迎加入小哈的星球,你将获得: 专属的项目实战(多个项目) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新项目:《Spring AI 项目实战》正在更新中..., 基于 Spring AI + Spring Boot 3.x + JDK 21;

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17..., 点击查看项目介绍;演示地址:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2期已完结,演示链接:http://116.62.199.48/;

  • 专栏阅读地址:https://www.quanxiaoha.com/column

截止目前,累计输出 100w+ 字,讲解图 4013+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,解锁全部项目,已有3900+小伙伴加入

图片

图片

图片

1. 我的私密学习小圈子,从0到1手撸企业实战项目~
2. 字节二面:Redis 内存淘汰策略 LRU 和传统 LRU 差异,我答懵了
3. 不要轻易把 Kibana 权限随意给别人,好吗?
4. 13 秒插入 30 万条数据,这才是批量插入正确的姿势!
最近面试BAT,整理一份面试资料《Java面试BATJ通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。
PS:因公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。
点“在看”支持小哈呀,谢谢啦
实现一个基于 **Spring Security + JWT + Redis + Token + Shiro 权限控制(到按钮级别)+ 日志处理** 的系统,是一个典型的 Java Web 安全架构。下面我将从整体架构设计、模块划分、核心代码实现几个方面来说明。 --- ## ✅ 架构概览 | 模块 | 技术 | 作用 | |------|------|------| | 认证 | JWT | 用户登录后生成 token,携带用户信息 | | 权限控制 | Apache Shiro | 控制接口访问权限,支持到按钮级别 | | Token 存储 | Redis | 存储 token,支持 token 注销和过期 | | 安全框架 | Spring Security | 控制登录流程、异常处理、CORS、CSRF 等 | | 日志 | AOP + MDC | 记录操作日志,包含用户、操作、IP 等信息 | --- ## ✅ 核心实现步骤 ### 1. 引入依赖(Maven) ```xml <dependencies> <!-- Spring Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- Shiro 权限 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>1.13.0</version> </dependency> <!-- JWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> </dependency> <!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- AOP 日志 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> </dependencies> ``` --- ### 2. JWT 工具类 ```java import io.jsonwebtoken.*; import org.springframework.stereotype.Component; import java.util.Date; @Component public class JwtUtils { private String secret = "your-secret-key"; private long expiration = 86400000; // 24h public String generateToken(String username) { return Jwts.builder() .setSubject(username) .setExpiration(new Date(System.currentTimeMillis() + expiration)) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public String parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody() .getSubject(); } } ``` --- ### 3. 登录接口 + Redis 存储 token ```java @RestController @RequestMapping("/auth") public class AuthController { @Autowired private JwtUtils jwtUtils; @Autowired private RedisTemplate<String, String> redisTemplate; @PostMapping("/login") public ResponseEntity<?> login(@RequestBody LoginRequest request) { String username = request.getUsername(); String password = request.getPassword(); // TODO: 验证用户名密码 if (!"admin".equals(username) || !"123456".equals(password)) { throw new RuntimeException("用户名或密码错误"); } String token = jwtUtils.generateToken(username); redisTemplate.opsForValue().set("token:" + token, username, 24, TimeUnit.HOURS); return ResponseEntity.ok(token); } @PostMapping("/logout") public ResponseEntity<?> logout(@RequestHeader("Authorization") String token) { if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); redisTemplate.delete("token:" + token); } return ResponseEntity.ok("登出成功"); } } ``` --- ### 4. Spring Security 配置 ```java @Configuration @EnableWebSecurity public class SecurityConfig { @Autowired private JwtUtils jwtUtils; @Autowired private RedisTemplate<String, String> redisTemplate; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .addFilterBefore(new JwtAuthenticationFilter(jwtUtils, redisTemplate), UsernamePasswordAuthenticationFilter.class) .authorizeRequests() .antMatchers("/auth/login", "/auth/logout").permitAll() .anyRequest().authenticated(); return http.build(); } } ``` --- ### 5. JWT 过滤器 ```java public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtils jwtUtils; private final RedisTemplate<String, String> redisTemplate; public JwtAuthenticationFilter(JwtUtils jwtUtils, RedisTemplate<String, String> redisTemplate) { this.jwtUtils = jwtUtils; this.redisTemplate = redisTemplate; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); String username = jwtUtils.parseToken(token); String stored = redisTemplate.opsForValue().get("token:" + token); if (stored != null && stored.equals(username)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( username, null, new ArrayList<>()); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request, response); } } ``` --- ### 6. Shiro 权限控制(按钮级别) ```java @Configuration public class ShiroConfig { @Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/api/user/**", "authc,perms[user:view]"); filterMap.put("/api/admin/**", "authc,perms[admin:manage]"); return new DefaultShiroFilterChainDefinition(filterMap); } @Bean public Realm realm() { return new AuthorizingRealm() { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = (String) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); if ("admin".equals(username)) { info.addStringPermission("admin:manage"); } info.addStringPermission("user:view"); return info; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal(); return new SimpleAuthenticationInfo(username, "123456", getName()); } }; } } ``` --- ### 7. 日志记录(AOP + MDC) ```java @Aspect @Component public class LogAspect { @Autowired private HttpServletRequest request; @Pointcut("execution(* com.example.controller..*.*(..))") public void log() {} @Before("log()") public void before(JoinPoint joinPoint) { MDC.put("userId", SecurityContextHolder.getContext().getAuthentication().getName()); MDC.put("ip", request.getRemoteAddr()); } @AfterReturning(returning = "result", pointcut = "log()") public void after(Object result) { System.out.println("操作日志:" + MDC.get("userId") + " | IP: " + MDC.get("ip") + " | 结果: " + result); } } ``` --- ## ✅ 总结 以上代码实现了一个完整的 Spring Security + JWT + Redis + Shiro + 日志处理的权限系统,支持: - 登录认证(JWT) - Token 管理(Redis) - 接口权限控制(Shiro,支持按钮级别) - 操作日志记录(AOP + MDC) --- ##
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值