1. 簡介
Spring Security 6.3 版本引入了多種安全增強功能到框架中。
在本教程中,我們將討論其中一些最值得注意的功能,並突出顯示它們的優勢和用法。
2. 被動 JDK 序列化支持
Spring Security 6.3 包含了被動 JDK 序列化支持。但是,在進一步討論之前,讓我們先理解圍繞該問題和問題。
2.1. Spring Security 序列化設計
在 6.3 版本之前,Spring Security 對通過 JDK 序列化在不同版本之間序列化和反序列化其類持有嚴格的策略。這一限制是框架有意設計的決策,旨在確保安全性和穩定性。理由是防止通過使用不同版本的 Spring Security 序列化的對象進行反序列化而導致的不兼容性和安全漏洞。
其中一個關鍵方面是使用全局的serialVersionUID用於整個 Spring Security 項目。在 Java 中,序列化和反序列化過程使用唯一的標識符serialVersionUID來驗證加載的類與序列化的對象完全匹配。
通過維護全局的serialVersionUID,該serialVersionUID是每個 Spring Security 發佈版本獨有的,框架確保從一個版本序列化的對象不能使用另一個版本進行反序列化。這種方法有效地創建了版本屏障,防止具有不匹配的serialVersionUID值的對象進行反序列化。
例如,Spring Security 中的SecurityContextImpl類代表安全上下文信息。該類的序列化版本包含針對該版本的serialVersionUID。嘗試在不同版本的 Spring Security 中反序列化該對象,serialVersionUID不匹配會導致該過程失敗。
2.2. 序列化設計帶來的挑戰
雖然優先考慮增強安全性,但該設計策略也引入了幾個挑戰。開發人員通常將 Spring Security 集成到其他 Spring 庫中,如 Spring Session,用於管理用户登錄會話。這些會話包含關鍵的用户身份驗證和安全上下文信息,通常由 Spring Security 類實現。此外,為了優化用户體驗並提高應用程序的可伸縮性,開發人員通常將此會話數據存儲在各種持久存儲解決方案中,包括數據庫。
以下是由於序列化設計帶來的一些挑戰。通過 Canary 發佈的應用程序進行升級可能會導致問題,如果 Spring Security 版本發生更改,則可能無法反序列化持久的會話信息,從而可能需要用户重新登錄。
另一個問題出現在使用 Spring Security 的遠程方法調用 (RMI) 應用程序架構中。例如,如果客户端應用程序使用 Spring Security 類進行遠程方法調用,則必須在客户端端序列化它們,然後在另一端反序列化它們。如果兩個應用程序不共享相同的 Spring Security 版本,則該調用將失敗,結果是InvalidClassException異常。
2.3. 解決方法
針對此問題的典型解決方法如下。我們可以使用 JDK 序列化以外的其他序列化庫,如 Jackson 序列化。通過這樣做,而不是序列化 Spring Security 類,我們可以獲取所需詳細信息的 JSON 表示形式,並使用 Jackson 進行序列化。
另一個選項是擴展所需的 Spring Security 類,如Authentication,並通過readObject和writeObject方法顯式實現自定義序列化支持。
2.4. Spring Security 6.3 中的序列化更改
在 6.3 版本中,類序列化與前一個次要版本進行兼容性檢查。這確保了升級到新版本可以無縫地反序列化 Spring Security 類。
3. Authorization
Spring Security 6.3 引入了一些 Spring Security Authorization 的顯著變更。 在本部分,我們來探索這些變更。
3.1. Annotation Parameters
Spring Security 的方法安全策略支持元註解。我們可以對註解進行改進,使其更易於閲讀,具體取決於應用程序的使用案例。例如,我們可以簡化 @PreAuthorize(“hasRole(‘USER’)”) 為以下內容:
@Target({ ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('USER')")
public @interface IsUser {
String[] value();
}
接下來,我們可以使用此 @IsUser 註解在業務代碼中:
@Service
public class MessageService {
@IsUser
public Message readMessage() {
return "Message";
}
}
假設我們還有另一個角色,ADMIN。我們可以為這個角色創建一個名為 @IsAdmin 的註解。但是,這會造成冗餘。更合適的做法是使用此元註解作為模板,並將角色作為註解參數包含其中。Spring Security 6.3 引入了定義此類元註解的能力。我們來通過一個具體的例子來演示這一點:
為了對元註解進行模板化,首先需要定義一個 Bean PrePostTemplateDefaults:
@Bean
PrePostTemplateDefaults prePostTemplateDefaults() {
return new PrePostTemplateDefaults();
}
此 Bean 定義對於模板解析是必需的。
接下來,我們將定義一個元註解 @CustomHasAnyRole 用於 @PreAuthorize 註解,該註解可以接受 USER 和 ADMIN 角色:
@Target({ ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole({value})")
public @interface CustomHasAnyRole {
String[] value();
}
我們可以使用此元註解,並提供角色值:
@Service
public class MessageService {
private final List<Message> messages;
public MessageService() {
messages = new ArrayList<>();
messages.add(new Message(1, "Message 1"));
}
@CustomHasAnyRole({"'USER'", "'ADMIN'"})
public Message readMessage(Integer id) {
return messages.get(0);
}
@CustomHasAnyRole("'ADMIN'")
public String writeMessage(Message message) {
return "Message Written";
}
@CustomHasAnyRole({"'ADMIN'"})
public String deleteMessage(Integer id) {
return "Message Deleted";
}
}
在上面的示例中,我們提供了角色值 - USER 和 ADMIN 作為註解參數。
3.2. Securing Return Values
Spring Security 6.3 的另一個強大新功能是使用 @AuthorizeReturnObject 註解來安全域對象。 此增強功能允許通過對方法返回的對象的進行授權檢查,從而實現更精細的安全性,確保只有授權用户才能訪問特定的域對象。
我們來通過一個例子演示一下。假設我們有 Account 類,其中包含 iban 和 balance 字段。要求是,只有具有 read 權限的用户才能檢索賬户餘額。
public class Account {
private String iban;
private Double balance;
// Constructor
public String getIban() {
return iban;
}
@PreAuthorize("hasAuthority('read')")
public Double getBalance() {
return balance;
}
}
接下來,我們將定義 AccountService 類,該類返回一個 Account 實例:
@Service
public class AccountService {
@AuthorizeReturnObject
public Optional<Account> getAccountByIban(String iban) {
return Optional.of(new Account("XX1234567809", 2345.6));
}
}
在上面的片段中,我們使用了 @AuthorizeReturnObject 註解。Spring security 確保 Account 實例只能由具有 read 權限的用户訪問。
3.3. Error Handling
在上面的部分中,我們討論瞭如何使用 @AuthorizeReturnObject 註解來安全域對象。 一旦啓用,未授權的訪問會導致 AccessDeniedException。 Spring Security 6.3 提供了 MethodAuthorizationDeniedHandler 接口來處理授權失敗。
我們來通過一個例子演示一下。我們繼續在 3.2 節中使用 @AuthorizeReturnObject 註解來安全域對象。 但是,我們打算為任何未授權訪問提供一個掩碼值,而不是拋出 AccessDeniedException。
我們將定義 MethodAuthorizationDeniedHandler 接口的實現:
@Component
public class MaskMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler {
@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
return "****";
}
}
在上面的片段中,我們為存在 AccessDeniedException 時提供了掩碼值。
此處理程序類可以在 getIban() 方法中使用,如下所示:
@PreAuthorize("hasAuthority('read')")
@HandleAuthorizationDenied(handlerClass=MaskMethodAuthorizationDeniedHandler.class)
public String getIban() {
return iban;
}
4. 檢查已泄露密碼
Spring Security 6.3 提供用於檢查已泄露密碼的實現。該實現會將提供的密碼與已泄露密碼數據庫 (pwnedpasswords.com) 進行匹配。因此,應用程序可以在註冊時驗證用户提供的密碼。以下代碼片段演示了用法。
首先,定義 HaveIBeenPwnedRestApiPasswordChecker 類的 Bean 定義:
@Bean
public HaveIBeenPwnedRestApiPasswordChecker passwordChecker() {
return new HaveIBeenPwnedRestApiPasswordChecker();
}
接下來,使用此實現來檢查用户提供的密碼:
@RestController
@RequestMapping("/register")
public class RegistrationController {
private final HaveIBeenPwnedRestApiPasswordChecker haveIBeenPwnedRestApiPasswordChecker;
@Autowired
public RegistrationController(HaveIBeenPwnedRestApiPasswordChecker haveIBeenPwnedRestApiPasswordChecker) {
this.haveIBeenPwnedRestApiPasswordChecker = haveIBeenPwnedRestApiPasswordChecker;
}
@PostMapping
public String register(@RequestParam String username, @RequestParam String password) {
CompromisedPasswordDecision compromisedPasswordDecision = haveIBeenPwnedRestApiPasswordChecker.checkPassword(password);
if (compromisedPasswordDecision.isCompromised()) {
throw new IllegalArgumentException("Compromised Password.");
}
// ...
return "User registered successfully";
}
}
5. OAuth 2.0 Token 交換授權流程
Spring Security 6.3 也引入了對 OAuth 2.0 Token 交換 (RFC 8693) 授權流程的支持,允許客户端在保留用户身份的情況下交換令牌。 該功能使諸如冒用身份等場景得以實現,其中資源服務器可以作為客户端獲取新的令牌。 讓我們通過一個示例來詳細説明。
假設我們有一個名為 loan-service 的資源服務器,它提供各種貸款賬户的 API。 此服務已進行安全配置,客户端需要提供一個訪問令牌,該令牌必須具有 loan service 的 audience (aud 聲明)。
現在,假設 loan-service 需要調用另一個資源服務 loan-product-service,該服務公開了貸款產品的詳細信息。 loan-product-service 同樣已進行安全配置,並且需要具有 loan-product-service 的令牌作為 audience。
在這種情況下,資源服務器 loan-service 應該成為一個客户端,並交換現有的令牌以獲取 loan-product-service 的新令牌,同時保留原始令牌的身份。
Spring Security 6.3 提供了名為 TokenExchangeOAuth2AuthorizedClientProvider 的 OAuth2AuthorizedClientProvider 類的新實現,用於 token-exchange 授權流程。
6. 結論
在本文中,我們討論了 Spring Security 6.3 中引入的各種新功能。
值得注意的變更包括對授權框架的增強、對 JDK 序列化的支持以及對 OAuth 2.0 Token Exchange 的支持。