方法安全性

除了在請求層級建立授權模型之外,Spring Security 也支援在方法層級建立模型。

您可以在應用程式中透過使用 @EnableMethodSecurity 註解任何 @Configuration 類別,或將 <method-security> 新增至任何 XML 組態檔來啟用它,如下所示

  • Java

  • Kotlin

  • Xml

@EnableMethodSecurity
@EnableMethodSecurity
<sec:method-security/>

然後,您可以立即使用 @PreAuthorize@PostAuthorize@PreFilter@PostFilter 註解任何 Spring 管理的類別或方法,以授權方法調用,包括輸入參數和回傳值。

Spring Boot Starter Security 預設不會啟用方法層級的授權。

方法安全性也支援許多其他用例,包括 AspectJ 支援自訂註解 和數個組態點。請考慮了解以下用例

方法安全性的運作方式

Spring Security 的方法授權支援對於以下情況非常方便

  • 提取細粒度的授權邏輯;例如,當方法參數和回傳值有助於授權決策時。

  • 在服務層強制執行安全性

  • 風格上偏好基於註解而非基於 HttpSecurity 的組態

由於方法安全性是使用 Spring AOP 建構的,因此您可以存取其所有表達能力,以便在需要時覆寫 Spring Security 的預設值。

如前所述,首先將 @EnableMethodSecurity 新增至 @Configuration 類別,或在 Spring XML 組態檔中新增 <sec:method-security/>

此註解和 XML 元素取代了 @EnableGlobalMethodSecurity<sec:global-method-security/>。它們提供了以下改進

  1. 使用簡化的 AuthorizationManager API 而非 metadata sources、config attributes、decision managers 和 voters。這簡化了重用和自訂。

  2. 偏好直接基於 bean 的組態,而不是需要擴展 GlobalMethodSecurityConfiguration 來自訂 bean

  3. 使用原生 Spring AOP 建構,移除抽象概念,並允許您使用 Spring AOP 建構區塊進行自訂

  4. 檢查是否有衝突的註解,以確保明確的安全性組態

  5. 符合 JSR-250

  6. 預設啟用 @PreAuthorize@PostAuthorize@PreFilter@PostFilter

如果您正在使用 @EnableGlobalMethodSecurity<global-method-security/>,這些現在已棄用,建議您遷移。

方法授權是方法前和方法後授權的組合。考慮以以下方式註解的服務 bean

  • Java

  • Kotlin

@Service
public class MyCustomerService {
    @PreAuthorize("hasAuthority('permission:read')")
    @PostAuthorize("returnObject.owner == authentication.name")
    public Customer readCustomer(String id) { ... }
}
@Service
open class MyCustomerService {
    @PreAuthorize("hasAuthority('permission:read')")
    @PostAuthorize("returnObject.owner == authentication.name")
    fun readCustomer(val id: String): Customer { ... }
}

當方法安全性啟用時,對 MyCustomerService#readCustomer 的給定調用可能如下所示

methodsecurity
  1. Spring AOP 為 readCustomer 調用其代理方法。在代理的其他 advisors 中,它調用與 @PreAuthorize 切入點 匹配的 AuthorizationManagerBeforeMethodInterceptor

  2. 攔截器調用 PreAuthorizeAuthorizationManager#check

  3. 授權管理器使用 MethodSecurityExpressionHandler 來解析註解的 SpEL 表達式,並從包含 Supplier<Authentication>MethodInvocationMethodSecurityExpressionRoot 建構對應的 EvaluationContext

  4. 攔截器使用此 context 來評估表達式;具體而言,它從 Supplier 讀取 Authentication,並檢查其 授權 集合中是否具有 permission:read

  5. 如果評估通過,則 Spring AOP 繼續調用該方法。

  6. 如果沒有通過,攔截器會發布 AuthorizationDeniedEvent 並拋出 AccessDeniedExceptionExceptionTranslationFilter 會捕獲該異常,並向回應回傳 403 狀態碼

  7. 方法回傳後,Spring AOP 會調用與 @PostAuthorize 切入點 匹配的 AuthorizationManagerAfterMethodInterceptor,其運作方式與上述相同,但使用 PostAuthorizeAuthorizationManager

  8. 如果評估通過(在本例中,回傳值屬於已登入的使用者),則處理程序會正常繼續

  9. 如果沒有通過,攔截器會發布 AuthorizationDeniedEvent 並拋出 AccessDeniedExceptionExceptionTranslationFilter 會捕獲該異常,並向回應回傳 403 狀態碼

如果方法不是在 HTTP 請求的上下文中調用,您可能需要自行處理 AccessDeniedException

多個註解依序計算

如上所示,如果方法調用涉及多個 方法安全性註解,則每個註解都會一次處理一個。這表示它們可以統稱為「與」的關係。換句話說,為了授權調用,所有註解檢查都需要通過授權。

不支援重複的註解

也就是說,不支援在同一個方法上重複相同的註解。例如,您不能在同一個方法上放置兩次 @PreAuthorize

請改為使用 SpEL 的布林值支援或其委派給個別 bean 的支援。

每個註解都有自己的切入點

每個註解都有自己的切入點實例,該實例在整個物件階層中尋找該註解或其 meta-annotation 對應項,從 方法及其封閉類別 開始。

您可以在 AuthorizationMethodPointcuts 中查看此項目的具體細節。

每個註解都有自己的方法攔截器

每個註解都有自己的專用方法攔截器。這樣做的原因是為了使事物更具可組合性。例如,如果需要,您可以停用 Spring Security 的預設值,並僅發布 @PostAuthorize 方法攔截器

方法攔截器如下

