方法安全

除了在請求級別建模授權之外,Spring Security 還支援在方法級別建模授權。

您可以透過在任何 @Configuration 類上新增 @EnableMethodSecurity 註解來啟用它,或者將 <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,而不是元資料來源、配置屬性、決策管理器和投票器。這簡化了重用和定製。

  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(id: String): Customer { ... }
}

當方法安全被啟用時,對 MyCustomerService#readCustomer 的給定呼叫可能看起來像這樣

methodsecurity
  1. Spring AOP 呼叫其 readCustomer 的代理方法。在代理的其他通知器中,它呼叫了一個與@PreAuthorize 切點匹配的 AuthorizationManagerBeforeMethodInterceptor

  2. 攔截器呼叫 PreAuthorizeAuthorizationManager#check

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

  4. 攔截器使用此上下文評估表示式;具體來說,它從 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 的支援。

每個註解都有自己的切點

每個註解都有其自己的切點例項,該例項在整個物件層次結構中,從方法及其封閉類開始,查詢該註解或其元註解對應項。

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

每個註解都有自己專用的方法攔截器。這樣做的原因是為了使事物更具組合性。例如,如果需要,您可以停用 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

  • Kotlin

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

然而,您也可以改為向擁有 ROLE_ADMIN 的人授予 permission:read。一種方法是使用 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(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 也可以是元註解,可以定義在類或介面級別,並使用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(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 也可以是元註解,可以定義在類或介面級別,並使用SpEL 授權表示式

@PostAuthorize 在防禦不安全的直接物件引用時特別有用。實際上,它可以定義為元註解,如下所示

  • 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(id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}

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

請注意,不建議將 @PostAuthorize 用於執行資料庫寫入的類,因為這通常意味著在檢查安全不變式之前已進行資料庫更改。這樣做的常見示例是,如果您在同一個方法上同時使用 @Transactional@PostAuthorize。相反,請首先讀取值,在讀取上使用 @PostAuthorize,然後如果讀取被授權,則執行資料庫寫入。如果必須這樣做,您可以確保 @EnableTransactionManagement@EnableMethodSecurity 之前

使用 @PreFilter 過濾方法引數

當方法安全處於活動狀態時,您可以像這樣使用 @PreFilter 註解方法

  • Java

  • Kotlin

@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;
	}
}
@Component
open class BankService {
	@PreFilter("filterObject.owner == authentication.name")
	fun updateAccounts(vararg accounts: Account): Collection<Account> {
        // ... `accounts` will only contain the accounts owned by the logged-in user
        return updated
	}
}

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

然後您可以透過以下方式測試該類,以確認它正在執行授權規則

  • Java

  • Kotlin

@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);
}
@Autowired
lateinit var bankService: BankService

@WithMockUser(username="owner")
@Test
fun updateAccountsWhenOwnedThenReturns() {
    val ownedBy: Account = ...
    val notOwnedBy: Account = ...
    val updated: Collection<Account> = bankService.updateAccounts(ownedBy, notOwnedBy)
    assertThat(updated).containsOnly(ownedBy)
}
@PreFilter 也可以是元註解,可以定義在類或介面級別,並使用SpEL 授權表示式

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

例如,上面的 updateAccounts 宣告將以與以下其他四個相同的方式執行

  • Java

  • Kotlin

@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)
@PreFilter("filterObject.owner == authentication.name")
fun updateAccounts(accounts: Array<Account>): Collection<Account>

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

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

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

結果是,上述方法將只包含其 owner 屬性與登入使用者的 name 匹配的 Account 例項。

使用 @PostFilter 過濾方法結果

當方法安全處於活動狀態時,您可以像這樣使用 @PostFilter 註解方法

  • Java

  • Kotlin

@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;
	}
}
@Component
open class BankService {
	@PostFilter("filterObject.owner == authentication.name")
	fun readAccounts(vararg ids: String): Collection<Account> {
        // ... 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

  • Kotlin

@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");
}
@Autowired
lateinit var bankService: BankService

@WithMockUser(username="owner")
@Test
fun readAccountsWhenOwnedThenReturns() {
    val accounts: Collection<Account> = bankService.updateAccounts("owner", "not-owner")
    assertThat(accounts).hasSize(1)
    assertThat(accounts[0].owner).isEqualTo("owner")
}
@PostFilter 也可以是元註解,可以定義在類或介面級別,並使用SpEL 授權表示式

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

例如,上面的 readAccounts 宣告將與以下其他三個以相同的方式執行

