最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

解讀Token失效的6種方案

 更新時(shí)間:2025年08月05日 09:41:35   作者:隔壁老王的代碼  
本文探討SpringBoot中實(shí)現(xiàn)JWT令牌失效的六種方案,包括短期令牌+刷新令牌、Redis黑名單、令牌版本機(jī)制等,分析其優(yōu)缺點(diǎn)及適用場景,指導(dǎo)如何根據(jù)安全與性能需求選擇合適策略

JWT(JSON Web Token)作為一種輕量級(jí)的認(rèn)證方式,被廣泛應(yīng)用于現(xiàn)代Web應(yīng)用和微服務(wù)架構(gòu)中。

然而,JWT的無狀態(tài)特性雖然帶來了擴(kuò)展性優(yōu)勢,卻也帶來了令牌管理的挑戰(zhàn),特別是當(dāng)需要使令牌提前失效時(shí)。

本文將介紹在SpringBoot應(yīng)用中實(shí)現(xiàn)JWT令牌失效的6種方案。

一、JWT基礎(chǔ)與失效挑戰(zhàn)

1.1 JWT的基本結(jié)構(gòu)

JWT由三部分組成,以點(diǎn)(.)分隔:

  • Header(頭部) :包含令牌類型和使用的簽名算法
  • Payload(負(fù)載) :包含聲明(claims),如用戶信息和權(quán)限
  • Signature(簽名) :用于驗(yàn)證令牌的完整性和真實(shí)性

一個(gè)典型的JWT看起來像這樣:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

1.2 JWT的特點(diǎn)與失效挑戰(zhàn)

JWT的主要特點(diǎn)是無狀態(tài)性,服務(wù)器不需要存儲(chǔ)會(huì)話信息。

這帶來了以下挑戰(zhàn):

  • JWT一旦簽發(fā),在其有效期內(nèi)始終有效
  • 無法直接撤銷或使令牌失效
  • 服務(wù)器默認(rèn)無法跟蹤已發(fā)行的令牌

這些特性使得實(shí)現(xiàn)JWT的提前失效變得困難,特別是在以下場景:

  • 用戶登出系統(tǒng)
  • 用戶權(quán)限變更
  • 賬戶被盜,需要使所有令牌失效
  • 密碼更改后使舊令牌失效

二、短期令牌+刷新令牌方案

2.1 基本原理

該方案使用兩種令牌:

  • 短期訪問令牌(Access Token) :有效期短(如15分鐘),用于API訪問
  • 長期刷新令牌(Refresh Token) :有效期長(如7天),用于獲取新的訪問令牌

當(dāng)用戶需要登出時(shí),只需使刷新令牌失效,短期訪問令牌會(huì)自然過期。

2.2 SpringBoot實(shí)現(xiàn)

首先,添加必要的依賴:

<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>

創(chuàng)建JWT工具類:

@Component
public class JwtTokenProvider {
    
    @Value("${jwt.secret}")
    private String jwtSecret;
    
    @Value("${jwt.accessTokenExpiration}")
    private long accessTokenExpiration;
    
    @Value("${jwt.refreshTokenExpiration}")
    private long refreshTokenExpiration;
    
    public String generateAccessToken(UserDetails userDetails) {
        return generateToken(userDetails, accessTokenExpiration);
    }
    
    public String generateRefreshToken(UserDetails userDetails) {
        return generateToken(userDetails, refreshTokenExpiration);
    }
    
    private String generateToken(UserDetails userDetails, long expiration) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);
        
        return Jwts.builder()
                .setSubject(userDetails.getUsername())
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(Keys.hmacShaKeyFor(jwtSecret.getBytes()), SignatureAlgorithm.HS512)
                .compact();
    }
    
    public String getUsernameFromToken(String token) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes()))
                .build()
                .parseClaimsJws(token)
                .getBody();
        
        return claims.getSubject();
    }
    
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                .setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes()))
                .build()
                .parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

實(shí)現(xiàn)刷新令牌服務(wù):

@Service
@RequiredArgsConstructor
public class RefreshTokenService {
    
    private final RefreshTokenRepository refreshTokenRepository;
    private final JwtTokenProvider jwtTokenProvider;
    
    @Transactional
    public RefreshToken createRefreshToken(String username) {
        RefreshToken refreshToken = new RefreshToken();
        refreshToken.setUsername(username);
        refreshToken.setToken(UUID.randomUUID().toString());
        refreshToken.setExpiryDate(Instant.now().plusMillis(
                jwtTokenProvider.getRefreshTokenExpiration()));
        
        return refreshTokenRepository.save(refreshToken);
    }
    
    @Transactional
    public void deleteByUsername(String username) {
        refreshTokenRepository.deleteByUsername(username);
    }
    
    public Optional<RefreshToken> findByToken(String token) {
        return refreshTokenRepository.findByToken(token);
    }
    
    public RefreshToken verifyExpiration(RefreshToken token) {
        if (token.getExpiryDate().compareTo(Instant.now()) < 0) {
            refreshTokenRepository.delete(token);
            throw new TokenRefreshException(token.getToken(), 
                "Refresh token was expired. Please make a new signin request");
        }
        
        return token;
    }
}