一般來說,您可以將以下清單視為當您新增 @EnableMethodSecurity 時 Spring Security 發布的攔截器的代表

  • Java

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor preAuthorizeMethodInterceptor() {
    return AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor postAuthorizeMethodInterceptor() {
    return AuthorizationManagerAfterMethodInterceptor.postAuthorize();
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor preFilterMethodInterceptor() {
    return AuthorizationManagerBeforeMethodInterceptor.preFilter();
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor postFilterMethodInterceptor() {
    return AuthorizationManagerAfterMethodInterceptor.postFilter();
}

偏好授與權限而非複雜的 SpEL 表達式

通常很容易引入複雜的 SpEL 表達式,例如以下

  • Java

@PreAuthorize("hasAuthority('permission:read') || hasRole('ADMIN')")
Kotlin
@PreAuthorize("hasAuthority('permission:read') || hasRole('ADMIN')")

但是,您可以改為將 permission:read 授與給具有 ROLE_ADMIN 的使用者。一種方法是使用 RoleHierarchy,如下所示

  • Java

  • Kotlin

  • Xml

@Bean
static RoleHierarchy roleHierarchy() {
    return RoleHierarchyImpl.fromHierarchy("ROLE_ADMIN > permission:read");
}
companion object {
    @Bean
    fun roleHierarchy(): RoleHierarchy {
        return RoleHierarchyImpl.fromHierarchy("ROLE_ADMIN > permission:read")
    }
}
<bean id="roleHierarchy"
        class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl" factory-method="fromHierarchy">
    <constructor-arg value="ROLE_ADMIN > permission:read"/>
</bean>

然後MethodSecurityExpressionHandler 實例中設定它。然後,這允許您使用更簡單的 @PreAuthorize 表達式,例如以下這個

  • Java

  • Kotlin

@PreAuthorize("hasAuthority('permission:read')")
@PreAuthorize("hasAuthority('permission:read')")

或者,在可能的情況下,將特定於應用程式的授權邏輯改編為登入時授與的權限。

比較請求層級與方法層級授權

何時應該偏好方法層級授權而不是請求層級授權?其中一些取決於個人喜好;但是,請考慮以下每個優勢列表,以幫助您做出決定。

請求層級

方法層級

授權類型

粗粒度

細粒度

組態位置

在組態類別中宣告

方法宣告的本地

組態樣式

DSL

註解

授權定義

程式化

SpEL

主要的權衡似乎是您希望授權規則存在的位置。

重要的是要記住,當您使用基於註解的方法安全性時,未註解的方法不受保護。為了防止這種情況,請在您的 HttpSecurity 實例中宣告 全部捕獲授權規則

使用註解授權

Spring Security 啟用方法層級授權支援的主要方式是透過您可以新增至方法、類別和介面的註解。

使用 @PreAuthorize 授權方法調用

方法安全性已啟用時,您可以使用 @PreAuthorize 註解方法,如下所示

  • Java

  • Kotlin

@Component
public class BankService {
	@PreAuthorize("hasRole('ADMIN')")
	public Account readAccount(Long id) {
        // ... is only invoked if the `Authentication` has the `ROLE_ADMIN` authority
	}
}
@Component
open class BankService {
	@PreAuthorize("hasRole('ADMIN')")
	fun readAccount(val id: Long): Account {
        // ... is only invoked if the `Authentication` has the `ROLE_ADMIN` authority
	}
}

這表示只有在提供的表達式 hasRole('ADMIN') 通過時,才能調用該方法。

然後,您可以測試該類別,以確認它正在強制執行授權規則,如下所示

  • Java

  • Kotlin

@Autowired
BankService bankService;

@WithMockUser(roles="ADMIN")
@Test
void readAccountWithAdminRoleThenInvokes() {
    Account account = this.bankService.readAccount("12345678");
    // ... assertions
}

@WithMockUser(roles="WRONG")
@Test
void readAccountWithWrongRoleThenAccessDenied() {
    assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(
        () -> this.bankService.readAccount("12345678"));
}
@WithMockUser(roles="ADMIN")
@Test
fun readAccountWithAdminRoleThenInvokes() {
    val account: Account = this.bankService.readAccount("12345678")
    // ... assertions
}

@WithMockUser(roles="WRONG")
@Test
fun readAccountWithWrongRoleThenAccessDenied() {
    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
        this.bankService.readAccount("12345678")
    }
}
@PreAuthorize 也可以是 meta-annotation,定義在 類別或介面層級,並使用 SpEL 授權表達式

雖然 @PreAuthorize 對於宣告所需的權限非常有幫助,但它也可以用於評估更複雜的涉及方法參數的表達式

使用 @PostAuthorize 授權方法結果

當方法安全性已啟用時,您可以使用 @PostAuthorize 註解方法,如下所示

  • Java

  • Kotlin

@Component
public class BankService {
	@PostAuthorize("returnObject.owner == authentication.name")
	public Account readAccount(Long id) {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}
@Component
open class BankService {
	@PostAuthorize("returnObject.owner == authentication.name")
	fun readAccount(val id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}

這表示只有在提供的表達式 returnObject.owner == authentication.name 通過時,方法才能回傳值。returnObject 代表要回傳的 Account 物件。

然後,您可以測試該類別,以確認它正在強制執行授權規則

  • Java

  • Kotlin

@Autowired
BankService bankService;

@WithMockUser(username="owner")
@Test
void readAccountWhenOwnedThenReturns() {
    Account account = this.bankService.readAccount("12345678");
    // ... assertions
}

@WithMockUser(username="wrong")
@Test
void readAccountWhenNotOwnedThenAccessDenied() {
    assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(
        () -> this.bankService.readAccount("12345678"));
}
@WithMockUser(username="owner")
@Test
fun readAccountWhenOwnedThenReturns() {
    val account: Account = this.bankService.readAccount("12345678")
    // ... assertions
}

@WithMockUser(username="wrong")
@Test
fun readAccountWhenNotOwnedThenAccessDenied() {
    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
        this.bankService.readAccount("12345678")
    }
}
@PostAuthorize 也可以是 meta-annotation,定義在 類別或介面層級,並使用 SpEL 授權表達式

@PostAuthorize 在防禦 不安全的直接物件參考 時特別有用。實際上,它可以定義為 meta-annotation,如下所示

  • Java

  • Kotlin

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PostAuthorize("returnObject.owner == authentication.name")
public @interface RequireOwnership {}
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PostAuthorize("returnObject.owner == authentication.name")
annotation class RequireOwnership

允許您改為以下列方式註解服務

  • Java

  • Kotlin

@Component
public class BankService {
	@RequireOwnership
	public Account readAccount(Long id) {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}
@Component
open class BankService {
	@RequireOwnership
	fun readAccount(val id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}

結果是,只有當上述方法的 owner 屬性與已登入使用者的 name 匹配時,才會回傳 Account。否則,Spring Security 將拋出 AccessDeniedException 並回傳 403 狀態碼。

使用 @PreFilter 過濾方法參數

@PreFilter 尚不支援 Kotlin 特定的資料類型;因此,僅顯示 Java 代码片段

當方法安全性已啟用時,您可以使用 @PreFilter 註解方法,如下所示

  • Java

@Component
public class BankService {
	@PreFilter("filterObject.owner == authentication.name")
	public Collection<Account> updateAccounts(Account... accounts) {
        // ... `accounts` will only contain the accounts owned by the logged-in user
        return updated;
	}
}

這表示從 accounts 中過濾掉表達式 filterObject.owner == authentication.name 失敗的任何值。filterObject 代表 accounts 中的每個 account,並用於測試每個 account

然後,您可以以下列方式測試類別,以確認它正在強制執行授權規則

  • Java

@Autowired
BankService bankService;

@WithMockUser(username="owner")
@Test
void updateAccountsWhenOwnedThenReturns() {
    Account ownedBy = ...
    Account notOwnedBy = ...
    Collection<Account> updated = this.bankService.updateAccounts(ownedBy, notOwnedBy);
    assertThat(updated).containsOnly(ownedBy);
}
@PreFilter 也可以是 meta-annotation,定義在 類別或介面層級,並使用 SpEL 授權表達式

@PreFilter 支援陣列、集合、映射和串流(只要串流仍然開啟)。

例如,上述 updateAccounts 宣告的功能與以下其他四個相同

  • Java

@PreFilter("filterObject.owner == authentication.name")
public Collection<Account> updateAccounts(Account[] accounts)

@PreFilter("filterObject.owner == authentication.name")
public Collection<Account> updateAccounts(Collection<Account> accounts)

@PreFilter("filterObject.value.owner == authentication.name")
public Collection<Account> updateAccounts(Map<String, Account> accounts)

@PreFilter("filterObject.owner == authentication.name")
public Collection<Account> updateAccounts(Stream<Account> accounts)

結果是,上述方法將僅具有 Account 實例,其中其 owner 屬性與已登入使用者的 name 匹配。

使用 @PostFilter 過濾方法結果

@PostFilter 尚不支援 Kotlin 特定的資料類型;因此,僅顯示 Java 代码片段

當方法安全性已啟用時,您可以使用 @PostFilter 註解方法,如下所示

  • Java

@Component
public class BankService {
	@PostFilter("filterObject.owner == authentication.name")
	public Collection<Account> readAccounts(String... ids) {
        // ... the return value will be filtered to only contain the accounts owned by the logged-in user
        return accounts;
	}
}

這表示從回傳值中過濾掉表達式 filterObject.owner == authentication.name 失敗的任何值。filterObject 代表 accounts 中的每個 account,並用於測試每個 account

然後,您可以如下所示測試類別,以確認它正在強制執行授權規則

  • Java

@Autowired
BankService bankService;

@WithMockUser(username="owner")
@Test
void readAccountsWhenOwnedThenReturns() {
    Collection<Account> accounts = this.bankService.updateAccounts("owner", "not-owner");
    assertThat(accounts).hasSize(1);
    assertThat(accounts.get(0).getOwner()).isEqualTo("owner");
}
@PostFilter 也可以是 meta-annotation,定義在 類別或介面層級,並使用 SpEL 授權表達式

@PostFilter 支援陣列、集合、映射和串流(只要串流仍然開啟)。

例如,上述 readAccounts 宣告的功能與以下其他三個相同

@PostFilter("filterObject.owner == authentication.name")
public Account[] readAccounts(String... ids)

@PostFilter("filterObject.value.owner == authentication.name")
public Map<String, Account> readAccounts(String... ids)

@PostFilter("filterObject.owner == authentication.name")
public Stream<Account> readAccounts(String... ids)

結果是,上述方法將回傳 Account 實例,其中其 owner 屬性與已登入使用者的 name 匹配。

記憶體內過濾顯然可能很昂貴,因此請考慮是否最好在資料層中過濾資料

使用 @Secured 授權方法調用

@Secured 是用於授權調用的舊版選項。@PreAuthorize 取代了它,建議改用它。

若要使用 @Secured 註解,您應該先變更方法安全性宣告以啟用它,如下所示

  • Java

  • Kotlin

  • Xml

@EnableMethodSecurity(securedEnabled = true)
@EnableMethodSecurity(securedEnabled = true)
<sec:method-security secured-enabled="true"/>

這將導致 Spring Security 發布對應的方法攔截器,該攔截器授權使用 @Secured 註解的方法、類別和介面。

使用 JSR-250 註解授權方法調用

如果您想要使用 JSR-250 註解,Spring Security 也支援它。@PreAuthorize 具有更強大的表達能力,因此建議使用。

若要使用 JSR-250 註解,您應該先變更方法安全性宣告以啟用它們,如下所示

  • Java

  • Kotlin

  • Xml

@EnableMethodSecurity(jsr250Enabled = true)
@EnableMethodSecurity(jsr250Enabled = true)
<sec:method-security jsr250-enabled="true"/>

這將導致 Spring Security 發布對應的方法攔截器,該攔截器授權使用 @RolesAllowed@PermitAll@DenyAll 註解的方法、類別和介面。

在類別或介面層級宣告註解

也支援在類別和介面層級使用方法安全性註解。

如果在類別層級,如下所示

  • Java

  • Kotlin

@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
public class MyController {
    @GetMapping("/endpoint")
    public String endpoint() { ... }
}
@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
open class MyController {
    @GetMapping("/endpoint")
    fun endpoint(): String { ... }
}

則所有方法都會繼承類別層級的行為。

或者,如果像以下這樣在類別和方法層級都宣告了它

  • Java

  • Kotlin

@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
public class MyController {
    @GetMapping("/endpoint")
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public String endpoint() { ... }
}
@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
open class MyController {
    @GetMapping("/endpoint")
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    fun endpoint(): String { ... }
}

則宣告註解的方法會覆寫類別層級的註解。

介面也是如此,但如果類別從兩個不同的介面繼承了註解,則啟動將會失敗。這是因為 Spring Security 無法判斷您想要使用哪一個。

在這種情況下,您可以透過將註解新增至具體方法來解決歧義。

使用 Meta Annotations

方法安全性支援 meta annotations。這表示您可以採用任何註解,並根據特定於應用程式的用例來提高可讀性。

例如,您可以將 @PreAuthorize("hasRole('ADMIN')") 簡化為 @IsAdmin,如下所示

  • Java

  • Kotlin

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
public @interface IsAdmin {}
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
annotation class IsAdmin

結果是,在受保護的方法上,您現在可以改為執行以下操作

  • Java

  • Kotlin

@Component
public class BankService {
	@IsAdmin
	public Account readAccount(Long id) {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}
@Component
open class BankService {
	@IsAdmin
	fun readAccount(val id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}

這會產生更易讀的方法定義。

範本化 Meta-Annotation 表達式

您也可以選擇使用 meta-annotation 範本,這允許更強大的註解定義。

首先,發布以下 bean

  • Java

  • Kotlin

@Bean
static PrePostTemplateDefaults prePostTemplateDefaults() {
	return new PrePostTemplateDefaults();
}
companion object {
    @Bean
    fun prePostTemplateDefaults(): PrePostTemplateDefaults {
        return PrePostTemplateDefaults()
    }
}

現在,您可以建立更強大的 @HasRole,而不是 @IsAdmin,如下所示

  • Java

  • Kotlin

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('{value}')")
public @interface HasRole {
	String value();
}
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('{value}')")
annotation class HasRole(val value: String)

結果是,在受保護的方法上,您現在可以改為執行以下操作

  • Java

  • Kotlin

@Component
public class BankService {
	@HasRole("ADMIN")
	public Account readAccount(Long id) {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}
@Component
open class BankService {
	@HasRole("ADMIN")
	fun readAccount(val id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}

請注意,這也適用於方法變數和所有註解類型,但您需要小心正確處理引號,以使產生的 SpEL 表達式正確。

例如,考慮以下 @HasAnyRole 註解

  • Java

  • Kotlin

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole({roles})")
public @interface HasAnyRole {
	String[] roles();
}
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole({roles})")
annotation class HasAnyRole(val roles: Array<String>)

在這種情況下,您會注意到您不應在表達式中使用引號,而應在參數值中使用引號,如下所示

  • Java

  • Kotlin

@Component
public class BankService {
	@HasAnyRole(roles = { "'USER'", "'ADMIN'" })
	public Account readAccount(Long id) {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}
@Component
open class BankService {
	@HasAnyRole(roles = arrayOf("'USER'", "'ADMIN'"))
	fun readAccount(val id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}

以便在替換後,表達式變為 @PreAuthorize("hasAnyRole('USER', 'ADMIN')")

啟用特定註解

您可以關閉 @EnableMethodSecurity 的預先組態,並將其替換為您自己的組態。如果您想要自訂 AuthorizationManagerPointcut,您可以選擇這樣做。或者,您可能只想僅啟用特定註解,例如 @PostAuthorize

您可以透過以下方式執行此操作

僅 @PostAuthorize 組態
  • Java

  • Kotlin

  • Xml

@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	Advisor postAuthorize() {
		return AuthorizationManagerAfterMethodInterceptor.postAuthorize();
	}
}
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	fun postAuthorize() : Advisor {
		return AuthorizationManagerAfterMethodInterceptor.postAuthorize()
	}
}
<sec:method-security pre-post-enabled="false"/>

<aop:config/>

<bean id="postAuthorize"
	class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor"
	factory-method="postAuthorize"/>

上面的程式碼片段透過首先停用方法安全性的預先組態,然後發布@PostAuthorize 攔截器本身來實現此目的。

使用 <intercept-methods> 授權

雖然使用 Spring Security 的基於註解的支援是方法安全性的首選方式,您也可以使用 XML 來宣告 Bean 授權規則。

如果您需要在 XML 組態中宣告,則可以使用<intercept-methods>,如下所示

  • Xml

<bean class="org.mycompany.MyController">
    <intercept-methods>
        <protect method="get*" access="hasAuthority('read')"/>
        <protect method="*" access="hasAuthority('write')"/>
    </intercept-methods>
</bean>
這僅支援依前綴或名稱比對方法。如果您的需求比這更複雜,請改用註解支援

以程式設計方式授權方法

如您所見,有幾種方法可以使用方法安全性 SpEL 運算式來指定非簡單的授權規則。

您可以透過多種方式讓您的邏輯以 Java 為基礎,而不是以 SpEL 為基礎。這讓您可以使用完整的 Java 語言,以提高可測試性和流程控制。

在 SpEL 中使用自訂 Bean

以程式設計方式授權方法的第一種方法是一個兩步驟的過程。

首先,宣告一個 Bean,其中有一個方法接受 MethodSecurityExpressionOperations 實例,如下所示

  • Java

  • Kotlin

@Component("authz")
public class AuthorizationLogic {
    public boolean decide(MethodSecurityExpressionOperations operations) {
        // ... authorization logic
    }
}
@Component("authz")
open class AuthorizationLogic {
    fun decide(val operations: MethodSecurityExpressionOperations): boolean {
        // ... authorization logic
    }
}

然後,以下列方式在您的註解中參考該 Bean

  • Java

  • Kotlin

@Controller
public class MyController {
    @PreAuthorize("@authz.decide(#root)")
    @GetMapping("/endpoint")
    public String endpoint() {
        // ...
    }
}
@Controller
open class MyController {
    @PreAuthorize("@authz.decide(#root)")
    @GetMapping("/endpoint")
    fun String endpoint() {
        // ...
    }
}

Spring Security 將針對每個方法調用,在該 Bean 上調用給定的方法。

這樣做的好處是,您所有的授權邏輯都在一個單獨的類別中,可以獨立進行單元測試並驗證其正確性。它還可以存取完整的 Java 語言。

除了傳回 Boolean 之外,您還可以傳回 null 以表示程式碼放棄做出決定。

如果您想包含有關決策性質的更多資訊,您可以改為傳回自訂的 AuthorizationDecision,如下所示

  • Java

  • Kotlin

@Component("authz")
public class AuthorizationLogic {
    public AuthorizationDecision decide(MethodSecurityExpressionOperations operations) {
        // ... authorization logic
        return new MyAuthorizationDecision(false, details);
    }
}
@Component("authz")
open class AuthorizationLogic {
    fun decide(val operations: MethodSecurityExpressionOperations): AuthorizationDecision {
        // ... authorization logic
        return MyAuthorizationDecision(false, details)
    }
}

或擲回自訂的 AuthorizationDeniedException 實例。但是請注意,建議傳回物件,因為這樣不會產生堆疊追蹤的開銷。

然後,您可以在自訂授權結果的處理方式時存取自訂詳細資訊。

使用自訂授權管理員

以程式設計方式授權方法的第二種方法是建立自訂的 AuthorizationManager

首先,宣告一個授權管理員實例,可能像這樣

  • Java

  • Kotlin

@Component
public class MyAuthorizationManager implements AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
        // ... authorization logic
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocationResult invocation) {
        // ... authorization logic
    }
}
@Component
class MyAuthorizationManager : AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
    override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocation): AuthorizationDecision {
        // ... authorization logic
    }

    override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocationResult): AuthorizationDecision {
        // ... authorization logic
    }
}

然後,發布方法攔截器,其切入點對應於您希望 AuthorizationManager 執行的時間。例如,您可以取代 @PreAuthorize@PostAuthorize 的運作方式,如下所示

僅限 @PreAuthorize 和 @PostAuthorize 組態
  • Java

  • Kotlin

  • Xml

@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
    @Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	Advisor preAuthorize(MyAuthorizationManager manager) {
		return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	Advisor postAuthorize(MyAuthorizationManager manager) {
		return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager);
	}
}
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
   	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	fun preAuthorize(val manager: MyAuthorizationManager) : Advisor {
		return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager)
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	fun postAuthorize(val manager: MyAuthorizationManager) : Advisor {
		return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager)
	}
}
<sec:method-security pre-post-enabled="false"/>

<aop:config/>

<bean id="preAuthorize"
	class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor"
	factory-method="preAuthorize">
    <constructor-arg ref="myAuthorizationManager"/>
</bean>

<bean id="postAuthorize"
	class="org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor"
	factory-method="postAuthorize">
    <constructor-arg ref="myAuthorizationManager"/>
</bean>

您可以使用 AuthorizationInterceptorsOrder 中指定的順序常數,將您的攔截器放置在 Spring Security 方法攔截器之間。

自訂運算式處理

或者,第三,您可以自訂每個 SpEL 運算式的處理方式。若要執行此操作,您可以公開自訂的 MethodSecurityExpressionHandler,如下所示

自訂 MethodSecurityExpressionHandler
  • Java

  • Kotlin

  • Xml

@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
	DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
	handler.setRoleHierarchy(roleHierarchy);
	return handler;
}
companion object {
	@Bean
	fun methodSecurityExpressionHandler(val roleHierarchy: RoleHierarchy) : MethodSecurityExpressionHandler {
		val handler = DefaultMethodSecurityExpressionHandler()
		handler.setRoleHierarchy(roleHierarchy)
		return handler
	}
}
<sec:method-security>
	<sec:expression-handler ref="myExpressionHandler"/>
