跳至主要内容

Security API

Security 模組提供登入引擎、JWT、登入鎖定、Token 黑名單、M2M、API key 與 resource-server 組裝能力。

套件: io.leandev.appfuse.security

本 API 不定義任何應用 URL。下方若提及 /api/v1/...,只代表 app-tenant-server reference 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

建構子接受 TokenBlacklistStoreObjectMapper。另可傳 JwtTokenProvider,改以 session id 檢查,使同一 session 的 access 與 refresh token 一起失效。

Resource Server

ResourceServerFactory

AuthMode.STANDALONEAuthMode.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();

它負責:

  1. stateless session、CORS、CSRF 與 form login;
  2. TokenBlacklistFilter
  3. 選用 ApiKeyAuthenticationFilter
  4. 選用 BasicAuthenticationFilter
  5. 標準 oauth2ResourceServer().jwt()
  6. 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_basicclient_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。

相關連結