實(shí)現(xiàn)認(rèn)證控制器:

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
    
    private final AuthenticationManager authenticationManager;
    private final UserDetailsService userDetailsService;
    private final JwtTokenProvider jwtTokenProvider;
    private final RefreshTokenService refreshTokenService;
    
    @PostMapping("/login")
    public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                loginRequest.getUsername(), 
                loginRequest.getPassword()
            )
        );
        
        SecurityContextHolder.getContext().setAuthentication(authentication);
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        
        String accessToken = jwtTokenProvider.generateAccessToken(userDetails);
        RefreshToken refreshToken = refreshTokenService.createRefreshToken(userDetails.getUsername());
        
        return ResponseEntity.ok(new JwtResponse(accessToken, refreshToken.getToken()));
    }
    
    @PostMapping("/refresh")
    public ResponseEntity<?> refreshToken(@Valid @RequestBody TokenRefreshRequest request) {
        String requestRefreshToken = request.getRefreshToken();
        
        return refreshTokenService.findByToken(requestRefreshToken)
            .map(refreshTokenService::verifyExpiration)
            .map(RefreshToken::getUsername)
            .map(username -> {
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                String accessToken = jwtTokenProvider.generateAccessToken(userDetails);
                return ResponseEntity.ok(new TokenRefreshResponse(accessToken, requestRefreshToken));
            })
            .orElseThrow(() -> new TokenRefreshException(requestRefreshToken,
                "Refresh token is not in database!"));
    }
    
    @PostMapping("/logout")
    public ResponseEntity<?> logoutUser(@Valid @RequestBody LogoutRequest logoutRequest) {
        refreshTokenService.deleteByUsername(logoutRequest.getUsername());
        return ResponseEntity.ok(new MessageResponse("Log out successful!"));
    }
}

application.properties配置:

jwt.secret=yourVeryLongAndSecureSecretKeyHerePleaseMakeItAtLeast256Bits
jwt.accessTokenExpiration=900000    # 15分鐘
jwt.refreshTokenExpiration=604800000 # 7天

2.3 優(yōu)缺點(diǎn)分析

優(yōu)點(diǎn):

  • 無需維護(hù)黑名單,降低服務(wù)器負(fù)擔(dān)
  • 訪問令牌有效期短,安全性較高
  • 用戶體驗(yàn)良好,透明刷新令牌
  • 實(shí)現(xiàn)簡單,容易理解

缺點(diǎn):

  • 無法即時(shí)使訪問令牌失效,最多等待其自然過期
  • 需要額外存儲(chǔ)刷新令牌,增加了狀態(tài)性
  • 增加了客戶端復(fù)雜度,需要處理令牌刷新邏輯
  • 如果刷新令牌泄露,可能導(dǎo)致長期安全風(fēng)險(xiǎn)

2.4 適用場景

  • 一般的Web應(yīng)用和移動(dòng)應(yīng)用
  • 對(duì)令牌即時(shí)失效要求不嚴(yán)格的場景
  • 希望減輕服務(wù)器負(fù)擔(dān)的系統(tǒng)
  • 用戶會(huì)話時(shí)間較長的應(yīng)用

三、Redis黑名單機(jī)制

3.1 基本原理

黑名單機(jī)制將已注銷或失效的令牌存儲(chǔ)在Redis等高性能緩存中,每次驗(yàn)證令牌時(shí)都會(huì)檢查它是否在黑名單中。

這種方法允許即時(shí)使令牌失效,同時(shí)保持良好的性能。

3.2 SpringBoot實(shí)現(xiàn)

首先,添加Redis依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

創(chuàng)建Redis配置類:

@Configuration
public class RedisConfig {
    
    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, String> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        return template;
    }
}

實(shí)現(xiàn)JWT黑名單服務(wù):

@Service
@RequiredArgsConstructor
public class JwtBlacklistService {
    
    private final RedisTemplate<String, String> redisTemplate;
    private final JwtTokenProvider jwtTokenProvider;
    
    private static final String BLACKLIST_PREFIX = "jwt:blacklist:";
    
    public void blacklistToken(String token) {
        try {
            // 獲取令牌過期時(shí)間
            Claims claims = jwtTokenProvider.getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            long ttl = (expiration.getTime() - System.currentTimeMillis()) / 1000;
            
            // 僅當(dāng)令牌未過期時(shí)添加到黑名單
            if (ttl > 0) {
                String key = BLACKLIST_PREFIX + token;
                redisTemplate.opsForValue().set(key, "blacklisted", ttl, TimeUnit.SECONDS);
            }
        } catch (Exception e) {
            // 令牌已無效,無需加入黑名單
        }
    }
    
    public boolean isBlacklisted(String token) {
        String key = BLACKLIST_PREFIX + token;
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }
}

更新JWT工具類:

@Component
public class JwtTokenProvider {
    
    // ... 之前的代碼 ...
    
    public Claims getClaimsFromToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes()))
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}

添加JWT過濾器,檢查黑名單:

@Component
@RequiredArgsConstructor
public class JwtTokenFilter extends OncePerRequestFilter {
    
