知識庫 / Spring / Spring Security RSS 訂閱

Spring 方法安全性入門

Spring Security
HongKong
5
02:06 PM · Dec 06 ,2025

1. 概述

簡單來説,Spring Security 支持在方法級別上的權限語義。

通常,我們可以通過限制特定角色執行特定方法的權限來安全地保護我們的服務層,並使用專門的方法級安全測試支持進行測試。

在本教程中,我們將回顧一些安全註解的使用。然後我們將重點關注使用不同的策略測試我們的方法安全。

2. 啓用方法安全

首先,要使用 Spring Method Security,我們需要添加 spring-security-config 依賴項:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
</dependency>

我們可以在 Maven Central 找到其最新版本:https://mvnrepository.com/artifact/org.springframework.security/spring-security-config

如果想要使用 Spring Boot,可以使用 spring-boot-starter-security 依賴,該依賴包含 spring-security-config

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

再次,最新版本可以在 Maven Central 找到。

接下來,我們需要啓用全局方法安全:

@Configuration
@EnableGlobalMethodSecurity(
  prePostEnabled = true, 
  securedEnabled = true, 
  jsr250Enabled = true)
public class MethodSecurityConfig 
  extends GlobalMethodSecurityConfiguration {
}
<ul>
 <li>The <em>prePostEnabled</em> 屬性啓用 Spring Security 的 pre/post 註解。</li>
 <li>The <em>securedEnabled</em> 屬性確定 <em>@Secured</em> 註解是否啓用。</li>
 <li>The <em>jsr250Enabled</em> 屬性允許我們使用 <em>@RoleAllowed</em> 註解。</li>
</ul>
<p>我們在下一部分將更深入地探討這些註解。</p>

3. Applying Method Security

This section describes how to apply method security to your application. Method security is a critical aspect of securing your application by controlling access to methods and preventing unauthorized access to sensitive data or functionality.

Key Concepts

  • Method Security Policies: These policies define the rules governing access to specific methods.
  • Access Control: Mechanisms that enforce the defined policies, determining which users or roles are permitted to execute a particular method.
  • Security Context: The identity of the user or process executing the method.
  • Least Privilege: Granting users and processes only the minimum necessary permissions to perform their tasks.

Implementing Method Security

There are several approaches to implementing method security:

  • Access Control Lists (ACLs): ACLs associate permissions with methods, specifying which users or roles have access.
  • Role-Based Access Control (RBAC): RBAC assigns permissions to roles, and users are assigned to roles.
  • Attribute-Based Access Control (ABAC): ABAC uses attributes of the user, resource, and environment to determine access rights.

Example Code Snippet (Java)

public class UserService {

    // Method to retrieve user details
    public UserDetails getUserDetails(String userId) {
        // Access control logic here - check user permissions
        // ...
        return new UserDetails();
    }

    // Method to update user profile
    public void updateProfile(String userId, String newProfileData) {
        // Access control logic here - check user permissions
        // ...
    }
}

Note: The code above demonstrates a basic example. In a real-world application, you would implement more sophisticated access control logic based on your specific security requirements. Consider using a framework or library to simplify the implementation.

3.1. 使用 @Secured 註解

@Secured 註解用於指定方法的角色列表。因此,如果用户擁有該方法中指定的一項或多項角色,則她才能訪問該方法。

讓我們定義一個 getUsername 方法:

@Secured("ROLE_VIEWER")
public String getUsername() {
    SecurityContext securityContext = SecurityContextHolder.getContext();
    return securityContext.getAuthentication().getName();
}

此處 @Secured(“ROLE_VIEWER”) 註解定義只有擁有 ROLE_VIEWER 角色的用户才能執行 getUsername 方法。

此外,我們可以在 @Secured 註解中定義角色的列表:

@Secured({ "ROLE_VIEWER", "ROLE_EDITOR" })
public boolean isValidUsername(String username) {
    return userRoleRepository.isValidUsername(username);
}

在這種情況下,配置表明如果用户具有ROLE_VIEWERROLE_EDITOR角色,則該用户可以調用isValidUsername方法。

@Secured註解不支持Spring Expression Language (SpEL)。

