跳至主要内容

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);
// LockoutException 繼承 Spring Security 的 LockedException,
// 可被框架的標準例外處理機制(RFC 7807)捕獲
throw new LockoutException(
"Account locked. Try again in " + remaining.toMinutes() + " minutes",
username, remaining, failures
);
}
}

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);
boolean contains(String token);
void remove(String token);
}
備註

TTL(黑名單記錄的自動過期)由底層 Cache 設定,不在 add 方法上指定。

CacheTokenBlacklistStore

基於快取的黑名單實作。建構子接受一個預先配置好的 Cache<String, Boolean>(非 CacheManager),TTL 由該 Cache 設定(建議與 access token 過期時間一致)。

@Bean
public Cache<String, Boolean> tokenBlacklistCache(CacheManager cacheManager) {
return CacheBuilder
.newCache(cacheManager, "tokenBlacklist", String.class, Boolean.class)
.heap(1000)
.ttl(15) // 15 分鐘(與 access token 過期時間一致)
.build();
}

@Bean
public TokenBlacklistStore tokenBlacklistStore(Cache<String, Boolean> tokenBlacklistCache) {
return new CacheTokenBlacklistStore(tokenBlacklistCache);
}

使用範例

@Autowired
private TokenBlacklistStore blacklistStore;

@Autowired
private JwtTokenProvider tokenProvider;

public void logout(String token) {
// 加入黑名單;記錄會在底層 Cache 的 TTL 到期後自動清除
blacklistStore.add(token);
}

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

認證過濾器

DelegatingJwtAuthenticationFilter

委派 JWT 認證過濾器。從請求標頭提取 JWT,委派給 AuthenticationManager 進行認證;認證失敗時由 AuthenticationEntryPoint 處理。實際的本地/OAuth2 token 驗證由註冊在 AuthenticationManager 中的 AuthenticationProviderLocalJwtAuthenticationProviderOAuthJwtAuthenticationProvider)承擔。

@Bean
public DelegatingJwtAuthenticationFilter jwtAuthenticationFilter(
AuthenticationManager authenticationManager,
AuthenticationEntryPoint authenticationEntryPoint) {
return new DelegatingJwtAuthenticationFilter(
authenticationManager,
authenticationEntryPoint
);
}

TokenBlacklistFilter

Token 黑名單過濾器。建構子接受 TokenBlacklistStoreObjectMapper(用於序列化 RFC 7807 錯誤回應)。另有 3 參數多載可額外傳入 JwtTokenProvider,改以 token 中的 session ID 檢查黑名單(讓登出同時失效 access token 與 refresh token)。

@Bean
public TokenBlacklistFilter tokenBlacklistFilter(
TokenBlacklistStore tokenBlacklistStore,
ObjectMapper objectMapper) {
return new TokenBlacklistFilter(tokenBlacklistStore, objectMapper);
}

配置 Security Filter Chain

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

return http.build();
}

異常類別

異常套件說明
LockoutExceptionio.leandev.appfuse.security.lockout.api帳號因連續登入失敗而鎖定。繼承 Spring Security 的 LockedException,攜帶 principalremainingTimefailureCount
備註

JWT token 的無效/過期不由獨立的例外類別表示——JwtTokenProvider.validateToken 失敗會丟出 Spring Security 的 AuthenticationException 子類,最終由框架的 RFC 7807 例外處理機制(ProblemDetailFactory / StandardRestExceptionHandler)統一映射為 401 回應。框架未提供 InvalidTokenException / ExpiredTokenException / AccountLockedException 等類別。

最佳實踐

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

相關連結