</sec:method-security>

<bean id="myExpressionHandler"
		class="org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler">
	<property name="roleHierarchy" ref="roleHierarchy"/>
</bean>

我們使用 static 方法公開 MethodSecurityExpressionHandler,以確保 Spring 在初始化 Spring Security 的方法安全性 @Configuration 類別之前發布它

您也可以子類別化 DefaultMessageSecurityExpressionHandler,以在預設值之外新增您自己的自訂授權運算式。

使用 AspectJ 授權

使用自訂切入點比對方法

由於建置在 Spring AOP 之上,您可以宣告與註解無關的模式,類似於請求層級授權。這具有集中方法層級授權規則的潛在優勢。

例如,您可以使用發布您自己的 Advisor,或使用<protect-pointcut>,將 AOP 運算式比對到您服務層的授權規則,如下所示

  • Java

  • Kotlin

  • Xml

import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor protectServicePointcut() {
    AspectJExpressionPointcut pattern = new AspectJExpressionPointcut()
    pattern.setExpression("execution(* com.mycompany.*Service.*(..))")
    return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER"))
}
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole

companion object {
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    fun protectServicePointcut(): Advisor {
        val pattern = AspectJExpressionPointcut()
        pattern.setExpression("execution(* com.mycompany.*Service.*(..))")
        return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER"))
    }
}
<sec:method-security>
    <protect-pointcut expression="execution(* com.mycompany.*Service.*(..))" access="hasRole('USER')"/>