    private final JwtTokenProvider jwtTokenProvider;
    private final JwtBlacklistService blacklistService;
    private final UserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 
                                   FilterChain filterChain) throws ServletException, IOException {
        try {
            String jwt = getJwtFromRequest(request);
            
            if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
                // 檢查令牌是否在黑名單中
                if (blacklistService.isBlacklisted(jwt)) {
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    response.getWriter().write("Token has been revoked");
                    return;
                }
                
                String username = jwtTokenProvider.getUsernameFromToken(jwt);
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                
                UsernamePasswordAuthenticationToken authentication = 
                    new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception ex) {
            logger.error("Could not set user authentication in security context", ex);
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

實(shí)現(xiàn)登出端點(diǎn):

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
    
    // ... 之前的代碼 ...
    
    private final JwtBlacklistService blacklistService;
    
    @PostMapping("/logout")
    public ResponseEntity<?> logoutUser(HttpServletRequest request) {
        String jwt = getJwtFromRequest(request);
        if (StringUtils.hasText(jwt)) {
            blacklistService.blacklistToken(jwt);
        }
        return ResponseEntity.ok(new MessageResponse("Log out successful!"));
    }
    
    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

3.3 優(yōu)缺點(diǎn)分析

優(yōu)點(diǎn):

  • 可以即時(shí)使令牌失效
  • 不影響令牌原有的有效期管理
  • 無需修改客戶端邏輯
  • Redis高性能,對(duì)系統(tǒng)影響小

缺點(diǎn):

  • 引入了狀態(tài)存儲(chǔ),部分犧牲JWT的無狀態(tài)特性
  • Redis需要存儲(chǔ)所有已注銷但未過期的令牌,增加存儲(chǔ)開銷
  • 每次API請求都需要檢查黑名單,增加了延遲

3.4 適用場景

  • 對(duì)安全性要求較高的應(yīng)用
  • 需要即時(shí)令牌失效功能的系統(tǒng)

四、令牌版本/計(jì)數(shù)器機(jī)制

4.1 基本原理

該方案為每個(gè)用戶維護(hù)一個(gè)令牌版本號(hào)或計(jì)數(shù)器。當(dāng)用戶登出或需要使令牌失效時(shí),增加用戶的令牌版本號(hào)。

令牌中包含發(fā)行時(shí)的版本號(hào),驗(yàn)證時(shí)比較令牌中的版本號(hào)與用戶當(dāng)前的版本號(hào),如果不匹配則拒絕訪問。

4.2 SpringBoot實(shí)現(xiàn)

首先,創(chuàng)建用戶令牌版本實(shí)體:

@Entity
@Table(name = "user_token_versions")
@Data
public class UserTokenVersion {
    
    @Id
    private String username;
    
    private int tokenVersion;
    
    public void incrementVersion() {
        this.tokenVersion++;
    }
}

創(chuàng)建令牌版本倉庫:

@Repository
public interface UserTokenVersionRepository extends JpaRepository<UserTokenVersion, String> {
}

實(shí)現(xiàn)令牌版本服務(wù):

@Service
@RequiredArgsConstructor
public class TokenVersionService {
    
    private final UserTokenVersionRepository repository;
    
    @Transactional
    public int getCurrentVersion(String username) {
        return repository.findById(username)
            .orElseGet(() -> {
                UserTokenVersion newVersion = new UserTokenVersion();
                newVersion.setUsername(username);
                newVersion.setTokenVersion(0);
                return repository.save(newVersion);
            })
            .getTokenVersion();
    }
    
    @Transactional
    public void incrementVersion(String username) {
        UserTokenVersion version = repository.findById(username)
            .orElseGet(() -> {
                UserTokenVersion newVersion = new UserTokenVersion();
                newVersion.setUsername(username);
                newVersion.setTokenVersion(0);
                return newVersion;
            });
        
        version.incrementVersion();
        repository.save(version);
    }
}

修改JWT工具類,在令牌中包含版本信息:

@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
    
    @Value("${jwt.secret}")
    private String jwtSecret;
    
    @Value("${jwt.expiration}")
    private long jwtExpiration;
    
    private final TokenVersionService tokenVersionService;
    
    public String generateToken(UserDetails userDetails) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpiration);
        
        // 獲取當(dāng)前令牌版本
        int tokenVersion = tokenVersionService.getCurrentVersion(userDetails.getUsername());
        
        return Jwts.builder()
                .setSubject(userDetails.getUsername())
                .claim("tokenVersion", tokenVersion)  // 添加版本信息
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(Keys.hmacShaKeyFor(jwtSecret.getBytes()), SignatureAlgorithm.HS512)
                .compact();
    }
    
    public boolean validateToken(String token, UserDetails userDetails) {
        try {
            Claims claims = getClaimsFromToken(token);
            
            // 驗(yàn)證用戶名
            boolean usernameMatches = claims.getSubject().equals(userDetails.getUsername());
            
            // 驗(yàn)證令牌未過期
            boolean isNotExpired = claims.getExpiration().after(new Date());
            
            // 驗(yàn)證令牌版本
            int tokenVersion = claims.get("tokenVersion", Integer.class);
            int currentVersion = tokenVersionService.getCurrentVersion(userDetails.getUsername());
            boolean versionMatches = tokenVersion == currentVersion;
            
            return usernameMatches && isNotExpired && versionMatches;
        } catch (Exception e) {
            return false;
        }
    }
    
    // ... 其他方法 ...
}

更新JWT過濾器:

@Component
@RequiredArgsConstructor
public class JwtTokenFilter extends OncePerRequestFilter {
    
