方法安全
除了在请求层面建模授权之外,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 找到了 AccountServiceBean | 
| 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 找到 MyControllerBean | 
| 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
    }
}