</sec:method-security>

與 AspectJ 位元組碼織入整合

有時,效能可以透過使用 AspectJ 將 Spring Security advice 織入 Bean 的位元組碼來增強。

設定 AspectJ 後,您可以非常簡單地在 @EnableMethodSecurity 註解或 <method-security> 元素中宣告您正在使用 AspectJ

  • Java

  • Kotlin

  • Xml

@EnableMethodSecurity(mode=AdviceMode.ASPECTJ)
@EnableMethodSecurity(mode=AdviceMode.ASPECTJ)
<sec:method-security mode="aspectj"/>

結果將是 Spring Security 將其 advisors 發布為 AspectJ advice,以便可以相應地織入它們。

指定順序

如先前所述,每個註解都有一個 Spring AOP 方法攔截器,並且每個攔截器在 Spring AOP advisor 鏈中都有一個位置。

也就是說,@PreFilter 方法攔截器的順序為 100,@PreAuthorize 的順序為 200,依此類推。

需要注意這一點的原因是,還有其他基於 AOP 的註解,例如 @EnableTransactionManagement,其順序為 Integer.MAX_VALUE。換句話說,預設情況下,它們位於 advisor 鏈的末端。

有時,讓其他 advice 在 Spring Security 之前執行可能很有價值。例如,如果您有一個使用 @Transactional@PostAuthorize 註解的方法,您可能希望在 @PostAuthorize 執行時交易仍然處於開啟狀態,以便 AccessDeniedException 會導致回滾。