    private final JwtTokenProvider jwtTokenProvider;
    private final UserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 
                                   FilterChain filterChain) throws ServletException, IOException {
        try {
            String jwt = getJwtFromRequest(request);
            
            if (StringUtils.hasText(jwt)) {
                String username = jwtTokenProvider.getUsernameFromToken(jwt);
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                
                // 使用版本驗(yàn)證令牌
                if (jwtTokenProvider.validateToken(jwt, userDetails)) {
                    UsernamePasswordAuthenticationToken authentication = 
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        } catch (Exception ex) {
            logger.error("Could not set user authentication in security context", ex);
        }
        
        filterChain.doFilter(request, response);
    }
    
    // ... getJwtFromRequest方法 ...
}

實(shí)現(xiàn)登出端點(diǎn):

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
    
    // ... 其他代碼 ...
    
    private final TokenVersionService tokenVersionService;
    
    @PostMapping("/logout")
    public ResponseEntity<?> logoutUser(Authentication authentication) {
        String username = authentication.getName();
        
        // 增加令牌版本號(hào),使所有現(xiàn)有令牌失效
        tokenVersionService.incrementVersion(username);
        
        return ResponseEntity.ok(new MessageResponse("Log out successful!"));
    }
}

4.3 優(yōu)缺點(diǎn)分析

優(yōu)點(diǎn):

  • 存儲(chǔ)開銷小,只需記錄用戶的當(dāng)前版本號(hào)
  • 無需維護(hù)黑名單,降低了內(nèi)存需求
  • 可以選擇性地使部分令牌失效

缺點(diǎn):

  • 需要存儲(chǔ)用戶令牌版本
  • 每次驗(yàn)證令牌都需要查詢數(shù)據(jù)庫或緩存
  • 可能影響系統(tǒng)性能,特別是在用戶量大的情況下

4.4 適用場景

  • 需要用戶主動(dòng)登出功能的系統(tǒng)
  • 用戶量適中的系統(tǒng)
  • 需要在特定操作后使令牌失效的場景

五、密鑰輪換策略

5.1 基本原理

密鑰輪換策略通過定期更換用于簽名JWT的密鑰來實(shí)現(xiàn)令牌失效。

當(dāng)系統(tǒng)需要使所有令牌失效時(shí),立即輪換密鑰,所有使用舊密鑰簽名的令牌將無法通過驗(yàn)證。

為了支持平滑過渡,系統(tǒng)通常保留多個(gè)最近的密鑰版本。

5.2 SpringBoot實(shí)現(xiàn)

創(chuàng)建密鑰管理服務(wù):

@Service
@Slf4j
public class KeyRotationService {
    
    private final Map<String, Key> keyStore = new ConcurrentHashMap<>();
    private String currentKeyId;
    
    @PostConstruct
    public void init() {
        // 初始化第一個(gè)密鑰
        rotateKey();
    }
    
    @Scheduled(cron = "${jwt.key-rotation-cron:0 0 0 * * ?}") // 默認(rèn)每天零點(diǎn)
    public void scheduledRotation() {
        log.info("Performing scheduled key rotation");
        rotateKey();
    }
    
    public synchronized void rotateKey() {
        String keyId = UUID.randomUUID().toString();
        Key key = generateKey();
        keyStore.put(keyId, key);
        
        // 只保留最近3個(gè)密鑰
        if (keyStore.size() > 3) {
            List<String> keyIds = new ArrayList<>(keyStore.keySet());
            keyIds.sort(null); // 自然排序
            
            for (int i = 0; i < keyIds.size() - 3; i++) {
                keyStore.remove(keyIds.get(i));
            }
        }
        
        currentKeyId = keyId;
        log.info("Key rotated, new key ID: {}", keyId);
    }
    
    public String getCurrentKeyId() {
        return currentKeyId;
    }
    
    public Key getKey(String keyId) {
        return keyStore.get(keyId);
    }
    
    public Key getCurrentKey() {
        return keyStore.get(currentKeyId);
    }
    
    private Key generateKey() {
        return Keys.secretKeyFor(SignatureAlgorithm.HS512);
    }
    
    public void forceRotation() {
        log.info("Forcing key rotation to invalidate all tokens");
        rotateKey();
    }
}

更新JWT工具類以支持密鑰輪換:

@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
    
    @Value("${jwt.expiration}")
    private long jwtExpiration;
    
    private final KeyRotationService keyRotationService;
    
    public String generateToken(UserDetails userDetails) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpiration);
        
        String keyId = keyRotationService.getCurrentKeyId();
        Key key = keyRotationService.getCurrentKey();
        
        return Jwts.builder()
                .setSubject(userDetails.getUsername())
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .setHeaderParam("kid", keyId) // 設(shè)置密鑰ID
                .signWith(key, SignatureAlgorithm.HS512)
                .compact();
    }
    
    public Claims getClaimsFromToken(String token) {
        // 從令牌頭部提取密鑰ID
        String kid = extractKeyId(token);
        if (kid == null) {
            throw new JwtException("Invalid JWT: Missing key ID");
        }
        
        // 獲取對(duì)應(yīng)的密鑰
        Key key = keyRotationService.getKey(kid);
        if (key == null) {
            throw new JwtException("Invalid JWT: Unknown key ID");
        }
        
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
    
    private String extractKeyId(String token) {
        try {
            String header = token.split("\.")[0];
            String decodedHeader = new String(Base64.getDecoder().decode(header));
            JsonNode headerNode = new ObjectMapper().readTree(decodedHeader);
            return headerNode.get("kid").asText();
        } catch (Exception e) {
            return null;
        }
    }
    
    public boolean validateToken(String token) {
        try {
            getClaimsFromToken(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    
    // ... 其他方法 ...
}

創(chuàng)建管理員控制器,提供強(qiáng)制失效所有令牌的功能:

@RestController
@RequestMapping("/api/admin")
@RequiredArgsConstructor
@PreAuthorize("hasRole('ADMIN')")
public class AdminController {
    
    private final KeyRotationService keyRotationService;
    
    @PostMapping("/invalidate-all-tokens")
    public ResponseEntity<?> invalidateAllTokens() {
        keyRotationService.forceRotation();
        return ResponseEntity.ok(new MessageResponse("All tokens have been invalidated"));
    }
}

5.3 優(yōu)缺點(diǎn)分析

優(yōu)點(diǎn):

  • 可以立即使所有令牌失效
  • 可以實(shí)現(xiàn)平滑過渡,支持舊密鑰一段時(shí)間
  • 符合安全最佳實(shí)踐,定期輪換密鑰

缺點(diǎn):

  • 無法選擇性使單個(gè)用戶的令牌失效
  • 可能導(dǎo)致所有用戶被迫重新登錄
  • 需要妥善管理密鑰

5.4 適用場景

  • 安全要求高,需要定期輪換密鑰的系統(tǒng)
  • 發(fā)生安全事件時(shí),需要緊急使所有令牌失效
  • 偏好無狀態(tài)設(shè)計(jì)的應(yīng)用
  • 系統(tǒng)重大升級(jí)或維護(hù)時(shí)

六、集中式令牌存儲(chǔ)

6.1 基本原理

這種方法將JWT作為訪問標(biāo)識(shí)符,但在服務(wù)器端維護(hù)一個(gè)集中式的令牌存儲(chǔ),存儲(chǔ)介質(zhì)可以使用數(shù)據(jù)庫或者緩存。

每次驗(yàn)證時(shí),不僅檢查JWT的簽名和有效期,還查詢存儲(chǔ)庫確認(rèn)令牌是否仍然有效。

這種方式結(jié)合了JWT的便利性和會(huì)話管理的靈活性。

6.2 SpringBoot實(shí)現(xiàn)

創(chuàng)建令牌實(shí)體:

@Entity
@Table(name = "active_tokens")
@Data
public class ActiveToken {
    
    @Id
    private String tokenId;
    
    private String username;
    
    private Date expiryDate;
    
    private boolean revoked;
    
    @CreationTimestamp
    private Date createdAt;
    
    public boolean isExpired() {
        return expiryDate.before(new Date());
    }
}

創(chuàng)建令牌倉庫:

@Repository
public interface ActiveTokenRepository extends JpaRepository<ActiveToken, String> {
    
    List<ActiveToken> findByUsername(String username);
    
    @Modifying
    @Query("UPDATE ActiveToken t SET t.revoked = true WHERE t.username = :username")
    void revokeAllUserTokens(@Param("username") String username);
    
    @Modifying
    @Query("DELETE FROM ActiveToken t WHERE t.expiryDate < :now")
    void deleteExpiredTokens(@Param("now") Date now);
}

實(shí)現(xiàn)令牌服務(wù):

@Service
@RequiredArgsConstructor
public class TokenStorageService {
    
    private final ActiveTokenRepository tokenRepository;
    
    @Transactional
    public void saveToken(String tokenId, String username, Date expiryDate) {
        ActiveToken token = new ActiveToken();
        token.setTokenId(tokenId);
        token.setUsername(username);
        token.setExpiryDate(expiryDate);
        token.setRevoked(false);
        
        tokenRepository.save(token);
    }
    
    @Transactional(readOnly = true)
    public boolean isTokenValid(String tokenId) {
        return tokenRepository.findById(tokenId)
            .map(token -> !token.isRevoked() && !token.isExpired())
            .orElse(false);
    }
    
    @Transactional
    public void revokeToken(String tokenId) {
        tokenRepository.findById(tokenId).ifPresent(token -> {
            token.setRevoked(true);
            tokenRepository.save(token);
        });
    }
    
    @Transactional
    public void revokeAllUserTokens(String username) {
        tokenRepository.revokeAllUserTokens(username);
    }
    
    @Scheduled(fixedRate = 86400000) // 每天清理一次
    @Transactional
    public void cleanExpiredTokens() {
        tokenRepository.deleteExpiredTokens(new Date());
    }
}

更新JWT工具類:

@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
    
    @Value("${jwt.secret}")
    private String jwtSecret;
    
    @Value("${jwt.expiration}")
    private long jwtExpiration;
    
    private final TokenStorageService tokenStorageService;
    
    public String generateToken(UserDetails userDetails) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpiration);
        
        // 生成唯一的令牌ID
        String tokenId = UUID.randomUUID().toString();
        
        String token = Jwts.builder()
                .setSubject(userDetails.getUsername())
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .setId(tokenId)  // 設(shè)置JWT ID (jti)
                .signWith(Keys.hmacShaKeyFor(jwtSecret.getBytes()), SignatureAlgorithm.HS512)
                .compact();
        
        // 將令牌保存到存儲(chǔ)中
        tokenStorageService.saveToken(tokenId, userDetails.getUsername(), expiryDate);
        
        return token;
    }
    
    public String getTokenId(String token) {
        return getClaimsFromToken(token).getId();
    }
    
    public boolean validateToken(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            
            // 驗(yàn)證JWT基本屬性
            boolean isNotExpired = claims.getExpiration().after(new Date());
            
            // 驗(yàn)證令牌是否在存儲(chǔ)中有效
            String tokenId = claims.getId();
            boolean isValidInStorage = tokenStorageService.isTokenValid(tokenId);
            
            return isNotExpired && isValidInStorage;
        } catch (Exception e) {
            return false;
        }
    }
    
    // ... 其他方法 ...
}

