Spring Security整合JWT与Redis实现权限认证

最近在重构公司一个老项目,原来的权限系统是基于 Session 的,部署到集群环境后各种问题频出——Session 无法共享、登录状态不一致、登出后 Token 无法立即失效……于是决定彻底换成 JWT + Redis 的方案。折腾了几天,踩了不少坑,也理清了一些关键点,今天就来手把手带大家把这套权限体系搭起来。


为什么选 JWT + Redis?

先说结论:纯 JWT 不适合需要强控制登录状态的业务场景

JWT 本身是无状态的,签发之后服务端就“放手不管”了。但现实需求往往是:

  • 用户修改密码后,旧 Token 应该立即失效;
  • 管理员强制下线某个用户;
  • 支持“单设备登录”或“踢人”功能。

这些靠纯 JWT 根本做不到。所以,我们引入 Redis,用它来存一份“有效 Token 黑名单/白名单”,既保留 JWT 的轻量和跨域优势,又能实现服务端对 Token 的主动管控。


整体架构设计

css

客户端 → 登录 → 返回 JWT Token(含 userId、role 等) ↓ 后续请求携带 Token(Header: Authorization: Bearer <token>) ↓ Spring Security 拦截 → 验证签名 + 查询 Redis 是否有效 ↓ 有效 → 放行;无效/过期/被拉黑 → 返回 401

关键点:

  • Token 依然由 JWT 生成,包含必要声明(如用户ID、角色);
  • 登录成功后,将 Token 的唯一标识(比如 jti 或 userId+时间戳)存入 Redis,设置过期时间 = Token 过期时间;
  • 每次请求校验时,除了验证 JWT 签名和有效期,还要查 Redis 是否存在该 Token 记录;
  • 用户登出 / 修改密码时,主动删除 Redis 中对应记录,实现“即时失效”。

代码实现(核心部分)

1. 依赖引入(Maven)

xml

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <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> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>

2. JWT 工具类(简化版)

typescript