若要讓 @EnableTransactionManagement 在方法授權 advice 執行之前開啟交易,您可以設定 @EnableTransactionManagement 的順序,如下所示

  • Java

  • Kotlin

  • Xml

@EnableTransactionManagement(order = 0)
@EnableTransactionManagement(order = 0)
<tx:annotation-driven ref="txManager" order="0"/>

由於最早的方法攔截器 (@PreFilter) 設定為順序 100,因此設定為零表示交易 advice 將在所有 Spring Security advice 之前執行。

使用 SpEL 表示授權

您已經看過幾個使用 SpEL 的範例,因此現在讓我們更深入地介紹 API。

Spring Security 將其所有授權欄位和方法封裝在一組根物件中。最通用的根物件稱為 SecurityExpressionRoot,它是 MethodSecurityExpressionRoot 的基礎。Spring Security 在準備評估授權運算式時,會將此根物件提供給 MethodSecurityEvaluationContext

使用授權運算式欄位和方法

它提供的第一件事是為您的 SpEL 運算式增強一組授權欄位和方法。以下是常見方法的快速概觀

  • permitAll - 方法調用不需要授權;請注意,在這種情況下,Authentication 永遠不會從會話中擷取

  • denyAll - 在任何情況下都不允許該方法;請注意,在這種情況下,Authentication 永遠不會從會話中擷取

  • hasAuthority - 方法要求 Authentication 具有與給定值比對的 GrantedAuthority

  • hasRole - hasAuthority 的捷徑,會加上前綴 ROLE_ 或任何組態為預設前綴的值

  • hasAnyAuthority - 方法要求 Authentication 具有與任何給定值比對的 GrantedAuthority

  • hasAnyRole - hasAnyAuthority 的捷徑,會加上前綴 ROLE_ 或任何組態為預設前綴的值

  • hasPermission - PermissionEvaluator 實例的掛鉤,用於執行物件層級授權

以下簡要介紹最常見的欄位

  • authentication - 與此方法調用關聯的 Authentication 實例

  • principal - 與此方法調用關聯的 Authentication#getPrincipal

現在您已經了解了模式、規則以及如何將它們配對在一起,您應該能夠理解這個更複雜的範例中發生了什麼

授權請求
  • Java

  • Kotlin

  • Xml

@Component
public class MyService {
    @PreAuthorize("denyAll") (1)
    MyResource myDeprecatedMethod(...);

    @PreAuthorize("hasRole('ADMIN')") (2)
    MyResource writeResource(...)

    @PreAuthorize("hasAuthority('db') and hasRole('ADMIN')") (3)
    MyResource deleteResource(...)

    @PreAuthorize("principal.claims['aud'] == 'my-audience'") (4)
    MyResource readResource(...);

	@PreAuthorize("@authz.check(authentication, #root)")
    MyResource shareResource(...);
}
@Component
open class MyService {
    @PreAuthorize("denyAll") (1)
    fun myDeprecatedMethod(...): MyResource

    @PreAuthorize("hasRole('ADMIN')") (2)
    fun writeResource(...): MyResource

    @PreAuthorize("hasAuthority('db') and hasRole('ADMIN')") (3)
    fun deleteResource(...): MyResource

    @PreAuthorize("principal.claims['aud'] == 'my-audience'") (4)
    fun readResource(...): MyResource

    @PreAuthorize("@authz.check(#root)")
    fun shareResource(...): MyResource
}
<sec:method-security>
    <protect-pointcut expression="execution(* com.mycompany.*Service.myDeprecatedMethod(..))" access="denyAll"/> (1)
    <protect-pointcut expression="execution(* com.mycompany.*Service.writeResource(..))" access="hasRole('ADMIN')"/> (2)
    <protect-pointcut expression="execution(* com.mycompany.*Service.deleteResource(..))" access="hasAuthority('db') and hasRole('ADMIN')"/> (3)
    <protect-pointcut expression="execution(* com.mycompany.*Service.readResource(..))" access="principal.claims['aud'] == 'my-audience'"/> (4)
    <protect-pointcut expression="execution(* com.mycompany.*Service.shareResource(..))" access="@authz.check(#root)"/> (5)
</sec:method-security>
1 任何人不得以任何理由調用此方法
2 此方法只能由被授予 ROLE_ADMIN 權限的 Authentication 調用
3 此方法只能由被授予 dbROLE_ADMIN 權限的 Authentication 調用
4 只有當 Princpalaud 宣告等於 "my-audience" 時,才能調用此方法
5 只有當 Bean authzcheck 方法傳回 true 時,才能調用此方法

您可以使用像上面 authz 這樣的 Bean 來新增程式設計授權

使用方法參數

此外,Spring Security 提供了一種機制來探索方法參數,以便也可以在 SpEL 運算式中存取它們。

如需完整參考,Spring Security 使用 DefaultSecurityParameterNameDiscoverer 來探索參數名稱。預設情況下,會嘗試以下方法的選項。

  1. 如果 Spring Security 的 @P 註解存在於方法的單一引數上,則會使用該值。以下範例使用 @P 註解

    • Java

    • Kotlin

    import org.springframework.security.access.method.P;
    
    ...
    
    @PreAuthorize("hasPermission(#c, 'write')")
    public void updateContact(@P("c") Contact contact);
    import org.springframework.security.access.method.P
    
    ...
    
    @PreAuthorize("hasPermission(#c, 'write')")
    fun doSomething(@P("c") contact: Contact?)

    此運算式的意圖是要求目前的 Authentication 具有特定於此 Contact 實例的 write 權限。

    在幕後,這是透過使用 AnnotationParameterNameDiscoverer 實作的,您可以自訂它以支援任何指定註解的值屬性。

    • 如果Spring Data 的 @Param 註解存在於方法的至少一個參數上,則會使用該值。以下範例使用 @Param 註解

      • Java

      • Kotlin

      import org.springframework.data.repository.query.Param;
      
      ...
      
      @PreAuthorize("#n == authentication.name")
      Contact findContactByName(@Param("n") String name);
      import org.springframework.data.repository.query.Param
      
      ...
      
      @PreAuthorize("#n == authentication.name")
      fun findContactByName(@Param("n") name: String?): Contact?

      此運算式的意圖是要求 name 等於 Authentication#getName,才能授權調用。

      在幕後,這是透過使用 AnnotationParameterNameDiscoverer 實作的,您可以自訂它以支援任何指定註解的值屬性。

    • 如果您使用 -parameters 引數編譯程式碼,則會使用標準 JDK 反射 API 來探索參數名稱。這適用於類別和介面。

    • 最後,如果您使用偵錯符號編譯程式碼,則會使用偵錯符號探索參數名稱。這不適用於介面,因為它們沒有關於參數名稱的偵錯資訊。對於介面,必須使用註解或 -parameters 方法。

授權任意物件

Spring Security 也支援包裝任何使用方法安全性註解註解的物件。

實現此目的最簡單的方法是標記任何傳回您希望使用 @AuthorizeReturnObject 註解授權的物件的方法。

例如,考慮以下 User 類別

  • Java

  • Kotlin

public class User {
	private String name;
	private String email;

	public User(String name, String email) {
		this.name = name;
		this.email = email;
	}

	public String getName() {
		return this.name;
	}

    @PreAuthorize("hasAuthority('user:read')")
    public String getEmail() {
		return this.email;
    }
}
class User (val name:String, @get:PreAuthorize("hasAuthority('user:read')") val email:String)

給定像這樣的一個介面

  • Java

  • Kotlin

public class UserRepository {
	@AuthorizeReturnObject
    Optional<User> findByName(String name) {
		// ...
    }
}
class UserRepository {
    @AuthorizeReturnObject
    fun findByName(name:String?): Optional<User?>? {
        // ...
    }
}