  • Java

  • Kotlin

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

@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)
@PostFilter("filterObject.owner == authentication.name")
fun readAccounts(vararg ids: String): Collection<Account>

@PostFilter("filterObject.owner == authentication.name")
fun readAccounts(vararg ids: String): Array<Account>

@PostFilter("filterObject.owner == authentication.name")
fun readAccounts(vararg ids: String): Map<String, Account>

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

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

記憶體過濾顯然可能代價高昂,因此請考慮是否最好在資料層過濾資料

使用 @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 無法判斷您想使用哪個。

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

使用元註解

方法安全支援元註解。這意味著您可以獲取任何註解並根據您的應用程式特定用例提高可讀性。

例如,您可以將 @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(id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}

這使得方法定義更具可讀性。

元註解表示式模板化

您還可以選擇使用元註解模板,這允許更強大的註解定義。

首先,釋出以下 bean

  • Java

  • Kotlin

@Bean
static AnnotationTemplateExpressionDefaults templateExpressionDefaults() {
	return new AnnotationTemplateExpressionDefaults();
}
companion object {
    @Bean
    fun templateExpressionDefaults(): AnnotationTemplateExpressionDefaults {
        return AnnotationTemplateExpressionDefaults()
    }
}

現在,您可以建立一個更強大的註解,如 @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(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(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,該 bean 的方法接受 MethodSecurityExpressionOperations 例項,如下所示

  • Java

  • Kotlin

@Component("authz")
public class AuthorizationLogic {
    public boolean decide(MethodSecurityExpressionOperations operations) {
        // ... authorization logic
    }
}
@Component("authz")
open class AuthorizationLogic {
    fun decide(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(operations: MethodSecurityExpressionOperations): AuthorizationDecision {
        // ... authorization logic
        return MyAuthorizationDecision(false, details)
    }
}

或者丟擲自定義 AuthorizationDeniedException 例項。但是請注意,返回物件是首選,因為這不會產生生成堆疊跟蹤的開銷。

然後,當您自定義如何處理授權結果時,您可以訪問自定義詳細資訊。

此外,您可以返回 AuthorizationManager 本身。這在將自定義 Web 授權規則與方法安全規則統一時很有用,因為 Web 安全預設要求指定 AuthorizationManager 例項。

使用自定義授權管理器

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

首先,宣告一個授權管理器例項,也許像這樣

  • Java

  • Kotlin

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

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

    override fun authorize(authentication: Supplier<Authentication>, invocation: MethodInvocationResult): AuthorizationResult {
        // ... 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(manager: MyAuthorizationManager) : Advisor {
		return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager)
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	fun postAuthorize(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(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,以新增您自己的自定義授權表示式,超出預設設定。

使用 AOT

Spring Security 將掃描應用程式上下文中的所有 bean,以查詢使用 @PreAuthorize@PostAuthorize 的方法。當找到時,它將解析安全表示式中使用的任何 bean,併為該 bean 註冊適當的執行時提示。如果找到使用 @AuthorizeReturnObject 的方法,它將遞迴搜尋該方法的返回型別中是否有 @PreAuthorize@PostAuthorize 註解,並相應地註冊它們。

例如,考慮以下 Spring Boot 應用程式

  • Java

  • Kotlin

@Service
public class AccountService { (1)

    @PreAuthorize("@authz.decide()") (2)
    @AuthorizeReturnObject (3)
    public Account getAccountById(String accountId) {
        // ...
    }

}

public class Account {

    private final String accountNumber;

    // ...

    @PreAuthorize("@accountAuthz.canViewAccountNumber()") (4)
    public String getAccountNumber() {
        return this.accountNumber;
    }

    @AuthorizeReturnObject (5)
    public User getUser() {
        return new User("John Doe");
    }

}

public class User {

    private final String fullName;

    // ...

    @PostAuthorize("@myOtherAuthz.decide()") (6)
    public String getFullName() {
        return this.fullName;
    }

}
@Service
class AccountService { (1)

    @PreAuthorize("@authz.decide()") (2)
    @AuthorizeReturnObject (3)
    fun getAccountById(accountId: String): Account {
        // ...
    }

}

class Account(private val accountNumber: String) {

    @PreAuthorize("@accountAuthz.canViewAccountNumber()") (4)
    fun getAccountNumber(): String {
        return this.accountNumber
    }

    @AuthorizeReturnObject (5)
    fun getUser(): User {
        return User("John Doe")
    }

}

class User(private val fullName: String) {

    @PostAuthorize("@myOtherAuthz.decide()") (6)
    fun getFullName(): String {
        return this.fullName
    }

}
1 Spring Security 找到了 AccountService bean
2 找到使用 @PreAuthorize 的方法後,它將解析表示式中使用的任何 bean 名稱(在這種情況下為 authz),併為該 bean 類註冊執行時提示
3 找到使用 @AuthorizeReturnObject 的方法後,它將檢視該方法的返回型別中是否有 @PreAuthorize@PostAuthorize
4 然後,它找到另一個帶有另一個 bean 名稱:accountAuthz@PreAuthorize;執行時提示也為該 bean 類註冊
5 找到另一個 @AuthorizeReturnObject 後,它將再次檢視方法的返回型別
6 現在,找到一個 @PostAuthorize,其中使用了另一個 bean 名稱:myOtherAuthz;執行時提示也為該 bean 類註冊

很多時候,Spring Security 無法提前確定方法的實際返回型別,因為它可能隱藏在已擦除的泛型型別中。

考慮以下服務

  • Java

  • Kotlin

@Service
public class AccountService {

    @AuthorizeReturnObject
    public List<Account> getAllAccounts() {
        // ...
    }

}
@Service
class AccountService {

    @AuthorizeReturnObject
    fun getAllAccounts(): List<Account> {
        // ...
    }

}

在這種情況下,泛型型別被擦除,因此 Spring Security 無法提前知道需要訪問 Account 以檢查 @PreAuthorize@PostAuthorize

為了解決這個問題,您可以釋出一個 PrePostAuthorizeExpressionBeanHintsRegistrar,如下所示

  • Java

  • Kotlin

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static SecurityHintsRegistrar registerTheseToo() {
    return new PrePostAuthorizeExpressionBeanHintsRegistrar(Account.class);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun registerTheseToo(): SecurityHintsRegistrar {
    return PrePostAuthorizeExpressionBeanHintsRegistrar(Account::class.java)
}

使用 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 通知織入到 bean 的位元組碼中,有時可以提高效能。

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

  • Java

  • Kotlin

  • Xml

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

結果是 Spring Security 會將其通知器作為 AspectJ 通知釋出,以便它們可以相應地織入。

指定順序

如前所述,每個註解都有一個 Spring AOP 方法攔截器,並且這些攔截器中的每一個都在 Spring AOP 通知器鏈中有一個位置。

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

您可以使用 @EnableMethodSecurity 上的 offset 引數,將所有攔截器整體移動,以在方法呼叫中提前或延後提供其建議。

使用 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_ 或配置為預設字首的任何內容

  • hasAllAuthorities - 該方法要求 Authentication 具有與所有給定值匹配的 GrantedAuthority

  • hasAllRoles - hasAllAuthorities 的快捷方式,其字首為 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 實現的,您可以自定義它以支援任何指定註解的值屬性。

  2. 如果方法的至少一個引數上存在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 實現的,您可以自定義它以支援任何指定註解的值屬性。

  3. 如果您使用 -parameters 引數編譯程式碼,則使用標準 JDK 反射 API 來發現引數名稱。這適用於類和介面。

  4. 最後,如果您使用除錯符號編譯程式碼,則透過使用除錯符號來發現引數名稱。這不適用於介面,因為它們沒有關於引數名稱的除錯資訊。對於介面,必須使用註解或 -parameters 方法。

自定義授權管理器

當您將 SpEL 表示式與@PreAuthorize@PostAuthorize@PreFilter@PostFilter一起使用時,Spring Security 會為您建立適當的 AuthorizationManager 例項。在某些情況下,您可能希望自定義所建立的內容,以便完全控制框架級別的授權決策。

為了控制為前置和後置註解建立 AuthorizationManager 例項,您可以建立自定義的 AuthorizationManagerFactory。例如,假設您希望在需要任何其他角色時允許具有 ADMIN 角色的使用者。為此,您可以為方法安全建立自定義實現,如下例所示

  • Java

  • Kotlin

@Component
public class CustomMethodInvocationAuthorizationManagerFactory
		implements AuthorizationManagerFactory<MethodInvocation> {

	private final AuthorizationManagerFactory<MethodInvocation> delegate =
			new DefaultAuthorizationManagerFactory<>();

	@Override
	public AuthorizationManager<MethodInvocation> hasRole(String role) {
		return AuthorizationManagers.anyOf(
			this.delegate.hasRole(role),
			this.delegate.hasRole("ADMIN")
		);
	}

	@Override
	public AuthorizationManager<MethodInvocation> hasAnyRole(String... roles) {
		return AuthorizationManagers.anyOf(
			this.delegate.hasAnyRole(roles),
			this.delegate.hasRole("ADMIN")
		);
	}

}
@Component
class CustomMethodInvocationAuthorizationManagerFactory : AuthorizationManagerFactory<MethodInvocation> {
    private val delegate = DefaultAuthorizationManagerFactory<MethodInvocation>()

    override fun hasRole(role: String): AuthorizationManager<MethodInvocation> {
        return AuthorizationManagers.anyOf(
            delegate.hasRole(role),
            delegate.hasRole("ADMIN")
        )
    }

    override fun hasAnyRole(vararg roles: String): AuthorizationManager<MethodInvocation> {
        return AuthorizationManagers.anyOf(
            delegate.hasAnyRole(*roles),
            delegate.hasRole("ADMIN")
        )
    }
}

現在,每當您使用 @PreAuthorize 註解hasRolehasAnyRole 時,Spring Security 將自動呼叫您的自定義工廠來建立一個 AuthorizationManager 例項,該例項允許給定角色 ADMIN 角色訪問。

我們將其作為一個建立自定義 AuthorizationManagerFactory 的簡單示例,儘管可以使用角色層次結構實現相同的結果。請根據您的情況選擇最合適的方法。

授權任意物件

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

import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;

// ...

@Bean
static TargetVisitor skipValueTypes() {
    return TargetVisitor.defaultsSkipValueTypes();
}
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor

// ...

@Bean
open fun skipValueTypes() = TargetVisitor.defaultsSkipValueTypes()

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

以程式設計方式代理

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

為此,您可以自動注入提供的 AuthorizationProxyFactory 例項,該例項基於您配置的方法安全攔截器。如果您使用 @EnableMethodSecurity,則預設情況下它將包含 @PreAuthorize@PostAuthorize@PreFilter@PostFilter 的攔截器。

您可以按以下方式代理使用者例項

  • 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)
}

代理集合

AuthorizationProxyFactory 支援 Java 集合、流、陣列、Optional 和迭代器,透過代理元素型別以及透過代理值型別來代理對映。

這意味著在代理物件 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 通知器,因此您通常無需執行任何操作即可啟用此功能。

使用 returnObjectfilterObject 的 SpEL 表示式位於代理之後,因此可以完全訪問物件。

自定義建議

如果您還有希望應用的自定義安全建議,可以釋出自己的 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 會將該通知器新增到 AuthorizationProxyFactory 在代理物件時新增的建議集中。

使用 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)
    }
}
  • Java

  • Kotlin

@Component
public class Null implements MethodAuthorizationDeniedHandler {
    @Override
    public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
        return null;
    }
}