實(shí)現(xiàn)登出功能:

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
    
    // ... 其他代碼 ...
    
    private final JwtTokenProvider jwtTokenProvider;
    private final TokenStorageService tokenStorageService;
    
    @PostMapping("/logout")
    public ResponseEntity<?> logoutUser(HttpServletRequest request) {
        String jwt = getJwtFromRequest(request);
        
        if (StringUtils.hasText(jwt)) {
            String tokenId = jwtTokenProvider.getTokenId(jwt);
            tokenStorageService.revokeToken(tokenId);
        }
        
        return ResponseEntity.ok(new MessageResponse("Log out successful!"));
    }
    
    @PostMapping("/logout-all")
    public ResponseEntity<?> logoutAllDevices(Authentication authentication) {
        String username = authentication.getName();
        tokenStorageService.revokeAllUserTokens(username);
        
        return ResponseEntity.ok(new MessageResponse("Logged out from all devices"));
    }
    
    // ... 其他方法 ...
}

6.3 優(yōu)缺點(diǎn)分析

優(yōu)點(diǎn):

  • 能夠即時(shí)使單個(gè)令牌或所有令牌失效

  • 提供精細(xì)的令牌管理,如查看活躍會(huì)話

  • 可以實(shí)現(xiàn)"記住我"等高級(jí)功能

  • 便于審計(jì)和監(jiān)控

缺點(diǎn):

  • 完全放棄了JWT的無狀態(tài)優(yōu)勢

  • 每次請求都需要查詢存儲(chǔ)庫

  • 系統(tǒng)復(fù)雜度提高

6.4 適用場景

  • 對(duì)安全性要求極高的系統(tǒng)

  • 需要精細(xì)令牌管理的應(yīng)用

  • 已有會(huì)話管理需求的項(xiàng)目

  • 多設(shè)備登錄管理

  • 企業(yè)級(jí)應(yīng)用,需要詳細(xì)的審計(jì)日志

七、會(huì)話狀態(tài)監(jiān)控機(jī)制

7.1 基本原理

會(huì)話狀態(tài)監(jiān)控機(jī)制在保持JWT無狀態(tài)特性的同時(shí),通過跟蹤用戶會(huì)話狀態(tài)來間接控制令牌有效性。

系統(tǒng)維護(hù)用戶登錄狀態(tài)(如最后活動(dòng)時(shí)間、登錄設(shè)備等),當(dāng)狀態(tài)變更(如密碼修改、異常登錄)時(shí),可以拒絕特定令牌的訪問。

7.2 SpringBoot實(shí)現(xiàn)

創(chuàng)建用戶會(huì)話狀態(tài)實(shí)體:

@Entity
@Table(name = "user_sessions")
@Data
public class UserSessionStatus {
    
    @Id
    private String username;
    
    private Date passwordLastChanged;
    
    private Date lastForcedLogout;
    
    private String securityContext;
    
    @Version
    private Long version;
    
    public boolean hasChangedAfter(Date tokenIssuedAt) {
        return (passwordLastChanged != null && passwordLastChanged.after(tokenIssuedAt)) ||
               (lastForcedLogout != null && lastForcedLogout.after(tokenIssuedAt));
    }
}

創(chuàng)建會(huì)話狀態(tài)倉庫:

@Repository
public interface UserSessionStatusRepository extends JpaRepository<UserSessionStatus, String> {
}

實(shí)現(xiàn)會(huì)話狀態(tài)服務(wù):

@Service
@RequiredArgsConstructor
public class UserSessionService {
    
    private final UserSessionStatusRepository repository;
    
    @Transactional(readOnly = true)
    public UserSessionStatus getSessionStatus(String username) {
        return repository.findById(username)
            .orElseGet(() -> {
                UserSessionStatus status = new UserSessionStatus();
                status.setUsername(username);
                return status;
            });
    }
    
    @Transactional
    public void updatePasswordChanged(String username) {
        UserSessionStatus status = getSessionStatus(username);
        status.setPasswordLastChanged(new Date());
        repository.save(status);
    }
    
    @Transactional
    public void forceLogout(String username) {
        UserSessionStatus status = getSessionStatus(username);
        status.setLastForcedLogout(new Date());
        repository.save(status);
    }
    
    @Transactional
    public void updateSecurityContext(String username, String securityContext) {
        UserSessionStatus status = getSessionStatus(username);
        status.setSecurityContext(securityContext);
        repository.save(status);
    }
    