然後,從 findById 傳回的任何 User 都將像其他受 Spring Security 保護的元件一樣受到保護

  • Java

  • Kotlin

@Autowired
UserRepository users;

@Test
void getEmailWhenProxiedThenAuthorizes() {
    Optional<User> securedUser = users.findByName("name");
    assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> securedUser.get().getEmail());
}
import jdk.incubator.vector.VectorOperators.Test
import java.nio.file.AccessDeniedException
import java.util.*

@Autowired
var users:UserRepository? = null

@Test
fun getEmailWhenProxiedThenAuthorizes() {
    val securedUser: Optional<User> = users.findByName("name")
    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy{securedUser.get().getEmail()}
}

在類別層級使用 @AuthorizeReturnObject

@AuthorizeReturnObject 可以放置在類別層級。但是請注意,這表示 Spring Security 將嘗試代理任何傳回物件,包括 StringInteger 和其他類型。這通常不是您想要執行的操作。

如果您想在類別或介面上使用 @AuthorizeReturnObject,而這些類別或介面的方法傳回值類型 (例如 intStringDouble 或這些類型的集合),則您也應該發布適當的 AuthorizationAdvisorProxyFactory.TargetVisitor,如下所示

  • Java

  • Kotlin

@Bean
static Customizer<AuthorizationAdvisorProxyFactory> skipValueTypes() {
    return (factory) -> factory.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes());
}
@Bean
open fun skipValueTypes() = Customizer<AuthorizationAdvisorProxyFactory> {
    it.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes())
}

您可以設定您自己的 AuthorizationAdvisorProxyFactory.TargetVisitor,以自訂任何類型集合的代理

以程式設計方式代理

您也可以以程式設計方式代理給定的物件。

若要實現此目的,您可以自動裝配提供的 AuthorizationProxyFactory 實例,該實例基於您已組態的方法安全性攔截器。如果您正在使用 @EnableMethodSecurity,則表示預設情況下,它將具有 @PreAuthorize@PostAuthorize@PreFilter@PostFilter 的攔截器。

您可以透過以下方式代理 user 的實例

  • Java

  • Kotlin

@Autowired
AuthorizationProxyFactory proxyFactory;

@Test
void getEmailWhenProxiedThenAuthorizes() {
    User user = new User("name", "email");
    assertThat(user.getEmail()).isNotNull();
    User securedUser = proxyFactory.proxy(user);
    assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail);
}
@Autowired
var proxyFactory:AuthorizationProxyFactory? = null

@Test
fun getEmailWhenProxiedThenAuthorizes() {
    val user: User = User("name", "email")
    assertThat(user.getEmail()).isNotNull()
    val securedUser: User = proxyFactory.proxy(user)
    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy(securedUser::getEmail)
}

手動建構

如果您需要與 Spring Security 預設值不同的東西,您也可以定義自己的實例。

例如,如果您定義一個 AuthorizationProxyFactory 實例,如下所示

  • Java

  • Kotlin

import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
import static org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize;
// ...

AuthorizationProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
// and if needing to skip value types
proxyFactory.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes());
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize

// ...

val proxyFactory: AuthorizationProxyFactory = AuthorizationProxyFactory(preAuthorize())
// and if needing to skip value types
proxyFactory.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes())

然後,您可以將 User 的任何實例包裝如下

  • Java

  • Kotlin

@Test
void getEmailWhenProxiedThenAuthorizes() {
	AuthorizationProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
    User user = new User("name", "email");
    assertThat(user.getEmail()).isNotNull();
    User securedUser = proxyFactory.proxy(user);
    assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail);
}
@Test
fun getEmailWhenProxiedThenAuthorizes() {
    val proxyFactory: AuthorizationProxyFactory = AuthorizationAdvisorProxyFactory.withDefaults()
    val user: User = User("name", "email")
    assertThat(user.getEmail()).isNotNull()
    val securedUser: User = proxyFactory.proxy(user)
    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy(securedUser::getEmail)
}

此功能尚不支援 Spring AOT

代理集合

AuthorizationProxyFactory 支援 Java 集合、串流、陣列、optionals 和迭代器,方法是代理元素類型,並透過代理值類型來代理 map。

這表示當代理物件的 List 時,以下也適用

  • Java

@Test
void getEmailWhenProxiedThenAuthorizes() {
	AuthorizationProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
    List<User> users = List.of(ada, albert, marie);
    List<User> securedUsers = proxyFactory.proxy(users);
	securedUsers.forEach((securedUser) ->
        assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail));
}

代理類別

在有限的情況下,代理 Class 本身可能很有價值,而 AuthorizationProxyFactory 也支援這一點。這大致相當於在 Spring Framework 對建立代理的支援中呼叫 ProxyFactory#getProxyClass

當您需要預先建構代理類別時,例如使用 Spring AOT 時,這很方便。

支援所有方法安全性註解

AuthorizationProxyFactory 支援在您的應用程式中啟用的任何方法安全性註解。它基於作為 Bean 發布的任何 AuthorizationAdvisor 類別。

由於 @EnableMethodSecurity 預設發布 @PreAuthorize@PostAuthorize@PreFilter@PostFilter advisors,因此您通常不需要執行任何操作即可啟用此功能。

使用 returnObjectfilterObject 的 SpEL 運算式位於代理之後,因此可以完全存取物件。

自訂 Advice

如果您還有想要套用的安全性 advice,您可以發布您自己的 AuthorizationAdvisor,如下所示

  • Java

  • Kotlin

@EnableMethodSecurity
class SecurityConfig {
    @Bean
    static AuthorizationAdvisor myAuthorizationAdvisor() {
        return new AuthorizationAdvisor();
    }
}
@EnableMethodSecurity
internal class SecurityConfig {
    @Bean
    fun myAuthorizationAdvisor(): AuthorizationAdvisor {
        return AuthorizationAdvisor()
    }
]

Spring Security 會將該 advisor 新增到 AuthorizationProxyFactory 在代理物件時新增的 advice 集合中。

與 Jackson 搭配使用

此功能的一個強大用途是從控制器傳回安全值,如下所示

  • Java

  • Kotlin

@RestController
public class UserController {
	@Autowired
    AuthorizationProxyFactory proxyFactory;

	@GetMapping
    User currentUser(@AuthenticationPrincipal User user) {
        return this.proxyFactory.proxy(user);
    }
}
@RestController
class UserController  {
    @Autowired
    var proxyFactory: AuthorizationProxyFactory? = null

    @GetMapping
    fun currentUser(@AuthenticationPrincipal user:User?): User {
        return proxyFactory.proxy(user)
    }
}

但是,如果您正在使用 Jackson,則可能會導致序列化錯誤,如下所示

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-reference leading to cycle

這是由於 Jackson 與 CGLIB 代理的運作方式。若要解決此問題,請將以下註解新增至 User 類別的頂端

  • Java

  • Kotlin

@JsonSerialize(as = User.class)
public class User {

}
@JsonSerialize(`as` = User::class)
class User

最後,您需要發布自訂攔截器來捕獲每個欄位擲回的 AccessDeniedException,您可以透過以下方式執行此操作

  • Java

  • Kotlin

@Component
public class AccessDeniedExceptionInterceptor implements AuthorizationAdvisor {
    private final AuthorizationAdvisor advisor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize();

	@Override
	public Object invoke(MethodInvocation invocation) throws Throwable {
		try {
			return invocation.proceed();
		} catch (AccessDeniedException ex) {
			return null;
		}
	}

	@Override
	public Pointcut getPointcut() {
		return this.advisor.getPointcut();
	}

	@Override
	public Advice getAdvice() {
		return this;
	}

