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 中的 AuthenticationProvider(LocalJwtAuthenticationProvider、OAuthJwtAuthenticationProvider)承擔。
@Bean
public DelegatingJwtAuthenticationFilter jwtAuthenticationFilter(
AuthenticationManager authenticationManager,
AuthenticationEntryPoint authenticationEntryPoint) {
return new DelegatingJwtAuthenticationFilter(
authenticationManager,
authenticationEntryPoint
);
}
TokenBlacklistFilter
Token 黑名單過濾器。建構子接受 TokenBlacklistStore 與 ObjectMapper(用於序列化 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();
}
異常類別
| 異常 | 套件 | 說明 |
|---|---|---|
LockoutException | io.leandev.appfuse.security.lockout.api | 帳號因連續登入失敗而鎖定。繼承 Spring Security 的 LockedException,攜帶 principal、remainingTime、failureCount |
JWT token 的無效/過期不由獨立的例外類別表示——JwtTokenProvider.validateToken 失敗會丟出 Spring Security 的 AuthenticationException 子類,最終由框架的 RFC 7807 例外處理機制(ProblemDetailFactory / StandardRestExceptionHandler)統一映射為 401 回應。框架未提供 InvalidTokenException / ExpiredTokenException / AccountLockedException 等類別。
最佳實踐
- Session ID - 在 Token 中加入 Session ID,支援登出所有裝置
- 短期 Access Token - Access Token 設定較短的有效期(如 15 分鐘)
- 長期 Refresh Token - Refresh Token 可設定較長有效期(如 7 天)
- 黑名單 TTL - 黑名單 TTL 應與 Token 剩餘有效時間一致
- 鎖定策略 - 根據安全需求選擇適當的鎖定策略