// ...

@HandleAuthorizationDenied(handlerClass = Null.class)
public class User {
	...
}
@Component
class Null : MethodAuthorizationDeniedHandler {
    override fun handleDeniedInvocation(methodInvocation: MethodInvocation?, authorizationResult: AuthorizationResult?): Any? {
        return null
    }
}

// ...

@HandleAuthorizationDenied(handlerClass = Null.class)
open class User {
	...
}

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

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

如果他們有該許可權,他們將看到

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

您還可以新增 Spring Boot 屬性 spring.jackson.default-property-inclusion=non_null 以將空值從序列化中排除,如果您也不想向未經授權的使用者透露 JSON 鍵。

與 AOT 配合使用

Spring Security 將掃描應用程式上下文中的所有 bean,查詢使用 @AuthorizeReturnObject 的方法。當它找到一個時,它將提前建立並註冊適當的代理類。它還將遞迴搜尋也使用 @AuthorizeReturnObject 的其他巢狀物件並相應地註冊它們。

例如,考慮以下 Spring Boot 應用程式

  • Java

  • Kotlin

@SpringBootApplication
public class MyApplication {
	@RestController
    public static class MyController { (1)
		@GetMapping
        @AuthorizeReturnObject
        Message getMessage() { (2)
			return new Message(someUser, "hello!");
        }
    }