    public boolean isTokenValid(String username, Date tokenIssuedAt, String tokenSecurityContext) {
        UserSessionStatus status = getSessionStatus(username);
        
        // 檢查令牌是否在密碼更改或強(qiáng)制登出之前簽發(fā)
        if (status.hasChangedAfter(tokenIssuedAt)) {
            return false;
        }
        
        // 檢查安全上下文是否匹配(可選)
        if (status.getSecurityContext() != null && tokenSecurityContext != null) {
            return status.getSecurityContext().equals(tokenSecurityContext);
        }
        
        return true;
    }
}

更新JWT工具類:

@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
    
    @Value("${jwt.secret}")
    private String jwtSecret;
    
    @Value("${jwt.expiration}")
    private long jwtExpiration;
    
    private final UserSessionService sessionService;
    
    public String generateToken(UserDetails userDetails, String securityContext) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpiration);
        
        return Jwts.builder()
                .setSubject(userDetails.getUsername())
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .claim("securityContext", securityContext)
                .signWith(Keys.hmacShaKeyFor(jwtSecret.getBytes()), SignatureAlgorithm.HS512)
                .compact();
    }
    
    public boolean validateToken(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            
            // 基本驗(yàn)證
            boolean isNotExpired = claims.getExpiration().after(new Date());
            if (!isNotExpired) {
                return false;
            }
            
            // 驗(yàn)證會(huì)話狀態(tài)
            String username = claims.getSubject();
            Date issuedAt = claims.getIssuedAt();
            String securityContext = claims.get("securityContext", String.class);
            
            return sessionService.isTokenValid(username, issuedAt, securityContext);
        } catch (Exception e) {
            return false;
        }
    }
    
    // ... 其他方法 ...
}

實(shí)現(xiàn)認(rèn)證和密碼更改接口:

@RestController
@RequiredArgsConstructor
public class AuthController {
    
    // ... 其他依賴 ...
    
    private final UserSessionService sessionService;
    private final UserService userService;
    
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
        // ... 認(rèn)證邏輯 ...
        
        // 生成安全上下文(例如,設(shè)備信息、IP地址等)
        String securityContext = generateSecurityContext(request);
        
        // 更新用戶會(huì)話狀態(tài)
        sessionService.updateSecurityContext(userDetails.getUsername(), securityContext);
        
        // 生成令牌,包含安全上下文
        String token = jwtTokenProvider.generateToken(userDetails, securityContext);
        
        // ... 返回令牌 ...
    }
    
    @PostMapping("/change-password")
    public ResponseEntity<?> changePassword(@RequestBody PasswordChangeRequest request, 
                                           Authentication authentication) {
        String username = authentication.getName();
        
        // 更改密碼
        userService.changePassword(username, request.getOldPassword(), request.getNewPassword());
        
        // 更新密碼更改時(shí)間,使舊令牌失效
        sessionService.updatePasswordChanged(username);
        
        return ResponseEntity.ok(new MessageResponse("Password changed successfully"));
    }
    
    @PostMapping("/logout-all-devices")
    public ResponseEntity<?> logoutAllDevices(Authentication authentication) {
        String username = authentication.getName();
        
        // 強(qiáng)制所有設(shè)備登出
        sessionService.forceLogout(username);
        
        return ResponseEntity.ok(new MessageResponse("Logged out from all devices"));
    }
    
    private String generateSecurityContext(HttpServletRequest request) {
        // 生成包含設(shè)備信息、IP地址等的安全上下文
        String ipAddress = request.getRemoteAddr();
        String userAgent = request.getHeader("User-Agent");
        
        return DigestUtils.md5DigestAsHex((ipAddress + ":" + userAgent).getBytes());
    }
}

7.3 優(yōu)缺點(diǎn)分析

優(yōu)點(diǎn):

  • 保持了JWT的大部分無狀態(tài)特性

  • 可以基于用戶狀態(tài)變更使令牌失效

  • 可以實(shí)現(xiàn)細(xì)粒度的會(huì)話控制

  • 安全上下文可以防止令牌被盜用

缺點(diǎn):

  • 每次請求需要檢查用戶會(huì)話狀態(tài)

  • 狀態(tài)管理增加了系統(tǒng)復(fù)雜性

  • 安全上下文驗(yàn)證可能導(dǎo)致合法用戶被拒絕(如IP變化)

7.4 適用場景

  • 需要賬戶安全功能(如密碼更改后使令牌失效)的系統(tǒng)

  • 對(duì)可疑活動(dòng)監(jiān)控有需求的應(yīng)用

  • 需要防止令牌盜用的場景

  • 平衡無狀態(tài)性和安全性的應(yīng)用

八、六種方案對(duì)比與選擇指南

方案

即時(shí)失效

存儲(chǔ)需求

性能影響

實(shí)現(xiàn)復(fù)雜度

維護(hù)成本

適用場景

短期令牌+刷新令牌

部分(僅刷新令牌)

一般Web/移動(dòng)應(yīng)用

Redis黑名單

完全

安全性要求高的應(yīng)用

令牌版本/計(jì)數(shù)器

完全

特定操作下需要控制Token有效性需求的應(yīng)用

密鑰輪換

全局

極低

需要定期輪換密鑰的系統(tǒng)

集中式令牌存儲(chǔ)

完全

企業(yè)級(jí)應(yīng)用,多設(shè)備管理

會(huì)話狀態(tài)監(jiān)控

條件性

平衡安全和性能的系統(tǒng)

九、總結(jié)