3.2. 使用 @RolesAllowed 註解

@RolesAllowed 註解是 JSR-250 中與 @Secured 註解等效的註解。

基本上,我們可以像使用 @Secured 註解一樣,使用 @RolesAllowed 註解。

通過這種方式,我們可以重新定義 getUsernameisValidUsername 方法:

@RolesAllowed("ROLE_VIEWER")
public String getUsername2() {
    //...
}
    
@RolesAllowed({ "ROLE_VIEWER", "ROLE_EDITOR" })
public boolean isValidUsername2(String username) {
    //...
}

同樣,只有擁有 ROLE_VIEWER 角色的用户才能執行 getUsername2

再次説明,用户只有在擁有至少一個 ROLE_VIEWERROLER_EDITOR 角色時,才能調用 isValidUsername2

3.3. 使用 @PreAuthorize@PostAuthorize 註解

@PreAuthorize 和 @PostAuthorize 註解都提供基於表達式的訪問控制。因此,可以使用 SpEL(Spring 表達式語言)編寫謂詞。

@PreAuthorize 註解在方法執行前檢查給定的表達式,而 @PostAuthorize 註解則在方法執行後驗證表達式,並可能修改結果。

下面聲明一個 getUsernameInUpperCase 方法:

@PreAuthorize("hasRole('ROLE_VIEWER')")
public String getUsernameInUpperCase() {
    return getUsername().toUpperCase();
}

