You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

服务端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的无状态特性。

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 09:01:57