每種方案都有其優(yōu)缺點(diǎn)和適用場景,選擇合適的方案取決于應(yīng)用的安全需求、性能要求和架構(gòu)設(shè)計(jì)。

在實(shí)際應(yīng)用中,常常需要組合使用多種策略,構(gòu)建多層次的安全防護(hù)。

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。 

相關(guān)文章

  • mybatis 遍歷foreach中or拼接的操作

    mybatis 遍歷foreach中or拼接的操作

    這篇文章主要介紹了mybatis 遍歷foreach中or拼接的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2021-02-02
  • SpringBoot接口訪問頻率限制的實(shí)現(xiàn)方式

    SpringBoot接口訪問頻率限制的實(shí)現(xiàn)方式

    接口訪問頻率限制是通過在一定時(shí)間內(nèi)限制用戶對(duì)接口的訪問次數(shù)來實(shí)現(xiàn)的,在Spring Boot中,我們可以通過多種方式來實(shí)現(xiàn)接口的限流,如使用過濾器、攔截器或者借助第三方庫,本文給大家講解的非常詳細(xì),需要的朋友可以參考下
    2024-07-07
  • Java修飾符 abstract,static,final 的區(qū)別詳解

    Java修飾符 abstract,static,final 的區(qū)別詳解

    以下是對(duì)Java修飾符abstract,static,final的區(qū)別進(jìn)行了詳細(xì)的介紹,需要的朋友可以過來參考下
    2013-09-09
  • SpringBoot使用ResponseBodyEmitter處理流式日志和進(jìn)度條

    SpringBoot使用ResponseBodyEmitter處理流式日志和進(jìn)度條

    這篇文章主要為大家詳細(xì)介紹了SpringBoot如何使用ResponseBodyEmitter處理流式日志和進(jìn)度條,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2025-02-02
  • java實(shí)現(xiàn)文件的上傳功能

    java實(shí)現(xiàn)文件的上傳功能

    這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)文件的上傳功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-01-01
  • Maven 安裝目錄的詳細(xì)介紹

    Maven 安裝目錄的詳細(xì)介紹

    這篇文章主要介紹了Maven 安裝目錄的詳細(xì)介紹的相關(guān)資料,這里對(duì)Maven進(jìn)行了解讀,希望通過本文可以幫助到大家,需要的朋友可以參考下
    2017-08-08
  • Spring Cloud OpenFeign實(shí)現(xiàn)動(dòng)態(tài)服務(wù)名調(diào)用的示例代碼

    Spring Cloud OpenFeign實(shí)現(xiàn)動(dòng)態(tài)服務(wù)名調(diào)用的示例代碼

    在微服務(wù)架構(gòu)中,我們經(jīng)常需要根據(jù)動(dòng)態(tài)傳入的服務(wù)名來遠(yuǎn)程調(diào)用其他服務(wù),例如,你的業(yè)務(wù)中可能有多個(gè)子服務(wù):service-1、service-2……需要?jiǎng)討B(tài)決定調(diào)用哪個(gè),所以本文給大家介紹了Spring Cloud OpenFeign 實(shí)現(xiàn)動(dòng)態(tài)服務(wù)名調(diào)用指南,需要的朋友可以參考下
    2025-06-06
  • Java8中StringJoiner類的使用詳解

    Java8中StringJoiner類的使用詳解

    Java在java.util包中添加了一個(gè)新的最終類StringJoiner??梢杂糜跇?gòu)造由定界符分隔的字符序列。本文將通過示例和大家分享一下StringJoiner類的使用,需要的可以參考一下
    2022-10-10
  • IntelliJ IDEA修復(fù)ESLint:修復(fù)'prettier/prettier' 警告的六種方法

    IntelliJ IDEA修復(fù)ESLint:修復(fù)'prettier/prettier'&n

    本文介紹了在IntelliJIDEA或WebStorm中修復(fù)ESLint警告的多種方法,包括使用快速修復(fù)功能、配置保存時(shí)自動(dòng)修復(fù)、手動(dòng)運(yùn)行ESLint修復(fù)、檢查配置和依賴、臨時(shí)忽略規(guī)則以及常見問題排查,需要的朋友可以參考下
    2026-03-03
  • Java多數(shù)據(jù)源的三種實(shí)現(xiàn)方式小結(jié)

    Java多數(shù)據(jù)源的三種實(shí)現(xiàn)方式小結(jié)

    多數(shù)據(jù)源是在一個(gè)應(yīng)用程序中配置和使用多個(gè)不同的數(shù)據(jù)庫連接,本文主要介紹了Java多數(shù)據(jù)源的三種實(shí)現(xiàn)方式小結(jié),具有一定的參考價(jià)值,感興趣的可以了解一下
    2025-03-03

最新評(píng)論

望都县| 饶阳县| 长武县| 武陟县| 仁寿县| 绿春县| 莱阳市| 松溪县| 昆明市| 宁津县| 台北县| 扎赉特旗| 无极县| 铜陵市| 仙桃市| 丹棱县| 张家界市| 贡觉县| 钟山县| 什邡市| 临邑县| 怀仁县| 镇康县| 大英县| 安达市| 盐边县| 涪陵区| 灵丘县| 孙吴县| 福泉市| 宣威市| 宜昌市| 江油市| 上蔡县| 济阳县| 留坝县| 疏附县| 瓦房店市| 三门峡市| 读书| 皋兰县|