@PreAuthorize(“hasRole(‘ROLE_VIEWER’)” 具有與@Secured(“ROLE_VIEWER”) 相同的作用,正如我們在上一部分中所使用的。 歡迎查閲之前的文章以瞭解更多安全表達式的細節。

因此,註解@Secured({“ROLE_VIEWER”,”ROLE_EDITOR”}) 可以被@PreAuthorize(“hasRole(‘ROLE_VIEWER’)hasRole(‘ROLE_EDITOR’)”) 替換:

@PreAuthorize("hasRole('ROLE_VIEWER') or hasRole('ROLE_EDITOR')")
public boolean isValidUsername3(String username) {
    //...
}

此外,我們還可以將方法參數作為表達式的一部分使用

@PreAuthorize("#username == authentication.principal.username")
public String getMyRoles(String username) {
    //...
}

用户只能調用 getMyRoles 方法,前提是參數 username 的值與當前 principal 的用户名相同。

需要注意的是,@PreAuthorize 表達式可以替換為 @PostAuthorize 表達式。

讓我們重寫 getMyRoles:

@PostAuthorize("#username == authentication.principal.username")
public String getMyRoles2(String username) {
    //...
}

在之前的示例中,但是授權會在目標方法執行後延遲。 此外,@PostAuthorize 標註提供訪問方法結果的能力:

@PostAuthorize
  ("returnObject.username == authentication.principal.nickName")
public CustomUser loadUserDetail(String username) {
    return userRoleRepository.loadUserByUserName(username);
}

在這裏,loadUserDetail 方法只有在返回的 CustomUserusername 等於當前認證 principals 的 nickname 時才會成功執行。

在本節中,我們主要使用簡單的 Spring 表達式。對於更復雜的場景,我們可以創建自定義安全表達式。

3.4. 使用 @PreFilter@PostFilter 註解

Spring Security 提供 @PreFilter 註解,用於在執行方法之前過濾集合參數

@PreFilter("filterObject != authentication.principal.username")
public String joinUsernames(List<String> usernames) {
    return usernames.stream().collect(Collectors.joining(";"));
}

在此示例中,我們正在將所有用户名連接起來,但不包括已認證的用户名。

在這裏,在我們的表達式中,我們使用 filterObject 屬性來表示當前集合中的對象。

但是,如果方法有多個集合類型的參數,則需要使用 filterTarget 屬性來指定要過濾的參數:

@PreFilter
  (value = "filterObject != authentication.principal.username",
  filterTarget = "usernames")
public String joinUsernamesAndRoles(
  List<String> usernames, List<String> roles) {
 
    return usernames.stream().collect(Collectors.joining(";")) 
      + ":" + roles.stream().collect(Collectors.joining(";"));
}

此外,我們還可以通過使用 @PostFilter 註解來過濾方法的返回集合:

@PostFilter("filterObject != authentication.principal.username")
public List<String> getAllUsernamesExceptCurrent() {
    return userRoleRepository.getAllUsernames();
}

在這種情況下,名稱 filterObject 指的是返回集合中的當前對象。

採用這種配置,Spring Security 將迭代返回的列表,並刪除與 principal 的用户名匹配的任何值。

我們的 Spring Security – @PreFilter 和 @PostFilter 文章詳細描述了這兩個註解。

3.5. 方法安全元註解

我們通常會遇到一種情況,即使用相同的安全配置來保護不同的方法。

在這種情況下,我們可以定義一個安全元註解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('VIEWER')")
public @interface IsViewer {
}

接下來,我們可以直接使用 @IsViewer 註解來保護我們的方法:

@IsViewer
public String getUsername4() {
    //...
}

安全元標註是一個很好的想法,因為它們增加了語義,並使我們的業務邏輯與安全框架解耦。

3.6. 類級別安全註解

如果我們在一個類中發現使用相同的安全註解對所有方法,我們應該考慮將該註解放在類級別。

@Service
@PreAuthorize("hasRole('ROLE_ADMIN')")
public class SystemService {

    public String getSystemYear(){
        //...
    }
 
    public String getSystemDate(){
        //...
    }
}

在上述示例中,安全規則 hasRole('ROLE_ADMIN') 將應用於 getSystemYeargetSystemDate 方法。

3.7. 方法上的多個安全註解

我們可以使用多個安全註解在一個方法上:

@PreAuthorize("#username == authentication.principal.username")
@PostAuthorize("returnObject.username == authentication.principal.nickName")
public CustomUser securedLoadUserDetail(String username) {
    return userRoleRepository.loadUserByUserName(username);
}

這樣一來,Spring 將在執行 securedLoadUserDetail 方法之前和之後都驗證授權。

4. 重要注意事項

關於方法安全,我們想強調以下兩點:

  • 默認情況下,Spring AOP 代理用於應用方法安全。如果一個受保護的方法 A 被同一類中另一個方法調用,則方法 A 中的安全檢查將被完全忽略。這意味着方法 A 將不會執行任何安全檢查。同樣,這也適用於私有方法。
  • Spring SecurityContext 是線程綁定的。默認情況下,安全上下文不會傳遞到子線程。有關更多信息,請參閲我們的 Spring Security Context Propagation 文章。

5. 測試安全方法

5.1. 配置

為了使用 JUnit 測試 Spring Security,我們需要 spring-security-test 依賴項。

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
</dependency>

我們不需要指定依賴的版本,因為我們使用的是 Spring Boot 插件。可以在 Maven Central 找到該依賴的最新版本。

接下來,讓我們通過指定運行器和 ApplicationContext 配置來配置一個簡單的 Spring Integration 測試:

@RunWith(SpringRunner.class)
@ContextConfiguration
public class MethodSecurityIntegrationTest {
    // ...
}

5.2. 測試用户名和角色

現在我們的配置已準備就緒,讓我們嘗試測試我們使用 getUsername 方法,並使用 @Secured(“ROLE_VIEWER”) 註解進行安全保護的方法:

@Secured("ROLE_VIEWER")
public String getUsername() {
    SecurityContext securityContext = SecurityContextHolder.getContext();
    return securityContext.getAuthentication().getName();
}

由於我們在此處使用了 @Secured 註解,因此需要一個已認證的用户才能調用該方法。否則,我們將收到 AuthenticationCredentialsNotFoundException 異常。

因此,我們需要提供一個用户來測試我們的安全方法。

為了實現這一點,我們使用 @WithMockUser 裝飾測試方法,並提供用户和角色:

@Test
@WithMockUser(username = "john", roles = { "VIEWER" })
public void givenRoleViewer_whenCallGetUsername_thenReturnUsername() {
    String userName = userRoleService.getUsername();
    
    assertEquals("john", userName);
}

我們已提供一個經過身份驗證的用户,其用户名是 john, 其角色是 ROLE_VIEWER。如果未指定 usernamerole, 則默認 usernameuser, 默認 roleROLE_USER

請注意,這裏不需要添加 ROLE_ 前綴,因為 Spring Security 會自動添加該前綴。

如果不想使用該前綴,可以考慮使用 authority 代替 role

例如,讓我們聲明一個 getUsernameInLowerCase 方法:

@PreAuthorize("hasAuthority('SYS_ADMIN')")
public String getUsernameLC(){
    return getUsername().toLowerCase();
}

我們可以使用權威數據進行測試。

@Test
@WithMockUser(username = "JOHN", authorities = { "SYS_ADMIN" })
public void givenAuthoritySysAdmin_whenCallGetUsernameLC_thenReturnUsername() {
    String username = userRoleService.getUsernameInLowerCase();

    assertEquals("john", username);
}

方便地,如果我們要使用同一個用户進行多個測試用例,可以在測試類中聲明 @WithMockUser 註解

@RunWith(SpringRunner.class)
@ContextConfiguration
@WithMockUser(username = "john", roles = { "VIEWER" })
public class MockUserAtClassLevelIntegrationTest {
    //...
}

如果我們想以匿名用户的身份運行我們的測試,我們可以使用 註解:

@Test(expected = AccessDeniedException.class)
@WithAnonymousUser
public void givenAnomynousUser_whenCallGetUsername_thenAccessDenied() {
    userRoleService.getUsername();
}

在上面的示例中,我們期望收到一個 AccessDeniedException,因為匿名用户未被授予 ROLE_VIEWER 角色或 SYS_ADMIN 權限。

5.3. 使用自定義 <em UserDetailsService 進行測試

對於大多數應用程序,通常使用自定義類作為身份驗證主體。在這種情況下,自定義類需要實現 接口。

在本文中,我們聲明一個名為 的類,該類繼承了 接口的現有實現,即

public class CustomUser extends User {
    private String nickName;
    // getter and setter
}

讓我們回顧一下在第3部分中使用的帶有@PostAuthorize註解的示例:

@PostAuthorize("returnObject.username == authentication.principal.nickName")
public CustomUser loadUserDetail(String username) {
    return userRoleRepository.loadUserByUserName(username);
}

在這種情況下,該方法只有在返回的 CustomUserusername 與當前認證主體的 nickname 相等時才會成功執行。

如果我們想測試該方法,我們可以提供一個 UserDetailsService 的實現,該實現可以根據用户名加載我們的 CustomUser

@Test
@WithUserDetails(
  value = "john", 
  userDetailsServiceBeanName = "userDetailService")
public void whenJohn_callLoadUserDetail_thenOK() {
 
    CustomUser user = userService.loadUserDetail("jane");

    assertEquals("jane", user.getNickName());
}

此處使用 @WithUserDetails 註解表明我們將使用 UserDetailsService 來初始化我們的已認證用户。服務通過 userDetailsServiceBeanName 屬性引用。 UserDetailsService 可能是實際實現或用於測試目的的假實現。

此外,服務將使用 value 屬性的值作為用户名來加載 UserDetails

方便地,我們也可以在類級別裝飾使用 @WithUserDetails 註解,類似於我們使用 @WithMockUser 註解時所做的那樣。

5.4. 使用元標註進行測試

我們經常在各種測試中反覆使用相同的用户/角色。

對於這些情況,創建元標註非常方便。

再次查看之前的示例 @WithMockUser(username=”john”, roles={“VIEWER”}),我們可以聲明一個元標註:

@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value = "john", roles = "VIEWER")
public @interface WithMockJohnViewer { }

然後我們只需在測試中使用 @WithMockJohnViewer

@Test
@WithMockJohnViewer
public void givenMockedJohnViewer_whenCallGetUsername_thenReturnUsername() {
    String userName = userRoleService.getUsername();

    assertEquals("john", userName);
}

同樣,我們也可以使用元標註來創建特定領域的用户,使用 @WithUserDetails

6. 結論

在本文中,我們探討了在 Spring Security 中使用 Method Security 的各種選項。

我們還研究了一些技術,可以輕鬆地測試 Method Security,並學習瞭如何在不同的測試中使用模擬用户。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.