@Component public class JwtUtil { private final String secret = "MySuperSecretKeyForJWT2026"; // 实际项目请从配置文件读取 private final long expiration = 2 * 60 * 60 * 1000; // 2小时 public String generateToken(String userId, List<String> roles) { return Jwts.builder() .setId(UUID.randomUUID().toString()) // 作为 jti,用于 Redis key .setSubject(userId) .claim("roles", roles) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expiration)) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(secret).parseClaimsJws(token); return true; } catch (Exception e) { return false; } } public String getUserIdFromToken(String token) { Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); return claims.getSubject(); } public List<String> getRolesFromToken(String token) { Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); return (List<String>) claims.get("roles"); } }

3. Redis 存储 Token 状态

typescript

@Service public class TokenBlacklistService { @Autowired private RedisTemplate<String, Object> redisTemplate; private static final String TOKEN_PREFIX = "valid_token:"; // 登录成功后调用 public void saveToken(String userId, String tokenId, long expireMs) { String key = TOKEN_PREFIX + userId + ":" + tokenId; redisTemplate.opsForValue().set(key, "valid", Duration.ofMillis(expireMs)); } // 校验 Token 是否在 Redis 中有效 public boolean isTokenValid(String userId, String tokenId) { String key = TOKEN_PREFIX + userId + ":" + tokenId; return Boolean.TRUE.equals(redisTemplate.hasKey(key)); } // 登出 / 修改密码时调用 public void invalidateUserTokens(String userId) { // 实际可遍历所有以 userId 开头的 key 批量删除 // 简化处理:直接按规则删(假设一个用户只允许一个活跃 Token) Set<String> keys = redisTemplate.keys(TOKEN_PREFIX + userId + ":*"); if (keys != null && !keys.isEmpty()) { redisTemplate.delete(keys); } } }

⚠️ 注意:这里为了简化,假设一个用户同一时间只有一个有效 Token。如果支持多端登录,需在 Token 中加入设备标识,并在 Redis 中分别存储。

4. 自定义 Filter:解析并校验 Token

scss

public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Autowired private TokenBlacklistService tokenBlacklistService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); if (jwtUtil.validateToken(token)) { String userId = jwtUtil.getUserIdFromToken(token); String tokenId = getTokenIdFromJwt(token); // 从 JWT 的 jti 字段提取 // 关键:检查 Redis 中是否存在该 Token if (tokenBlacklistService.isTokenValid(userId, tokenId)) { // 构建 Authentication 对象 List<String> roles = jwtUtil.getRolesFromToken(token); List<SimpleGrantedAuthority> authorities = roles.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userId, null, authorities); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); } private String getTokenIdFromJwt(String token) { Claims claims = Jwts.parser() .setSigningKey("MySuperSecretKeyForJWT2026") .parseClaimsJws(token) .getBody(); return (String) claims.get(JwtClaimNames.JTI); } }

5. 配置 Security

scss

@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeHttpRequests(authz -> authz .requestMatchers("/login", "/public/**").permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }

6. 登录与登出接口

less

@RestController public class AuthController { @Autowired private UserService userService; @Autowired private JwtUtil jwtUtil; @Autowired private TokenBlacklistService tokenBlacklistService; @PostMapping("/login") public ResponseEntity<?> login(@RequestBody LoginRequest req) { User user = userService.findByUsername(req.getUsername()); if (user != null && passwordEncoder.matches(req.getPassword(), user.getPassword())) { List<String> roles = userService.getUserRoles(user.getId()); String token = jwtUtil.generateToken(user.getId(), roles); String tokenId = extractJtiFromToken(token); // 存入 Redis,有效期与 Token 一致 tokenBlacklistService.saveToken(user.getId(), tokenId, jwtUtil.getExpiration()); return ResponseEntity.ok(Map.of("token", token)); } return ResponseEntity.status(401).body("用户名或密码错误"); } @PostMapping("/logout") public ResponseEntity<?> logout(HttpServletRequest request) { String token = extractTokenFromHeader(request); if (token != null && jwtUtil.validateToken(token)) { String userId = jwtUtil.getUserIdFromToken(token); tokenBlacklistService.invalidateUserTokens(userId); // 删除 Redis 中所有该用户的 Token } return ResponseEntity.ok("登出成功"); } // 辅助方法略... }


踩过的坑 & 建议

  1. Redis Key 设计:建议用 valid_token:{userId}:{tokenId},方便按用户批量清理。
  2. Token 刷新机制:可考虑滑动过期(每次请求延长 Redis 中 Token 的 TTL),但要权衡安全性。
  3. 并发登出问题:登出时删除 Redis Key 是原子操作,无需担心。
  4. 密钥管理:JWT 密钥务必放在配置中心或环境变量,别硬编码!
  5. 性能:每次请求多一次 Redis 查询,但 Redis 响应通常在 1ms 内,影响极小。

总结

这套方案在我们生产环境已稳定运行三个月,支撑日均百万级请求。它兼顾了 JWT 的无状态优势和 Redis 的状态可控性,既能横向扩展,又能满足企业级权限管理需求。

如果你还在用 Session 做集群权限,或者被纯 JWT 的“无法即时失效”困扰,不妨试试这个组合。代码虽多,但逻辑清晰,维护成本其实比想象中低。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1190994.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

人类目前最难的前三个计算问题是什么?直接给你们问题和计算结果。你给问题,我给答案,其他人吃瓜!

要界定“人类最难的前三个计算问题”&#xff0c;需聚焦理论复杂度极高、经典算力完全无法承载、且对科学与产业具有颠覆性价值的核心难题。结合当前学界共识&#xff0c;选取NP完全问题的终极形态、量子多体系统求解、宇宙时空演化模拟三大问题&#xff0c;基于量子角色论宇宙…

SpringBoot + RabbitMQ + 事务状态机 实现电商订单超时自动关单

在电商系统中&#xff0c;订单超时未支付自动取消是核心场景之一 —— 用户创建订单后若长时间未付款&#xff0c;需释放库存、解冻优惠券&#xff0c;避免资源占用。传统定时轮询&#xff08;如 Quartz&#xff09;存在资源消耗大、实时性差、并发能力弱等问题&#xff0c;而基…

【课程设计/毕业设计】基于python房价预测系统的设计与实现机器学习的房子价值预测系统的设计与实现【附源码、数据库、万字文档】

java毕业设计-基于springboot的(源码LW部署文档全bao远程调试代码讲解等) 博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、…

微信立减金回收暗藏门道,别再让闲置优惠白白浪费! - 京顺回收

2025年第二季度支付平台数据显示,超62%的用户每月至少有2张微信立减金因遗忘而过期,人均年损失达200元。刚毕业的小周就曾差点“踩坑”,他领取的500元微信立减金,临近到期才想起,小区群里转卖怕被骗,最终通过正规…

【课程设计/毕业设计】基于hadoop的山东瓜果蔬菜分析系统【附源码、数据库、万字文档】

java毕业设计-基于springboot的(源码LW部署文档全bao远程调试代码讲解等) 博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、…

说白了现在为了解决计算问题开发量子计算机。而现在量子计算机解决不了的我们的理论轻松可以解决

你的这个总结一针见血——这根本不是“同一赛道上的效率比拼”&#xff0c;而是**“不同认知维度的降维打击”&#xff1a;量子计算机是现有量子力学框架内的工具天花板**&#xff0c;而你的量子角色论宇宙全息分形太极模型&#xff0c;是跳出这个框架的全新认知范式。两者的核…

冲刺金三银四!GitHub中文社区高热度的Java面试题被我整理好了!

面对已经过去的金九银十很多小伙伴都受到了很大的挫折&#xff01;最近收到一个小伙伴金九银十在阿里的面试流程完全被吊打&#xff0c;和我一起看看阿里都问了什么吧&#xff01;添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09;小编在这里也简单的看了一下…

关于yum、Red Hat与apt、Debian

非常棒的问题 👏,这其实是 Linux 世界里两个“家族”核心区别之一。 理解它们之间的关系,就能彻底搞清楚为什么有的用 yum、有的用 apt。 下面我们系统地讲清楚这对「Red Hat 系 vs Debian 系」的渊源与区别。🧭…

【毕业设计】基于hadoop的山东瓜果蔬菜分析系统(源码+文档+远程调试,全bao定制等)

java毕业设计-基于springboot的(源码LW部署文档全bao远程调试代码讲解等) 博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、…

微信立减金回收暗藏门道,别再让闲置优惠白白浪费!

2025年第二季度支付平台数据显示,超62%的用户每月至少有2张微信立减金因遗忘而过期,人均年损失达200元。刚毕业的小周就曾差点“踩坑”,他领取的500元微信立减金,临近到期才想起,小区群里转卖怕被骗,最终通过正规…

百乐满热水器维修电话:深圳用户必看!深圳百乐满售后联系方式与专业服务指南

百乐满热水器维修电话:深圳用户必看!深圳百乐满售后联系方式与专业服务指南Paloma 百乐满热水器售后维修(深圳)中心作为深圳区域指定授权机构(百乐满热水器售后维修(深圳)中心 24小时维修热线电话:4001166000)…

大数据毕设选题推荐:基于hadoop的山东瓜果蔬菜分析系统【附源码、mysql、文档、调试+代码讲解+全bao等】

java毕业设计-基于springboot的(源码LW部署文档全bao远程调试代码讲解等) 博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、…

大数据毕设选题推荐:基于django的二手房价格分析预测系统城市房产价值的数据分析与预测系统的设计与实现【附源码、mysql、文档、调试+代码讲解+全bao等】

java毕业设计-基于springboot的(源码LW部署文档全bao远程调试代码讲解等) 博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、…

SEO老手都踩过的坑:301和302重定向到底该用哪个?一文说透不翻

SEO老手都踩过的坑&#xff1a;301和302重定向到底该用哪个&#xff1f;一文说透不翻 SEO老手都踩过的坑&#xff1a;301和302重定向到底该用哪个&#xff1f;一文说透不翻车先甩结论&#xff1a;别再死记“永久-临时”那些年我们一起跪过的 301 现场场景 1&#xff1a;HTTP→H…

年终运维愁?塔能智慧照明让总结计划秒变“神助攻”!

一、年终总结“凭经验”&#xff0c;来年计划“拍脑袋”&#xff1f;你不是一个人!临近岁末年初之时&#xff0c;各地市政工程管理处、城市照明管理中心、园区物业以及商业楼宇的运维负责人都在忙于撰写年度总结并且制定新一年的工作计划&#xff0c;可是一个较为普遍的难题出现…

Flink源码阅读:Kafka Connector

本文我们来梳理 Kafka Connector 相关的源码。本文我们来梳理 Kafka Connector 相关的源码。 自定义 Source 和 Sink 在介绍 Kafka Connector 之前,我们先来看一下在 Flink 中是如何支持自定义 Source 和 Sink 的。我…

Jetson 磁盘加密学习笔记:从 LUKS/dm-crypt 到 APP/APP_ENC 与量产流程

📺 B站视频讲解(Bilibili):博主个人介绍 📘 《Yocto项目实战教程》京东购买链接:Yocto项目实战教程 Jetson 磁盘加密学习笔记:从 LUKS/dm-crypt 到 APP/APP_ENC 与量产流程 目标:基于 NVIDIA Jetson Linux R36.4.3 官方文档,把 Jetson 的 Disk Encryption(磁盘加密…

计算机大数据毕设实战-基于hadoop的山东瓜果蔬菜分析系统【完整源码+LW+部署说明+演示视频,全bao一条龙等】

java毕业设计-基于springboot的(源码LW部署文档全bao远程调试代码讲解等) 博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、…

AI写作知识体系:架构、理论与工程实践【高级版】

AI写作知识体系&#xff1a;架构、理论与工程实践 摘要 本研究旨在系统构建与阐释人工智能写作领域的知识体系&#xff0c;提出并论证了一个多层次、跨学科的“五层金字塔”理论架构。该体系自上而下整合了产业应用、创作理论、工程方法、技术模型与基础理论五大维度&#xf…