	@Override
	public int getOrder() {
		return this.advisor.getOrder() - 1;
	}
}
@Component
class AccessDeniedExceptionInterceptor: AuthorizationAdvisor {
    var advisor: AuthorizationAdvisor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize()

    @Throws(Throwable::class)
    fun invoke(invocation: MethodInvocation): Any? {
        return try  {
            invocation.proceed()
        } catch (ex:AccessDeniedException) {
            null
        }
    }

     val pointcut: Pointcut
     get() = advisor.getPointcut()

     val advice: Advice
     get() = this

     val order: Int
     get() = advisor.getOrder() - 1
}

然後,您將根據使用者的授權層級看到不同的 JSON 序列化。如果他們沒有 user:read 權限,則他們將看到

{
    "name" : "name",
    "email" : null
}

如果他們確實擁有該權限,則他們將看到

{
    "name" : "name",
    "email" : "email"
}

如果您也不想向未經授權的使用者顯示 JSON 金鑰,您也可以新增 Spring Boot 屬性 spring.jackson.default-property-inclusion=non_null 以排除 null 值。

在授權遭拒時提供後備值

在某些情況下,當方法在沒有所需權限的情況下被調用時,您可能不希望擲回 AuthorizationDeniedException。相反地,您可能希望傳回後處理的結果,例如遮罩的結果,或在授權遭拒發生在調用方法之前的情況下的預設值。

Spring Security 透過使用 @HandleAuthorizationDenied,為處理方法調用時授權遭拒提供支援。處理常式適用於 @PreAuthorize@PostAuthorize 註解以及從方法調用本身擲回的 AuthorizationDeniedException 中發生的授權遭拒。

讓我們考慮前一節中的範例,但我們將使用 @HandleAuthorizationDenied 中的 handlerClass 屬性,而不是建立 AccessDeniedExceptionInterceptor 來將 AccessDeniedException 轉換為 null 傳回值

  • Java

  • Kotlin

public class NullMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler { (1)

    @Override
    public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
        return null;
    }

}

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Bean (2)
    public NullMethodAuthorizationDeniedHandler nullMethodAuthorizationDeniedHandler() {
        return new NullMethodAuthorizationDeniedHandler();
    }

}

public class User {
    // ...

    @PreAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = NullMethodAuthorizationDeniedHandler.class)
    public String getEmail() {
        return this.email;
    }
}
class NullMethodAuthorizationDeniedHandler : MethodAuthorizationDeniedHandler { (1)

    override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
        return null
    }

}

@Configuration
@EnableMethodSecurity
class SecurityConfig {

    @Bean (2)
    fun nullMethodAuthorizationDeniedHandler(): NullMethodAuthorizationDeniedHandler {
        return MaskMethodAuthorizationDeniedHandler()
    }

}

class User (val name:String, @PreAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = NullMethodAuthorizationDeniedHandler::class) val email:String) (3)
1 建立傳回 null 值的 MethodAuthorizationDeniedHandler 實作
2 NullMethodAuthorizationDeniedHandler 註冊為 Bean
3 使用 @HandleAuthorizationDenied 註解方法,並將 NullMethodAuthorizationDeniedHandler 傳遞給 handlerClass 屬性

然後,您可以驗證是否傳回 null 值而不是 AccessDeniedException

您也可以使用 @Component 註解您的類別,而不是建立 @Bean 方法

  • Java

  • Kotlin

@Autowired
UserRepository users;

@Test
void getEmailWhenProxiedThenNullEmail() {
    Optional<User> securedUser = users.findByName("name");
    assertThat(securedUser.get().getEmail()).isNull();
}
@Autowired
var users:UserRepository? = null

@Test
fun getEmailWhenProxiedThenNullEmail() {
    val securedUser: Optional<User> = users.findByName("name")
    assertThat(securedUser.get().getEmail()).isNull()
}

使用方法調用中遭拒的結果

在某些情況下,您可能想要傳回從遭拒結果衍生的安全結果。例如,如果使用者未獲授權查看電子郵件地址,您可能想要對原始電子郵件地址套用一些遮罩,即 [email protected] 將變成 use******@example.com

對於這些情況,您可以覆寫 MethodAuthorizationDeniedHandler 中的 handleDeniedInvocationResult,它具有 MethodInvocationResult 作為引數。讓我們繼續使用先前的範例,但我們將傳回電子郵件的遮罩值,而不是傳回 null

  • Java

  • Kotlin

public class EmailMaskingMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler { (1)

    @Override
    public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
        return "***";
    }

    @Override
    public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult) {
        String email = (String) methodInvocationResult.getResult();
        return email.replaceAll("(^[^@]{3}|(?!^)\\G)[^@]", "$1*");
    }

}

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Bean (2)
    public EmailMaskingMethodAuthorizationDeniedHandler emailMaskingMethodAuthorizationDeniedHandler() {
        return new EmailMaskingMethodAuthorizationDeniedHandler();
    }

}

public class User {
    // ...

    @PostAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = EmailMaskingMethodAuthorizationDeniedHandler.class)
    public String getEmail() {
        return this.email;
    }
}
class EmailMaskingMethodAuthorizationDeniedHandler : MethodAuthorizationDeniedHandler {

    override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
        return "***"
    }

    override fun handleDeniedInvocationResult(methodInvocationResult: MethodInvocationResult, authorizationResult: AuthorizationResult): Any {
        val email = methodInvocationResult.result as String
        return email.replace("(^[^@]{3}|(?!^)\\G)[^@]".toRegex(), "$1*")
    }

}

@Configuration
@EnableMethodSecurity
class SecurityConfig {

    @Bean
    fun emailMaskingMethodAuthorizationDeniedHandler(): EmailMaskingMethodAuthorizationDeniedHandler {
        return EmailMaskingMethodAuthorizationDeniedHandler()
    }

}

class User (val name:String, @PostAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = EmailMaskingMethodAuthorizationDeniedHandler::class) val email:String) (3)
1 建立傳回未經授權結果值的遮罩值的 MethodAuthorizationDeniedHandler 實作
2 EmailMaskingMethodAuthorizationDeniedHandler 註冊為 Bean
3 使用 @HandleAuthorizationDenied 註解方法,並將 EmailMaskingMethodAuthorizationDeniedHandler 傳遞給 handlerClass 屬性

然後,您可以驗證是否傳回遮罩的電子郵件而不是 AccessDeniedException

由於您可以存取原始遭拒的值,請確保您正確處理它,並且不要將其傳回給調用方。

  • Java

  • Kotlin

@Autowired
UserRepository users;

@Test
void getEmailWhenProxiedThenMaskedEmail() {
    Optional<User> securedUser = users.findByName("name");
    // email is [email protected]
    assertThat(securedUser.get().getEmail()).isEqualTo("use******@example.com");
}
@Autowired
var users:UserRepository? = null

@Test
fun getEmailWhenProxiedThenMaskedEmail() {
    val securedUser: Optional<User> = users.findByName("name")
    // email is [email protected]
    assertThat(securedUser.get().getEmail()).isEqualTo("use******@example.com")
}

實作 MethodAuthorizationDeniedHandler 時,您可以選擇要傳回的類型,有幾個選項

  • null 值。

  • 非 null 值,尊重方法的傳回類型。

  • 擲回例外,通常是 AuthorizationDeniedException 的實例。這是預設行為。

  • 反應式應用程式的 Mono 類型。

請注意,由於處理常式必須註冊為應用程式環境定義中的 Bean,因此如果您需要更複雜的邏輯,您可以將相依性注入到其中。除了這一點,您還可以取得 MethodInvocationMethodInvocationResult,以及 AuthorizationResult,以取得與授權決策相關的更多詳細資訊。

根據可用參數決定要傳回的內容

