服务端JWT令牌存储位置、最佳实践及Spring Boot实现咨询
服务端JWT存储与最佳实践及Spring Boot实现
嘿,关于JWT在服务端的存储和最佳实践,我结合实际开发经验给你拆解清楚,还有Spring Boot的具体实现步骤:
先搞懂核心:JWT的设计初衷
JWT本身是自包含的无状态令牌,所有必要信息(用户身份、权限、过期时间)都加密在令牌里,服务端只需要验证签名就能确认合法性,理论上完全不需要存储。但实际开发中,总会遇到需要主动失效令牌的场景(比如用户登出、账号冻结),这时候才需要考虑存储的事。
1. 将JWT存储在数据库中是否是良好实践?
分两种情况看:
- 如果你的系统可以依赖令牌的过期时间自动失效(比如设置较短的有效期,比如1小时,配合刷新令牌),那完全没必要存数据库——这会违背JWT的无状态设计,平白增加数据库的查询压力,失去了JWT的核心优势。
- 如果必须支持主动失效(比如用户点了登出就立即不能用旧令牌),存储到数据库是可行的,但要注意优化:
- 别存完整令牌,只存关键标识:比如JWT的
jti(令牌唯一ID)、用户ID、过期时间,节省存储空间。 - 优先用缓存代替数据库:比如Redis,因为查询频率高,Redis的过期键可以自动清理过期的无效令牌,性能比数据库好太多。
- 别把JWT当会话用:如果每次请求都要查数据库验证JWT,那和传统的会话ID模式没区别,完全浪费了JWT的无状态特性。
- 别存完整令牌,只存关键标识:比如JWT的
2. 能否将JWT存储为会话值?
当然可以,但这其实是把JWT当成了传统的会话ID来用,等于放弃了JWT的无状态优势:
- 如果你的系统是单服务部署,不需要分布式扩展,这么做没问题,但直接用会话ID可能更简单。
- 如果是微服务架构,就算把JWT存在分布式会话(比如Redis)里,客户端还是要带JWT吗?其实没必要——这时候客户端带会话ID,服务端从会话里取用户信息就行,用JWT反而多此一举。
- 除非你需要在会话里额外存令牌的元数据(比如令牌的权限范围、失效时间),否则不建议这么做。
在Spring Boot中实现服务端JWT处理
我平时开发常用jjwt库来实现,以下是核心步骤,都是实际项目里验证过的:
1. 引入依赖
在pom.xml里添加JJWT的依赖:
<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工具类
这个类负责生成令牌、解析令牌、验证签名,是核心:
import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.security.Key; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.UUID; @Component public class JwtUtils { @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private long expirationMs; // 生成带jti的JWT令牌(jti用于主动失效) public String generateJwtToken(String username) { Map<String, Object> claims = new HashMap<>(); String jti = UUID.randomUUID().toString(); claims.put("jti", jti); return Jwts.builder() .setClaims(claims) .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expirationMs)) .signWith(getSigningKey(), SignatureAlgorithm.HS256) .compact(); } // 从令牌中获取用户名 public String getUserNameFromJwtToken(String token) { return Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody() .getSubject(); } // 从令牌中获取jti public String getJtiFromJwtToken(String token) { return Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody() .get("jti", String.class); } // 获取令牌过期时间 public long getExpirationFromToken(String token) { return Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody() .getExpiration() .getTime(); } // 验证令牌合法性(支持黑名单校验) public boolean validateJwtToken(String authToken, StringRedisTemplate redisTemplate) { try { Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(authToken); // 检查令牌是否在失效黑名单里 String jti = claimsJws.getBody().get("jti", String.class); if (redisTemplate.hasKey("invalid_jwt:" + jti)) { return false; } return true; } catch (SignatureException e) { System.err.println("JWT签名无效: " + e.getMessage()); } catch (MalformedJwtException e) { System.err.println("JWT格式错误: " + e.getMessage()); } catch (ExpiredJwtException e) { System.err.println("JWT已过期: " + e.getMessage()); } catch (UnsupportedJwtException e) { System.err.println("不支持的JWT类型: " + e.getMessage()); } catch (IllegalArgumentException e) { System.err.println("JWT内容为空或非法: " + e.getMessage()); } return false; } private Key getSigningKey() { // 密钥长度至少32位,否则会报错 return Keys.hmacShaKeyFor(secret.getBytes()); } }
3. 实现JWT认证过滤器
这个过滤器会拦截所有请求,从Authorization头里提取JWT并验证:
import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; public class AuthTokenFilter extends OncePerRequestFilter { @Autowired private JwtUtils jwtUtils; @Autowired private UserDetailsService userDetailsService; @Autowired private StringRedisTemplate redisTemplate; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = parseJwt(request); if (jwt != null && jwtUtils.validateJwtToken(jwt, redisTemplate)) { String username = jwtUtils.getUserNameFromJwtToken(jwt); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 将认证信息存入Security上下文 SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { System.err.println("设置用户认证失败: " + e.getMessage()); } filterChain.doFilter(request, response); } // 从请求头中提取JWT private String parseJwt(HttpServletRequest request) { String headerAuth = request.getHeader("Authorization"); if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { return headerAuth.substring(7); } return null; } }
4. 配置Spring Security
把过滤器加入Security配置,设置无状态会话:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration public class WebSecurityConfig { @Autowired private AuthTokenFilter authTokenFilter; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()) // 设置无状态会话,因为JWT是无状态的 .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth.requestMatchers("/api/auth/**").permitAll() // 开放登录、注册接口 .anyRequest().authenticated() // 其他接口需要认证 ); // 把JWT过滤器加到UsernamePasswordAuthenticationFilter前面 http.addFilterBefore(authTokenFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
5. 实现主动失效(可选)
如果需要支持用户登出,用Redis存储失效的jti:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Service public class JwtBlacklistService { @Autowired private JwtUtils jwtUtils; @Autowired private StringRedisTemplate redisTemplate; // 将令牌加入黑名单 public void addToBlacklist(String token) { String jti = jwtUtils.getJtiFromJwtToken(token); // 获取令牌剩余过期时间 long remainingTime = jwtUtils.getExpirationFromToken(token) - System.currentTimeMillis(); if (remainingTime > 0) { redisTemplate.opsForValue().set("invalid_jwt:" + jti, "true", remainingTime, TimeUnit.MILLISECONDS); } } }
然后在登出接口里调用addToBlacklist方法即可。
内容的提问来源于stack exchange,提问作者Bala venkatesh
相关产品推荐
相关产品推荐