	public static class Message { (3)
		User to;
		String text;

		// ...

        @AuthorizeReturnObject
        public User getTo() { (4)
			return this.to;
        }

		// ...
	}

	public static class User { (5)
		// ...
	}

	public static void main(String[] args) {
		SpringApplication.run(MyApplication.class);
	}
}
@SpringBootApplication
open class MyApplication {
	@RestController
    open class MyController { (1)
		@GetMapping
        @AuthorizeReturnObject
        fun getMessage():Message { (2)
			return Message(someUser, "hello!")
        }
    }

	open class Message { (3)
		val to: User
		val test: String

		// ...

        @AuthorizeReturnObject
        fun getTo(): User { (4)
			return this.to
        }

		// ...
	}

	open class User { (5)
		// ...
	}

	fun main(args: Array<String>) {
		SpringApplication.run(MyApplication.class)
	}
}
1 - 首先,Spring Security 找到 MyController bean
2 - 找到使用 @AuthorizeReturnObject 的方法後,它會代理返回值 Message,並將該代理類註冊到 RuntimeHints
3 - 然後,它遍歷 Message 以檢視它是否使用 @AuthorizeReturnObject
4 - 找到使用 @AuthorizeReturnObject 的方法後,它會代理返回值 User,並將該代理類註冊到 RuntimeHints
5 - 最後,它遍歷 User 以檢視它是否使用 @AuthorizeReturnObject;什麼也沒找到,演算法完成

