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();
}
異常類別
| 異常 | 說明 |
|---|---|
InvalidTokenException | Token 無效 |
ExpiredTokenException | Token 已過期 |
AccountLockedException | 帳號已鎖定 |
最佳實踐
- Session ID - 在 Token 中加入 Session ID,支援登出所有裝置
- 短期 Access Token - Access Token 設定較短的有效期(如 15 分鐘)
- 長期 Refresh Token - Refresh Token 可設定較長有效期(如 7 天)
- 黑名單 TTL - 黑名單 TTL 應與 Token 剩餘有效時間一致
- 鎖定策略 - 根據安全需求選擇適當的鎖定策略