Spring Security 6.3 新功能

banq發表於2024-07-16

Spring Security 6.3 版本在框架中引入了一系列安全增強功能。

在本教程中,我們將討論一些最顯著的功能,重點介紹它們的優點和用途。

被動 JDK 序列化支援
Spring Security 6.3 包含被動 JDK 序列化支援。然而,在進一步討論這個問題之前,讓我們先了解一下它的問題和相關問題。

Spring Security 序列化設計
在 6.3 版之前,Spring Security對透過 JDK 序列化在不同版本中序列化和反序列化其類有嚴格的策略。此限制是框架為確保安全性和穩定性而做出的刻意設計決定。這樣做的理由是防止使用不同版本的 Spring Security 反序列化在一個版本中序列化的物件時出現不相容和安全漏洞。

本設計的一個關鍵方面是整個 Spring Security 專案使用一個全域性的serialVersionUID。在 Java 中,序列化和反序列化過程使用唯一識別符號serialVersionUID來驗證載入的類是否與序列化物件完全對應。

透過維護每個 Spring Security 釋出版本獨有的全域性serialVersionUID  ,框架可確保一個版本的序列化物件不能使用另一個版本進行反序列化。 這種方法有效地建立了版本屏障,防止反序列化具有不匹配serialVersionUID值的物件。

例如, Spring Security 中的SecurityContextImpl類表示安全上下文資訊。此類的序列化版本包含特定於該版本的 serialVersionUID。當嘗試在不同版本的 Spring Security 中反序列化此物件時,serialVersionUID不匹配會阻止該過程成功。

序列化設計帶來的挑戰
在優先考慮增強安全性的同時,這種設計策略也帶來了一些挑戰。開發人員通常將 Spring Security 與其他 Spring 庫(如Spring Session)整合,以管理使用者登入會話。這些會話包含關鍵的使用者身份驗證和安全上下文資訊,通常透過 Spring Security 類實現。此外,為了最佳化使用者體驗並增強應用程式的可擴充套件性,開發人員通常會將這些會話資料儲存在各種持久儲存解決方案中,包括資料庫。

以下是由於序列化設計而產生的一些挑戰。如果 Spring Security 版本發生變化,透過 Canary 釋出流程升級應用程式可能會導致問題。在這種情況下,持久會話資訊無法反序列化,可能需要使用者重新登入。

另一個問題出現在使用 Spring Security 的遠端方法呼叫 (RMI) 的應用程式架構中。例如,如果客戶端應用程式在遠端方法呼叫中使用 Spring Security 類,則必須在客戶端序列化它們,並在另一端反序列化它們。如果兩個應用程式不共享相同的 Spring Security 版本,則此呼叫會失敗,從而導致InvalidClassException異常。

解決方法
解決此問題的典型方法如下。我們可以使用 JDK 序列化以外的其他序列化庫,例如 Jackson 序列化。這樣,我們就不用序列化 Spring Security 類了,而是獲取所需詳細資訊的 JSON 表示,然後使用 Jackson 對其進行序列化。

另一種選擇是擴充套件所需的 Spring Security 類,例如Authentication,並透過readObject和writeObject方法明確實現自定義序列化支援。

Spring Security 6.3 中的序列化變化
在 6.3 版本中,類序列化會與前一個次要版本進行相容性檢查。這確保升級到較新版本後可以無縫地反序列化 Spring Security 類。

授權
Spring Security 6.3 在 Spring Security 授權中引入了一些值得注意的變化。讓我們在本節中探討這些變化。

註釋引數
Spring Security 的方法安全支援元註釋。我們可以根據應用程式的用例採用註釋並提高其可讀性。例如,我們可以將 @PreAuthorize (“hasRole('USER')”)簡化為以下內容:

@Target({ ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize(<font>"hasRole('USER')")
public @interface IsUser {
    String[] value();
}

接下來我們就可以在業務程式碼中使用這個@IsUser註解了:

@Service
public class MessageService {
    @IsUser
    public Message readMessage() {
        return <font>"Message";
    }
}

假設我們有另一個角色ADMIN 。我們可以為該角色建立一個名為@IsAdmin的註釋。但是,這將是多餘的。將此元註釋用作模板並將角色作為註釋引數包含會更合適。Spring Security 6.3 引入了定義此類元註釋的功能。讓我們用一個具體的例子來演示這一點:

要模板化元註釋,首先我們需要定義一個 bean PrePostTemplateDefaults:

@Bean
PrePostTemplateDefaults prePostTemplateDefaults() {
    return new PrePostTemplateDefaults();
}

模板解析需要這個 bean 定義。

接下來,我們將為@PreAuthorize註釋定義一個元註釋@CustomHasAnyRole,它可以接受USER和ADMIN角色:

@Target({ ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize(<font>"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, <font>"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作為註釋引數。

確保返回值安全
Spring Security 6.3 中另一個強大的新功能是使用@AuthorizeReturnObject註釋保護域物件的能力。此增強功能透過對方法返回的物件啟用授權檢查來實現更細粒度的安全性,確保只有授權使用者才能訪問特定的域物件。

讓我們用一個例子來說明這一點。假設我們有以下帶有iban和balance欄位的Account類。要求只有具有讀取許可權的使用者才能檢索帳戶餘額。

public class Account {
    private String iban;
    private Double balance;
    <font>// Constructor<i>
    public String getIban() {
        return iban;
    }
    @PreAuthorize(
"hasAuthority('read')")
    public Double getBalance() {
        return balance;
    }
}

接下來,讓我們定義AccountService類,它返回一個帳戶例項:

@Service
public class AccountService {
    @AuthorizeReturnObject
    public Optional<Account> getAccountByIban(String iban) {
        return Optional.of(new Account(<font>"XX1234567809", 2345.6));
    }
}

在上面的程式碼片段中,我們使用了@AuthorizeReturnObject註釋。Spring Security 確保只有具有讀取許可權的使用者才能訪問Account例項。

錯誤處理
在上一節中,我們討論了使用@AuthorizeReturnObject註釋來保護域物件。一旦啟用,未經授權的訪問將導致AccessDeniedException。Spring Security 6.3 提供了MethodAuthorizationDeniedHandler介面來處理授權失敗。

讓我們用一個例子來說明這一點。讓我們擴充套件第 3.2 節中的示例,並使用讀取許可權保護 IBAN。但是,我們打算提供一個遮蔽值,而不是對任何未經授權的訪問返回AccessDeniedException 。

讓我們定義MethodAuthorizationDeniedHandler介面的實現:

@Component
public class MaskMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler  {
    @Override
    public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
        return <font>"****";
    }
}

在上面的程式碼片段中,如果存在AccessDeniedException ,我們將提供一個遮蔽值。此處理程式類可在getIban()方法中使用,如下所示:

@PreAuthorize(<font>"hasAuthority('read')")
@HandleAuthorizationDenied(handlerClass=MaskMethodAuthorizationDeniedHandler.class)
public String getIban() {
    return iban;
}

密碼檢查被破解
Spring Security 6.3 提供了一個用於檢查洩露密碼的實現。此實現根據洩露密碼資料庫 ( pwnedpasswords.com ) 檢查提供的密碼。因此,應用程式可以在註冊時驗證使用者提供的密碼。以下程式碼片段演示了用法。

首先定義一個HaveIBeenPwnedRestApiPasswordChecker類的bean定義:

@Bean
public HaveIBeenPwnedRestApiPasswordChecker passwordChecker() {
    return new HaveIBeenPwnedRestApiPasswordChecker();
}

接下來,使用此實現來檢查使用者提供的密碼:

@RestController
@RequestMapping(<font>"/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.");
    }
       
// ...<i>
        return
"User registered successfully";
    }
}

OAuth 2.0 令牌交換授權
Spring Security 6.3 還引入了對OAuth 2.0令牌交換 ( RFC 8693 ) 授權的支援,允許客戶端在保留使用者身份的同時交換令牌。此功能支援模擬等場景,其中資源伺服器可以充當客戶端來獲取新令牌。讓我們透過一個例子來詳細說明這一點。

假設我們有一個名為 loan-service 的資源伺服器,它為貸款賬戶提供各種 API。此服務是安全的,客戶端需要提供訪問令牌,該令牌必須具有貸款服務的受眾(aud 宣告)。

現在讓我們假設 loan-service 需要呼叫另一個資源服務loan-product-service,該服務公開貸款產品的詳細資訊。 loan-product-service 也是安全的,並且需要具有loan-product-service受眾的令牌。由於這兩個服務的受眾不同,因此 loan 服務的令牌不能用於loan-product-service。

在這種情況下,資源伺服器 loan-service 應該成為客戶端,並將現有令牌交換為保留原始令牌身份的 loan-product-service 的新令牌。

Spring Security 6.3為令牌交換授權提供了OAuth2AuthorizedClientProvider類的新實現,名為TokenExchangeOAuthorizedClientProvider 。

結論
在本文中,我們討論了 Spring Security 6.3 中引入的各種新功能。

顯著的變化是授權框架的增強、被動 JDK 序列化支援和 OAuth 2.0 令牌交換支援。
 

相關文章