跳至主要内容

Security API

Security 模組提供 JWT 認證、登入鎖定和 Token 黑名單功能。

套件: io.leandev.appfuse.security

JWT 認證

JwtTokenProvider

JWT 令牌產生與驗證。

public class JwtTokenProvider {
// 產生 Access Token
String generateToken(UserDetails userDetails, Map<String, Object> claims)
String generateToken(UserDetails userDetails, Map<String, Object> claims, Instant expiryDate)
String generateToken(Authentication authentication, Map<String, Object> claims)
String generateToken(Authentication authentication)

// 產生 Refresh Token
String generateRefreshToken(Authentication authentication, Map<String, Object> claims)
String generateRefreshToken(UserDetails userDetails, Map<String, Object> claims)

// 解析與驗證
String getUsernameFromJwt(String token)
Claims getClaimsFromJwt(String token)
String getSessionIdFromJwt(String token)
void validateToken(String authToken)

// Token 刷新
String refreshToken(String refreshToken)

// 取得過期時間
long getRefreshTokenExpirationInMs()
}

JWT Claims 常數

public static final String CLAIM_SESSION_ID = "sessionId";
public static final String CLAIM_TOKEN_TYPE = "tokenType";

使用範例

@Autowired
private JwtTokenProvider tokenProvider;

// 登入時產生 Token
public AuthResponse login(LoginRequest request) {
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);

Map<String, Object> claims = new HashMap<>();
claims.put("tenantId", user.getTenantId());

String accessToken = tokenProvider.generateToken(auth, claims);
String refreshToken = tokenProvider.generateRefreshToken(auth, claims);

return new AuthResponse(accessToken, refreshToken);
}

// 驗證 Token
public void validateRequest(String token) {
tokenProvider.validateToken(token);
String username = tokenProvider.getUsernameFromJwt(token);
Claims claims = tokenProvider.getClaimsFromJwt(token);
}

登入鎖定

LoginAttemptTracker

登入嘗試追蹤器。

public interface LoginAttemptTracker {
int recordFailure(String principal);
boolean isLocked(String principal);
Optional<Duration> getRemainingLockoutTime(String principal);
void clearAttempts(String principal);
int getFailureCount(String principal);
}

LockoutPolicy

鎖定策略介面。

public interface LockoutPolicy {
int getThreshold();
Duration calculateLockoutDuration(int failureCount);
default boolean shouldLockout(int failureCount) {
return failureCount >= getThreshold();
}
}

策略實作

策略說明
FixedLockoutPolicy固定鎖定時間
IncrementalLockoutPolicy線性遞增鎖定時間
ExponentialLockoutPolicy指數型遞增鎖定時間

使用範例

@Autowired
private LoginAttemptTracker attemptTracker;

public void handleLoginFailure(String username) {
int failures = attemptTracker.recordFailure(username);

if (attemptTracker.isLocked(username)) {
Duration remaining = attemptTracker.getRemainingLockoutTime(username)
.orElse(Duration.ZERO);
throw new AccountLockedException(
"Account locked. Try again in " + remaining.toMinutes() + " minutes"
);
}
}

public void handleLoginSuccess(String username) {
attemptTracker.clearAttempts(username);
}

配置範例

@Bean
public LockoutPolicy lockoutPolicy() {
// 固定 15 分鐘鎖定
return new FixedLockoutPolicy(5, Duration.ofMinutes(15));
}

@Bean
public LockoutPolicy exponentialLockoutPolicy() {
// 指數型鎖定:5次後鎖定,基礎時間 5 分鐘
return new ExponentialLockoutPolicy(5, Duration.ofMinutes(5));
// 第6次:5分鐘,第7次:10分鐘,第8次:20分鐘...
}

Token 黑名單

TokenBlacklistStore

Token 黑名單存儲介面。

public interface TokenBlacklistStore {
void add(String token, Duration ttl);
boolean contains(String token);
void remove(String token);
}

CacheTokenBlacklistStore

基於快取的黑名單實作。

@Bean
public TokenBlacklistStore tokenBlacklistStore(CacheManager cacheManager) {
return new CacheTokenBlacklistStore(cacheManager);
}

使用範例

@Autowired
private TokenBlacklistStore blacklistStore;

@Autowired
private JwtTokenProvider tokenProvider;

public void logout(String token) {
// 計算 Token 剩餘有效時間
Claims claims = tokenProvider.getClaimsFromJwt(token);
Date expiration = claims.getExpiration();
Duration ttl = Duration.between(Instant.now(), expiration.toInstant());

// 加入黑名單
blacklistStore.add(token, ttl);
}

public boolean isTokenBlacklisted(String token) {
return blacklistStore.contains(token);
}

認證過濾器

DelegatingJwtAuthenticationFilter

委派 JWT 認證過濾器。

@Bean
public DelegatingJwtAuthenticationFilter jwtAuthenticationFilter() {
return new DelegatingJwtAuthenticationFilter(
localJwtAuthenticationProvider,
oauthJwtAuthenticationProvider
);
}

TokenBlacklistFilter

Token 黑名單過濾器。

@Bean
public TokenBlacklistFilter tokenBlacklistFilter() {
return new TokenBlacklistFilter(tokenBlacklistStore);
}

配置 Security Filter Chain

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.addFilterBefore(jwtAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(tokenBlacklistFilter(),
JwtAuthenticationFilter.class)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.anyRequest().authenticated()
);

return http.build();
}

異常類別

異常說明
InvalidTokenExceptionToken 無效
ExpiredTokenExceptionToken 已過期
AccountLockedException帳號已鎖定

最佳實踐

  1. Session ID - 在 Token 中加入 Session ID,支援登出所有裝置
  2. 短期 Access Token - Access Token 設定較短的有效期(如 15 分鐘)
  3. 長期 Refresh Token - Refresh Token 可設定較長有效期(如 7 天)
  4. 黑名單 TTL - 黑名單 TTL 應與 Token 剩餘有效時間一致
  5. 鎖定策略 - 根據安全需求選擇適當的鎖定策略

相關連結