考慮一種情況,其中可能有多個不同方法的遮罩值,如果我們必須為每個方法建立一個處理常式,那將不會那麼有效率,儘管這樣做完全可以。在這種情況下,我們可以使用透過參數傳遞的資訊來決定要執行什麼操作。例如,我們可以建立自訂的 @Mask 註解和一個處理常式,該處理常式偵測到該註解以決定要傳回的遮罩值

  • Java

  • Kotlin

import org.springframework.core.annotation.AnnotationUtils;

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Mask {

    String value();

}

public class MaskAnnotationDeniedHandler implements MethodAuthorizationDeniedHandler {

    @Override
    public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
        Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class);
        return mask.value();
    }

}

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public MaskAnnotationDeniedHandler maskAnnotationDeniedHandler() {
        return new MaskAnnotationDeniedHandler();
    }

}

@Component
public class MyService {

    @PreAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
    @Mask("***")
    public String foo() {
        return "foo";
    }

    @PreAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
    @Mask("???")
    public String bar() {
        return "bar";
    }

}
import org.springframework.core.annotation.AnnotationUtils

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Mask(val value: String)

class MaskAnnotationDeniedHandler : MethodAuthorizationDeniedHandler {

    override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
        val mask = AnnotationUtils.getAnnotation(methodInvocation.method, Mask::class.java)
        return mask.value
    }

}

@Configuration
@EnableMethodSecurity
class SecurityConfig {

    @Bean
    fun maskAnnotationDeniedHandler(): MaskAnnotationDeniedHandler {
        return MaskAnnotationDeniedHandler()
    }

}

@Component
class MyService {

    @PreAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
    @Mask("***")
    fun foo(): String {
        return "foo"
    }

    @PreAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
    @Mask("???")
    fun bar(): String {
        return "bar"
    }

}

現在,當存取遭拒時,傳回值將根據 @Mask 註解來決定

  • Java

  • Kotlin

@Autowired
MyService myService;

@Test
void fooWhenDeniedThenReturnStars() {
    String value = this.myService.foo();
    assertThat(value).isEqualTo("***");
}

@Test
void barWhenDeniedThenReturnQuestionMarks() {
    String value = this.myService.foo();
    assertThat(value).isEqualTo("???");
}
@Autowired
var myService: MyService

@Test
fun fooWhenDeniedThenReturnStars() {
    val value: String = myService.foo()
    assertThat(value).isEqualTo("***")
}

@Test
fun barWhenDeniedThenReturnQuestionMarks() {
    val value: String = myService.foo()
    assertThat(value).isEqualTo("???")
}

與中繼註解支援結合

您也可以將 @HandleAuthorizationDenied 與其他註解結合使用,以減少和簡化方法中的註解。讓我們考慮前一節中的範例,並將 @HandleAuthorizationDenied@Mask 合併

  • Java

  • Kotlin

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
public @interface Mask {

    String value();

}

@Mask("***")
public String myMethod() {
    // ...
}
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
annotation class Mask(val value: String)

@Mask("***")
fun myMethod(): String {
    // ...
}

現在,當您需要在方法中使用遮罩行為時,您不必記住新增這兩個註解。請務必閱讀中繼註解支援章節,以取得有關用法的更多詳細資訊。

@EnableGlobalMethodSecurity 遷移

如果您正在使用 @EnableGlobalMethodSecurity,您應該遷移到 @EnableMethodSecurity

全域方法安全性取代為方法安全性

@EnableGlobalMethodSecurity<global-method-security> 已被取代,建議改用 @EnableMethodSecurity<method-security>。新的註解和 XML 元素預設會啟用 Spring 的pre-post 註解,並在內部使用 AuthorizationManager

這表示以下兩個清單在功能上是等效的

  • Java

  • Kotlin

  • Xml

@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
<global-method-security pre-post-enabled="true"/>

  • Java

  • Kotlin

  • Xml

@EnableMethodSecurity
@EnableMethodSecurity
<method-security/>

對於未使用 pre-post 註解的應用程式,請務必將其關閉,以避免啟用不需要的行為。

例如,像這樣的清單

  • Java

  • Kotlin

  • Xml

@EnableGlobalMethodSecurity(securedEnabled = true)
@EnableGlobalMethodSecurity(securedEnabled = true)
<global-method-security secured-enabled="true"/>

應該變更為

  • Java

  • Kotlin

  • Xml

@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
<method-security secured-enabled="true" pre-post-enabled="false"/>

使用自訂 @Bean 而不是子類別化 DefaultMethodSecurityExpressionHandler

作為效能最佳化,MethodSecurityExpressionHandler 中引入了一個新方法,該方法採用 Supplier<Authentication> 而不是 Authentication

這允許 Spring Security 延遲 Authentication 的查找,並且當您使用 @EnableMethodSecurity 而不是 @EnableGlobalMethodSecurity 時,會自動利用這一點。

但是,假設您的程式碼擴展了 DefaultMethodSecurityExpressionHandler 並覆寫了 createSecurityExpressionRoot(Authentication, MethodInvocation) 以傳回自訂的 SecurityExpressionRoot 實例。這將不再起作用,因為 @EnableMethodSecurity 設定的安排改為呼叫 createEvaluationContext(Supplier<Authentication>, MethodInvocation)

幸運的是,這種程度的自訂通常是不必要的。相反地,您可以建立一個自訂 Bean,其中包含您需要的授權方法。

例如,假設您想要自訂評估 @PostAuthorize("hasAuthority('ADMIN')")。您可以建立一個像這樣的自訂 @Bean

  • Java

  • Kotlin

class MyAuthorizer {
	boolean isAdmin(MethodSecurityExpressionOperations root) {
		boolean decision = root.hasAuthority("ADMIN");
		// custom work ...
        return decision;
	}
}
class MyAuthorizer {
	fun isAdmin(val root: MethodSecurityExpressionOperations): boolean {
		val decision = root.hasAuthority("ADMIN");
		// custom work ...
        return decision;
	}
}

然後在註解中參考它,如下所示

  • Java

  • Kotlin

@PreAuthorize("@authz.isAdmin(#root)")
@PreAuthorize("@authz.isAdmin(#root)")

我仍然偏好子類別化 DefaultMethodSecurityExpressionHandler

如果您必須繼續子類別化 DefaultMethodSecurityExpressionHandler,您仍然可以這樣做。相反地,覆寫 createEvaluationContext(Supplier<Authentication>, MethodInvocation) 方法,如下所示

  • Java

  • Kotlin

@Component
class MyExpressionHandler extends DefaultMethodSecurityExpressionHandler {
    @Override
    public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication, MethodInvocation mi) {
		StandardEvaluationContext context = (StandardEvaluationContext) super.createEvaluationContext(authentication, mi);
        MethodSecurityExpressionOperations delegate = (MethodSecurityExpressionOperations) context.getRootObject().getValue();
        MySecurityExpressionRoot root = new MySecurityExpressionRoot(delegate);
        context.setRootObject(root);
        return context;
    }
}
@Component
class MyExpressionHandler: DefaultMethodSecurityExpressionHandler {
    override fun createEvaluationContext(val authentication: Supplier<Authentication>,
        val mi: MethodInvocation): EvaluationContext {
		val context = super.createEvaluationContext(authentication, mi) as StandardEvaluationContext
        val delegate = context.getRootObject().getValue() as MethodSecurityExpressionOperations
        val root = MySecurityExpressionRoot(delegate)
        context.setRootObject(root)
        return context
    }
}

延伸閱讀

現在您已經保護了應用程式的請求,如果您尚未執行,請保護其請求。您也可以進一步閱讀測試您的應用程式,或將 Spring Security 與應用程式的其他方面整合,例如資料層追蹤和指標