方法安全
除了在請求層面建模授權之外,Spring Security 還支援在方法層面進行建模。
您可以透過使用 @EnableMethodSecurity
註解任何 @Configuration
類或將 <method-security>
新增到任何 XML 配置檔案中來在應用程式中啟用它,如下所示:
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity
@EnableMethodSecurity
<sec:method-security/>
然後,您可以立即使用以下註解標註任何 Spring 管理的類或方法:@PreAuthorize
、@PostAuthorize
、@PreFilter
和 @PostFilter
,以授權方法呼叫,包括輸入引數和返回值。
Spring Boot Starter Security 預設情況下不啟用方法級別授權。 |
方法安全還支援許多其他用例,包括AspectJ 支援、自定義註解和幾個配置點。考慮學習以下用例:
-
理解方法安全的工作原理及使用它的原因
-
使用
@PreAuthorize
和@PostAuthorize
授權方法 -
在授權被拒絕時提供回退值
-
使用
@PreFilter
和@PostFilter
過濾方法 -
使用JSR-250 註解授權方法
-
使用AspectJ 表示式授權方法
-
自定義SpEL 表示式處理
-
整合自定義授權系統
方法安全的工作原理
Spring Security 的方法授權支援對於以下方面非常方便:
-
提取細粒度的授權邏輯;例如,當方法引數和返回值有助於授權決策時。
-
在服務層強制實施安全
-
從風格上偏好基於註解的配置而非基於
HttpSecurity
的配置
由於方法安全是使用Spring AOP 構建的,您可以利用其所有表達能力,根據需要覆蓋 Spring Security 的預設設定。
如前所述,您可以透過向 @Configuration
類新增 @EnableMethodSecurity
註解或在 Spring XML 配置檔案中新增 <sec:method-security/>
來開始。
這個註解和 XML 元素分別取代了
如果您正在使用 |
方法授權是方法執行前和方法執行後授權的結合。考慮一個以如下方式標註的服務 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
的一次呼叫可能看起來像這樣:

-
Spring AOP 呼叫其
readCustomer
的代理方法。在代理的其他切面中,它會呼叫一個與@PreAuthorize
切入點匹配的AuthorizationManagerBeforeMethodInterceptor
。 -
授權管理器使用
MethodSecurityExpressionHandler
解析註解的SpEL 表示式,並從包含Supplier<Authentication>
和MethodInvocation
的MethodSecurityExpressionRoot
構建相應的EvaluationContext
。 -
攔截器使用此上下文評估表示式;具體來說,它從
Supplier
讀取Authentication
,並檢查其許可權集合中是否包含permission:read
。 -
如果評估透過,則 Spring AOP 繼續呼叫方法。
-
如果不透過,攔截器會發佈一個
AuthorizationDeniedEvent
並丟擲一個AccessDeniedException
,ExceptionTranslationFilter
會捕獲此異常並向響應返回 403 狀態碼。 -
方法返回後,Spring AOP 呼叫一個與
@PostAuthorize
切入點匹配的AuthorizationManagerAfterMethodInterceptor
,其操作與上述相同,但使用的是PostAuthorizeAuthorizationManager
。 -
如果評估透過(在此例中,返回值屬於已登入使用者),則處理正常繼續。
-
如果不透過,攔截器會發佈一個
AuthorizationDeniedEvent
並丟擲一個AccessDeniedException
,ExceptionTranslationFilter
會捕獲此異常並向響應返回 403 狀態碼。
如果方法不是在 HTTP 請求的上下文中被呼叫,您可能需要自己處理 AccessDeniedException 。 |
多個註解按順序計算
如上所示,如果一個方法呼叫涉及多個方法安全註解,每個註解都會被逐個處理。這意味著它們可以被整體視為進行了“與”操作。換句話說,要使呼叫被授權,所有註解檢查都需要透過授權。
每個註解都有自己的方法攔截器
每個註解都有其自己的專用方法攔截器。這樣做的原因是為了提高可組合性。例如,如果需要,您可以停用 Spring Security 的預設設定,只發布 @PostAuthorize
方法攔截器。
方法攔截器如下:
-
對於
@PreAuthorize
,Spring Security 使用AuthorizationManagerBeforeMethodInterceptor#preAuthorize
,後者又使用PreAuthorizeAuthorizationManager
。 -
對於
@PostAuthorize
,Spring Security 使用AuthorizationManagerAfterMethodInterceptor#postAuthorize
,後者又使用PostAuthorizeAuthorizationManager
。 -
對於
@PreFilter
,Spring Security 使用PreFilterAuthorizationMethodInterceptor
。 -
對於
@PostFilter
,Spring Security 使用PostFilterAuthorizationMethodInterceptor
。 -
對於
@Secured
,Spring Security 使用AuthorizationManagerBeforeMethodInterceptor#secured
,後者又使用SecuredAuthorizationManager
。 -
對於 JSR-250 註解,Spring Security 使用
AuthorizationManagerBeforeMethodInterceptor#jsr250
,後者又使用Jsr250AuthorizationManager
。
一般來說,以下列表可視為當您新增 @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')")
@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 例項中宣告一個兜底(catch-all)授權規則。 |
使用註解授權
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 也可以是元註解,可以在類或介面級別定義,並使用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 也可以是元註解,可以在類或介面級別定義,並使用SpEL 授權表示式。 |
@PostAuthorize
在防禦不安全直接物件引用 (Insecure Direct Object Reference) 時特別有用。事實上,它可以被定義為元註解,如下所示:
-
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
}
}
結果是,只有當 Account
的 owner
屬性與已登入使用者的 name
匹配時,上述方法才會返回 Account
物件。否則,Spring Security 將丟擲 AccessDeniedException
並返回 403 狀態碼。
使用 @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
支援陣列、集合、Map 和 Stream(只要 Stream 仍處於開啟狀態)。
例如,上面的 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>
結果是,上述方法將只保留 Account
例項,其中它們的 owner
屬性與已登入使用者的 name
匹配。
使用 @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
支援陣列、集合、Map 和 Stream(只要 Stream 仍處於開啟狀態)。
例如,上面的 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>
結果是,上述方法將返回 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 無法確定您想使用哪一個。
在這種情況下,您可以透過將註解新增到具體方法上解決歧義。
使用元註解
方法安全支援元註解。這意味著您可以採用任何註解,並根據您的應用程式特定用例提高可讀性。
例如,您可以將 @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
}
}
這使得方法定義更具可讀性。
元註解表示式模板化
您還可以選擇使用元註解模板,這允許更強大的註解定義。
首先,釋出以下 Bean:
-
Java
-
Kotlin
@Bean
static AnnotationTemplateExpressionDefaults templateExpressionDefaults() {
return new AnnotationTemplateExpressionDefaults();
}
companion object {
@Bean
fun templateExpressionDefaults(): AnnotationTemplateExpressionDefaults {
return AnnotationTemplateExpressionDefaults()
}
}
現在,您可以建立比 @IsAdmin
更強大的東西,例如 @HasRole
,如下所示:
-
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
的預配置並替換為自己的配置。如果您想自定義 AuthorizationManager
或 Pointcut
,可以選擇這樣做。或者您可能只想啟用某個特定註解,例如 @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>
您可以使用 |
自定義表示式處理
或者,第三種方式是自定義每個 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>
我們使用 |
您也可以繼承DefaultMessageSecurityExpressionHandler
來新增超出預設設定的自定義授權表示式。
與 AOT 協同工作
Spring Security 將掃描應用程式上下文中所有使用 @PreAuthorize
或 @PostAuthorize
的 Bean 的方法。當找到時,它會解析安全表示式中使用的任何 Bean,併為該 Bean 註冊相應的執行時提示 (runtime hints)。如果找到使用 @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 類註冊執行時提示 (runtime hints) |
3 | 找到使用 @AuthorizeReturnObject 的方法後,它將檢視該方法的返回型別中是否存在任何 @PreAuthorize 或 @PostAuthorize |
4 | 然後,它找到一個使用另一個 Bean 名稱(accountAuthz )的 @PreAuthorize ;也會為該 Bean 類註冊執行時提示 (runtime hints) |
5 | 找到另一個 @AuthorizeReturnObject 後,它將再次檢視該方法的返回型別 |
6 | 現在,找到一個使用了另一個 Bean 名稱(myOtherAuthz )的 @PostAuthorize ;也會為該 Bean 類註冊執行時提示 (runtime hints) |
有許多情況下,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 會將其 Advisor 釋出為 AspectJ 通知,以便它們能夠相應地被織入。
指定順序
如前所述,每個註解都有一個 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 | 此方法只能由授予 db 和 ROLE_ADMIN 許可權的 Authentication 呼叫 |
4 | 此方法只能由 Principal 的 aud 宣告等於 "my-audience" 的使用者呼叫 |
5 | 此方法只能在 bean authz 的 check 方法返回 true 時呼叫 |
您可以使用像上面的 |
使用方法引數
此外,Spring Security 提供了一種發現方法引數的機制,以便它們也可以在 SpEL 表示式中訪問。
作為完整參考,Spring Security 使用 DefaultSecurityParameterNameDiscoverer
來發現引數名稱。預設情況下,會對方法嘗試以下選項。
-
如果方法的單個引數上存在 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
實現的,您可以對其進行自定義以支援任何指定註解的 value 屬性。 -
-
如果方法的至少一個引數上存在 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
實現的,您可以對其進行自定義以支援任何指定註解的 value 屬性。 -
-
如果您使用
-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 將嘗試代理任何返回物件,包括 String
、Integer
和其他型別。這通常不是您想要做的事情。
如果您想在方法返回值型別(例如 int
、String
、Double
或這些型別的集合)的類或介面上使用 @AuthorizeReturnObject
,那麼您還應該釋出相應的 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())
}
您可以設定自己的 |
程式設計式代理
您也可以程式設計式地代理給定物件。
為此,您可以自動注入提供的 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 和迭代器,方法是代理元素型別;支援 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
的 Advisor,因此您通常無需執行任何操作即可啟用此功能。
使用 |
自定義 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 協作
此功能的一個強大用途是從控制器返回一個安全的 value,如下所示
-
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)
}
}
您需要新增一個 MethodAuthorizationDeniedHandler
,如下所示
-
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 屬性 |
與 AOT 協作
Spring Security 將掃描應用程式上下文中所有使用 @AuthorizeReturnObject
方法的 Bean。當找到時,它將提前建立並註冊相應的代理類。它還將遞迴搜尋其他巢狀物件,如果它們也使用 @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
。相反,您可能希望返回一個後處理的結果,例如一個經過掩碼處理的結果,或者在授權拒絕發生在方法呼叫之前的情況下返回一個預設值。
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 | 建立一個返回 null 值的 MethodAuthorizationDeniedHandler 實現 |
2 | 將 NullMethodAuthorizationDeniedHandler 註冊為 Bean |
3 | 使用 @HandleAuthorizationDenied 註解方法,並將 NullMethodAuthorizationDeniedHandler 傳遞給 handlerClass 屬性 |
然後您可以驗證返回的是 null
值而不是 AccessDeniedException
您也可以使用 |
-
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,因此如果您需要更復雜的邏輯,可以將依賴項注入到其中。除此之外,您還可以使用 MethodInvocation
或 MethodInvocationResult
,以及 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
}
}