Security API
Security 模組提供登入引擎、JWT、登入鎖定、Token 黑名單、M2M、API key 與 resource-server 組裝能力。
套件: io.leandev.appfuse.security
本 API 不定義任何應用 URL。下方若提及
/api/v1/...,只代表app-tenant-serverreference implementation 的範例;具體端點與公開政策由應用決定。分層原則見 Feature 設計與使用。
登入與主體
AuthPrincipal
登入引擎對應用身分模型的最小契約,繼承 Spring Security UserDetails,並增加:
public interface AuthPrincipal extends UserDetails {
String id();
String displayName();
boolean interactiveLoginAllowed();
}
租戶不是必要成員;需要租戶的 principal 另實作 TenantAwareUserDetails。框架提供
DefaultAuthPrincipal 作為一般 adapter 基底。
AuthPrincipalLookup
登入、refresh 與 M2M 發放重新載入主體的 SPI:
public interface AuthPrincipalLookup {
Optional<? extends AuthPrincipal> findByUsername(String username);
}
Entity、repository 與這個 SPI 的 adapter 都屬 reference implementation。
LoginService
封裝帳密登入、refresh 與 logout 的固定 orchestration:
LoginResult login(String username, String password)
LoginResult refresh(String refreshToken)
void logout(String token)
LoginResult 是引擎結果,不是 HTTP DTO。Controller 應在 presentation 層轉為自己的 response;
service 不應 import controller DTO。
JWT
JwtTokenProvider
負責簽發與驗證本地 JWT、access/refresh token、session id 與 acting claims。應用以建構子提供 private/public key 與有效期。
RsaKeyPairs
封裝固定的 JCA 機械細節:
KeyPair generated = RsaKeyPairs.generate(2048);
KeyPair decoded = RsaKeyPairs.decode(base64Pkcs8PrivateKey, base64X509PublicKey);
- private key wire format:Base64 PKCS#8
- public key wire format:Base64 X.509 SubjectPublicKeyInfo
- 小於 2048 bits 會拒絕
金鑰來源、是否允許開發期 ephemeral key 與 key size 仍由應用決定。
持久化資料加密
PersistenceEncryption
PersistenceEncryption 接受應用提供的 Base64 32-byte root key ring,依固定 purpose 以
HKDF-SHA-256 衍生 AES-256-GCM key。框架不讀 Spring 設定,也不暴露 root key:
import io.leandev.appfuse.security.crypto.PersistenceEncryption;
import io.leandev.appfuse.security.crypto.TextCipher;
PersistenceEncryption encryption = PersistenceEncryption.fromBase64(
"v2", Map.of("v1", oldRootKey, "v2", activeRootKey));
TextCipher cipher = encryption.scoped("my-app/customer-token/v1");
TextCipher.encrypt 產生帶 envelope version 與 root key id 的密文;decrypt 依 key id 從 key ring
選取 root key。新資料永遠使用 active key,舊 key 只用於輪替期間解密。不同 purpose 的衍生 key
互相隔離,某用途的密文不能交給另一用途解密。
TextCipher.identity() 是不改變內容的相容組裝 primitive,不是 at-rest protection。應用需要
保護資料時必須明確傳入 PersistenceEncryption.scoped(...) 的結果。
Spring binding、root key 的部署與輪替 runbook 見持久化資料加密指南。
DAO authentication manager
DaoAuthenticationManagers 建立 DaoAuthenticationProvider + ProviderManager,並固定補上手建
manager 容易漏掉的 authentication event publisher:
AuthenticationManager loginManager = DaoAuthenticationManagers
.builder(userDetailsService, passwordEncoder, eventPublisher)
.preAuthenticationChecks(loginLockout)
.build();
AuthenticationManager m2mManager = DaoAuthenticationManagers
.builder(userDetailsService, passwordEncoder, eventPublisher)
.build();
未指定 preAuthenticationChecks 時保留 Spring 標準帳號狀態檢查。是否套用互動登入鎖定是應用
政策;參考實作的 M2M manager 刻意不套用 LoginLockout,但仍發布認證事件。
登入鎖定
LoginLockout
LoginLockout 同時是:
UserDetailsChecker:在 DAO provider 的 pre-authentication 階段把關;- authentication event listener:記錄 bad credentials,成功時清除。
LoginLockout lockout =
new LoginLockout(attemptCache, 5, Duration.ofMinutes(1));
預設鎖定時長為線性遞增。要採固定或指數策略,可覆寫:
protected Duration lockoutDuration(int failureCount)
狀態以不可變的 AttemptRecord(failureCount, lockedUntil) 存入
Cache<String, AttemptRecord>。管理面可呼叫 clear(username) 解鎖。
Token 黑名單
TokenBlacklistStore
public interface TokenBlacklistStore {
void add(String token);
boolean contains(String token);
void remove(String token);
}
CacheTokenBlacklistStore 接受預先配置的 Cache<String, Boolean>;TTL 由 cache 設定,通常與
access token 有效期一致。
TokenBlacklistFilter
建構子接受 TokenBlacklistStore 與 ObjectMapper。另可傳 JwtTokenProvider,改以 session id
檢查,使同一 session 的 access 與 refresh token 一起失效。
Resource Server
ResourceServerFactory
依 AuthMode.STANDALONE 或 AuthMode.FEDERATED 建立 JwtDecoder 與 authentication converter,
並在設定互斥錯誤時 fail-fast。
ResourceServerSecurity
組裝 stateless resource server 的固定機制與 filter 順序:
ResourceServerSecurity
.builder(
http,
tokenBlacklistStore,
objectMapper,
jwtTokenProvider,
jwtDecoder,
jwtAuthenticationConverter,
bearerEntryPoint,
bearerAccessDeniedHandler)
.apiKey(apiKeyEnabled, apiKeyLookup, apiKeyHeader)
.basicAuth(basicAuthEnabled, authenticationManager, basicEntryPoint)
.configure();
它負責:
- stateless session、CORS、CSRF 與 form login;
TokenBlacklistFilter;- 選用
ApiKeyAuthenticationFilter; - 選用
BasicAuthenticationFilter; - 標準
oauth2ResourceServer().jwt(); - RFC 6750 + RFC 7807 的 401/403 handler。
它完全不持有 URL、HTTP method 或 authorization rule。呼叫端須在 configure() 後自行宣告:
http.authorizeHttpRequests(auth -> auth
// app-tenant-server 參考路徑;不是 capability contract。
.requestMatchers(HttpMethod.POST,
"/api/v1/auth/login",
"/api/v1/auth/refresh",
"/api/v1/auth/logout",
"/api/v1/auth/token",
"/api/v1/auth/exchange")
.permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/auth/link/consume")
.permitAll()
.anyRequest().authenticated());
不要以 /api/v1/auth/** 整段放行;exact method + path 能讓未來新增端點預設受保護。
M2M client credentials
ClientCredentialsService
驗證非互動主體並簽發短效 access token。憑證比對委派 AuthenticationManager;限流經選用
ClientCredentialsRateLimiter;是否要求 tenant 是建構值,不是 SPI。
ClientCredentialsRequest
解析 RFC 6749 的 client_secret_basic、client_secret_post 與 JSON 相容方言。兩種 client
認證方式同時出現時拒絕;不支援的 scope 以 invalid_scope 明確拒絕。
ClientCredentialsFactory
以 SecureRandom 產生 256-bit、URL-safe 的 client secret。唯一性重試屬持久層責任,留在
reference implementation。
API key
ApiKeyPrincipalLookup
API key 到 AuthPrincipal 的應用 adapter SPI。框架不擁有 key entity、repository、輪替或管理
端點。
ApiKeyHash
以 SHA-256 雜湊高熵 key。API key 每次請求都呈遞,使用 bcrypt 會造成不必要的 CPU 成本;明文 只應在簽發時回傳一次。
ApiKeyAuthenticationFilter
讀取應用指定的 header name,查找 principal 並寫入 SecurityContext。啟用但沒有
ApiKeyPrincipalLookup 時,ResourceServerSecurity 會 fail-fast;關閉時不註冊 filter。