很多時候,Spring Security 無法提前確定代理類,因為它可能隱藏在已擦除的泛型型別中。

考慮對 MyController 的以下更改

  • Java

  • Kotlin

@RestController
public static class MyController {
    @GetMapping
    @AuthorizeReturnObject
    List<Message> getMessages() {
        return List.of(new Message(someUser, "hello!"));
    }
}
@RestController
static class MyController {
    @AuthorizeReturnObject
    @GetMapping
    fun getMessages(): Array<Message> = arrayOf(Message(someUser, "hello!"))
}

在這種情況下,泛型型別被擦除,因此 Spring Security 無法提前知道 Message 將需要在執行時進行代理。

為了解決這個問題,您可以釋出 AuthorizeProxyFactoryHintsRegistrar,如下所示

  • Java

  • Kotlin

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static SecurityHintsRegsitrar registerTheseToo(AuthorizationProxyFactory proxyFactory) {
	return new AuthorizeReturnObjectHintsRegistrar(proxyFactory, Message.class);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun registerTheseToo(proxyFactory: AuthorizationProxyFactory?): SecurityHintsRegistrar {
    return AuthorizeReturnObjectHintsRegistrar(proxyFactory, Message::class.java)
}

Spring Security 將註冊該類,然後像以前一樣遍歷其型別。

授權被拒絕時提供回退值

在某些情況下,當方法在沒有所需許可權的情況下被呼叫時,您可能不希望丟擲 AuthorizationDeniedException。相反,您可能希望返回一個後處理結果,例如 masked 結果,或者在方法呼叫之前發生授權拒絕時返回預設值。

Spring Security 支援透過使用 @HandleAuthorizationDenied 處理方法呼叫上的授權拒絕。該處理程式適用於在 @PreAuthorize@PostAuthorize 註解中發生的拒絕授權,以及從方法呼叫本身丟擲的 AuthorizationDeniedException

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

  • 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 建立 MethodAuthorizationDeniedHandler 的實現,該實現返回 null
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 值。

  • 一個非空值,符合方法的返回型別。

  • 丟擲異常,通常是 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

如果您目前無法遷移,請將 spring-security-access 模組作為依賴項包含在內,如下所示

  • Maven

  • Gradle

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-access</artifactId>
</dependency>
implementation('org.springframework.security:spring-security-access')

方法安全替換全域性方法安全

@EnableGlobalMethodSecurity<global-method-security> 已棄用,取而代之的是 @EnableMethodSecurity<method-security>。新的註解和 XML 元素預設啟用 Spring 的前置-後置註解,並在內部使用 AuthorizationManager

這意味著以下兩個列表功能上是等效的

  • Java

  • Kotlin

  • Xml

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

  • Java

  • Kotlin

  • Xml

@EnableMethodSecurity
@EnableMethodSecurity
<method-security/>

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

例如,如下所示的列表

  • 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(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(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 與應用程式的其他方面整合,例如資料層跟蹤和指標

© . This site is unofficial and not affiliated with VMware.