方法安全(Method Security)

本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

除了 在请求级别对授权进行建模 外,Spring Security 还支持在方法级别对授权进行建模。

你可以通过在任何 @Configuration 类中注解 @EnableMethodSecurity 或在任何 XML 配置文件中添加 <method-security> 来激活它:

  • Java

  • Kotlin

  • Xml

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

然后,你就可以立即用 @PreAuthorize@PostAuthorize@PreFilter@PostFilter 注解任何 Spring 管理的类或方法,以授权方法调用,包括参数和返回值。

Spring Boot Starter Security 默认不激活方法级授权。

Method Security 还支持许多其他用例,包括 AspectJ 支持自定义注解 和多个配置点。考虑学习以下用例:

方法安全的工作原理

Spring Security 的方法授权支持在以下方面非常方便:

  • 提取细粒度的授权逻辑;例如,当方法参数和返回值有助于授权决策时。

  • 在服务层加强安全性。

  • 在风格上,基于注解的配置优于基于 HttpSecurity 的配置。

由 于Method Security 是使用 Spring AOP 构建的,因此你可以使用它所有的表达能力,根据需要覆盖 Spring Security 的默认值。

如前所述,首先要在 @Configuration 类或 Spring XML 配置文件中的 <sec:method-security/> 中添加 @EnableMethodSecurity

该注解和 XML 元素分别取代了 @EnableGlobalMethodSecurity<sec:global-method-security/>。它们提供了以下改进:

  1. 使用简化的 AuthorizationManager API,而不是元数据源、配置属性、决策管理器和投票器。这简化了重用和定制。

  2. 倾向于直接基于Bean的配置,而不是需要继承 GlobalMethodSecurityConfiguration 来定制Bean。

  3. 使用本地 Spring AOP 构建,移除抽象概念,并允许你使用 Spring AOP 构建模块来定制。

  4. 检查相互冲突的注解,确保安全配置的明确性。

  5. 符合JSR-250标准。

  6. 默认启用 @PreAuthorize@PostAuthorize@PreFilter@PostFilter

如果你正在使用 @EnableGlobalMethodSecurity<global-method-security/>,它们现在已被弃用,我们鼓励你迁移它们。

方法授权是方法“前”授权和方法“后”授权的结合。考虑一个按以下方式注解的 service bean:

  • Java

  • Kotlin

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

当方法安全 被激活 时,对 MyCustomerService#readCustomer 的给定调用可能如下所示:

methodsecurity
  1. Spring AOP 为 readCustomer 调用了代理方法。在代理的其他 advice 中,它调用了一个 AuthorizationManagerBeforeMethodInterceptor,该方法与 @PreAuthorize pointcut 匹配。

  2. 拦截器调用 PreAuthorizeAuthorizationManager#check

  3. 授权管理器使用 MethodSecurityExpressionHandler 解析注解的 SpEL 表达式,并从包含 Supplier<Authentication>MethodInvocationMethodSecurityExpressionRoot 构建相应的 EvaluationContext

  4. 拦截器使用该上下文来评估表达式;具体地说,它从 Supplier 读取 Authentication,并检查其 权限 集合中是否有 permission:read

  5. 如果评估通过,Spring AOP 将继续调用该方法。

  6. 如果没有,拦截器会发布一个 AuthorizationDeniedEvent,并抛出一个 AccessDeniedExceptionExceptionTranslationFilter 会捕获并向响应返回一个403状态码。

  7. 在方法返回后,Spring AOP 调用一个与 @PostAuthorize pointcut 相匹配的 AuthorizationManagerAfterMethodInterceptor,操作与上面相同,但使用了 PostAuthorizeAuthorizationManager

  8. 如果评估通过(在这种情况下,返回值属于登录的用户),处理继续正常进行。

  9. \如果没有,拦截器会发布一个 AuthorizationDeniedEvent,并抛出一个 AccessDeniedExceptionExceptionTranslationFilter 会捕获并向响应返回一个 403 状态码。

如果该方法不是在 HTTP 请求的上下文中被调用,你可能需要自己处理 AccessDeniedException

多个注解串联计算

如上所示,如果方法调用涉及多个 方法安全注解,则每个注解一次处理一个。这意味着它们可以被视为 "&&" 在一起。换句话说,要对调用进行授权,所有注解检查都需要通过授权。

不支持重复注解

也就是说,不支持在同一个方法上重复相同的注解。例如,你不能在同一个方法上重复 @PreAuthorize

相反,请使用SpEL的布尔支持或其对委托给单独Bean的支持。

每个注解都有自己的 Pointcut

每个注解都有自己的 pointcut 实例,从方法及其外层类开始,在整个对象层次结构中查找该注解或其对应的 元注解

你可以在 AuthorizationMethodPointcuts 中查看具体细节。

每个注解都有自己的方法拦截器

每个注解都有自己专用的方法拦截器。这样做的原因是为了提高可组合性。例如,如果需要,你可以禁用 Spring Security 默认值, 只发布 @PostAuthorize 方法拦截器

方法拦截器如下

一般来说,当你添加 @EnableMethodSecurity 时,你可以将下面的列表视为 Spring Security 发布的拦截器的代表:

  • Java

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

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

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

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

将授权优先于复杂的SpEL表达式

通常情况下,很容易引入像下面这样复杂的SpEL表达式:

  • Java

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

然而,你可以将权限 permission:read 授予那些具有 ROLE_ADMIN 的用户。一种方法是使用角色层次结构(RoleHierarchy)来实现:

  • Java

  • Kotlin

  • Xml

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

然后将其设置在 MethodSecurityExpressionHandler 实例中。这样,你就可以像这样使用更简单的 @PreAuthorize 表达式:

  • Java

  • Kotlin

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

或者,在可能的情况下,在登录时将特定于应用程序的授权逻辑调整到授予的权限中。

请求级授权与方法级授权的比较

在什么情况下,方法级授权应优先于 请求级授权?这其中有一部分取决于你的喜好;不过,你可以考虑下面列出的每种授权的优势,以帮助你做出决定。

请求级

方法级

授权类型

粗粒度

细粒度

配置位置

在配置类中声明

局部到方法声明

配置方式

DSL

注解

授权的定义

编程式

SpEL

主要的权衡因素似乎是你希望你的授权规则位于何处。

重要的是要记住,当你使用基于注解的方法安全时,未注解的方法是不安全的。为了防止这种情况,请在 HttpSecurity 实例中声明一个 全面的授权规则

使用注解授权

Spring Security 实现方法级授权支持的主要方式是通过注解,你可以将注解添加到方法、类和接口中。

使用 @PreAuthorize 授权方法调用

方法安全激活 时,你可以使用 @PreAuthorize 注解方法:

  • Java

  • Kotlin

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

这表示只有当所提供的表达式 hasRole('ADMIN') 通过时才能调用该方法。

然后你可以像这样 测试该类,以确认它执行了授权规则:

  • Java

  • Kotlin

@Autowired
BankService bankService;

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

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

@WithMockUser(roles="WRONG")
@Test
fun readAccountWithWrongRoleThenAccessDenied() {
    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
        this.bankService.readAccount("12345678")
    }
}
@PreAuthorize 也可以是 元注解,定义在 类或接口 级别,并使用 SpEL 授权表达式

虽然 @PreAuthorize 对于声明所需的授权很有帮助,但它也可用于评估 涉及方法参数 的更复杂的表达式。

以上两个代码段通过将 username 参数与 Authentication#getName 进行比较,确保用户只能请求属于自己的订单。

其结果是,只有当请求路径中的 username 与登录用户 name 相匹配时,上述方法才会被调用。否则,Spring Security 将抛出 AccessDeniedException 并返回 403 状态码。

使用 @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 在防御 不安全的直接对象引用 时特别有用。事实上,可以将其定义为类似的 元注解

  • Java

  • Kotlin

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

你可以通过以下方式对服务进行注解:

  • Java

  • Kotlin

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

结果是,上述方法只有在账户的 owner 属性与登录用户的 name 匹配时才会返回 Account。否则,Spring Security 将抛出 AccessDeniedException 并返回 403 状态码。

使用 @PreFilter 过滤方法参数

@PreFilter 还不支持特定于 Kotlin 的数据类型;因此,只显示了 Java 片段。

当方法安全激活时,你可以使用 @PreFilter 注解方法:

  • Java

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

这是为了过滤掉 filterObject.owner == authentication.name 表达式失败的 accounts 中的任何值。filterObject 代表 accounts 中的每个 account,用于测试每个 account

然后,你可以通过以下方式测试该类,以确认它执行了授权规则:

  • Java

@Autowired
BankService bankService;

@WithMockUser(username="owner")
@Test
void updateAccountsWhenOwnedThenReturns() {
    Account ownedBy = ...
    Account notOwnedBy = ...
    Collection<Account> updated = this.bankService.updateAccounts(ownedBy, notOwnedBy);
    assertThat(updated).containsOnly(ownedBy);
}
@PreFilter 也可以是 元注解,定义在 类或接口级别,并使用 SpEL 授权表达式

@PreFilter 支持数组、集合、map 和 stream(只要s tream 仍然打开)。

例如,上面的 updateAccounts 声明与下面的其他四个声明的方法相同:

  • Java

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

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

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

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

其结果是,上述方法将只拥有其 owner 属性与登录用户 name 相匹配的 Account 实例。

使用 @PostFilter 过滤方法结果

@PostFilter 还不支持特定于 Kotlin 的数据类型;因此,只显示了 Java 片段。

当方法安全处于激活状态时,你可以使用 @PostFilter 注解方法:

  • Java

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

这是为了从 filterObject.owner == authentication.name 表达式失败的返回值中过滤掉任何值。filterObject 代表 accounts 中的每个 account,用于测试每个 account

然后你可以像这样测试该类,以确认它执行了授权规则:

  • Java

@Autowired
BankService bankService;

@WithMockUser(username="owner")
@Test
void readAccountsWhenOwnedThenReturns() {
    Collection<Account> accounts = this.bankService.updateAccounts("owner", "not-owner");
    assertThat(accounts).hasSize(1);
    assertThat(accounts.get(0).getOwner()).isEqualTo("owner");
}
@PostFilter 也可以是 元注解,定义在 类或接口级别 ,并使用 SpEL 授权表达式

@PostFilter 支持数组、集合、map 和 stream(只要 stream 仍然打开)。

例如,上面的 readAccounts 声明与下面其他三个声明的函数相同:

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

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

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

结果是,上述方法将返回其 owner 属性与登录用户 name 相匹配的 Account 实例。

内存过滤显然会很昂贵,因此要考虑是否最好 在数据层中过滤数据

使用 @Secured 授权方法调用

@Secured 是授权调用的传统选项。我们推荐使用 @PreAuthorize 来取代它。

要使用 @Secured 注解,应首先更改方法的安全声明,使其能够像这样使用:

  • Java

  • Kotlin

  • Xml

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

这将导致 Spring Security 发布 相应的方法拦截器,该拦截器授权使用 @Secured 注解的方法、类和接口。

使用 JSR-250 注解授权方法调用

如果你想使用 JSR-250 注解,Spring Security 也支持。@PreAuthorize 的表达能力更强,因此推荐使用。

要使用 JSR-250 注解,应首先更改 Method Security 声明以启用注解:

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

这使得方法定义更加易读。

启用某些注解

你可以关闭 @EnableMethodSecurity 的预配置并用你自己的配置替换它。如果你想 自定义 AuthorizationManagerPointcut,你可以选择这样做。或者你可能只想启用特定的注解,如 @PostAuthorize

你可以通过以下方式实现:

Only @PostAuthorize Configuration
  • Java

  • Kotlin

  • Xml

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

<aop:config/>

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

上面的代码段首先禁用了 Method Security 的预配置,然后发布了 @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>
这仅支持通过前缀或名称匹配方法。如果你的需求比这更复杂,请使用 注解支持

以编程方式授权方法

正如你已经看到的,有几种方法可以让你使用 Method Security SpEL 表达式 指定非细腻度的授权规则。

有许多方法可以让你的逻辑基于Java而不是基于SpEL。这样就可以使用整个Java语言来提高可测试性和流程控制。

在SpEL中使用自定义Bean

通过编程授权方法的第一种方法是一个两步过程。

首先,声明一个 Bean,该 Bean 的方法需要一个 MethodSecurityExpressionOperations 实例,如下所示:

  • Java

  • Kotlin

@Component("authz")
public class AuthorizationLogic {
    public boolean decide(MethodSecurityExpressionOperations operations) {
        // ... authorization logic
    }
}
@Component("authz")
open class AuthorizationLogic {
    fun decide(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语言。

使用自定义授权管理器

以编程方式授权方法的第二种方法是创建一个自定义的 AuthorizationManager

首先,声明一个授权管理器实例,也许像这样:

  • Java

  • Kotlin

@Component
public class MyAuthorizationManager implements AuthorizationManager<MethodInvocation> {
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
        // ... authorization logic
    }
}
@Component("authz")
open class MyAuthorizationManager: AuthorizationManager<MethodInvocation> {
    fun check(val authentication: Supplier<Authentication>, val invocation: MethodInvocation): AuthorizationDecision {
        // ... authorization logic
    }
}

然后,发布方法拦截器,并在其上添加一个与你希望授权管理器运行时间相对应的快捷方式。例如,你可以这样替换 @PreAuthorize@PostAuthorize 的工作方式:

Only @PostAuthorize Configuration
  • Java

  • Kotlin

  • Xml

@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
    @Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	Advisor postAuthorize(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="postAuthorize"
	class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor"
	factory-method="preAuthorize">
    <constructor-arg ref="myAuthorizationManager"/>
</bean>

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

你可以使用 AuthorizationInterceptorsOrder 中指定顺序的常量将你的拦截器放在 Spring Security 方法拦截器之间。

自定义表达式处理

或者,第三,你可以自定义如何处理每个SpEL表达式。为此,你可以暴露一个自定义的 MethodSecurityExpressionHandler,就像这样:

Custom MethodSecurityExpressionHandler
  • Java

  • Kotlin

  • Xml

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

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

我们使用 static 方法暴露 MethodSecurityExpressionHandler,以确保 Spring 在初始化 Spring Security 的方法安全 @Configuration 类之前发布它。

You can also subclass DefaultMessageSecurityExpressionHandler to add your own custom authorization expressions beyond the defaults.

使用 AspectJ 授权

使用自定义的 Pointcut 匹配方法

基于 Spring AOP 构建,你可以声明与注解无关的模式,类似于 请求级授权。这具有集中方法级授权规则的潜在优势。

例如,你可以使用发布自己的 Advisor 或使用 <protect-pointcut> 将 AOP 表达式与服务层的授权规则相匹配:

  • Java

  • Kotlin

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

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor protectServicePointcut() {
    JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut();
    pattern.setPattern("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 {
        var pattern = JdkRegexpMethodPointcut();
        pattern.setPattern("execution(* com.mycompany.*Service.*(..))");
        return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER"));
    }
}
<sec:method-security>
    <protect-pointcut expression="execution(* com.mycompany.*Service.*(..))" access="hasRole('USER')"/>
</sec:method-security>

与 AspectJ 字节码织入集成

使用 AspectJ 将 Spring Security advice 织入到 Bean 的字节码中,有时可以提高性能。

设置 AspectJ 后,你可以在 @EnableMethodSecurity 注解或 <method-security> 元素中简单地说明你正在使用 AspectJ:

  • Java

  • Kotlin

  • Xml

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

其结果是,Spring Security 将以 AspectJ advice 的形式发布其 advisor,以便相应地织入它们。

指定顺序

如前所述,每个注解都有一个 Spring AOP 方法拦截器,每个方法在 Spring AOP advisor 链中都有一个位置。

也就是说,@PreFilter 方法拦截器的顺序是 100,@PreAuthorize 的顺序是 200,以此类推。

之所以需要注意这一点,是因为还有其他基于 AOP 的注解,如 @EnableTransactionManagement,其顺序为 Integer.MAX_VALUE。换句话说,它们默认位于 advisor 链的末尾。

有时,在 Spring Security 之前执行其他 advice 可能很有价值。例如,如果你有一个注解了 @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,设置为 0 意味着事务 advice 将在所有Spring Security advice 之前运行。

使用 SpEL 表达授权

你已经看过几个使用SpEL的例子,现在让我们更深入地介绍一下API。

Spring Security 将所有授权字段和方法封装在一组根(root)对象中。最通用的根对象称为 SecurityExpressionRoot,它构成了 MethodSecurityExpressionRoot 的基础。当准备评估授权表达式时,Spring Security 会将该根对象提供给 MethodSecurityEvaluationContext

使用授权表达式字段和方法

这首先为你的 SpEL 表达式提供了一组增强的授权字段和方法。下面是对最常用方法的快速概述:

  • permitAll - 该方法无需授权即可调用;请注意,在这种情况下,将不会从 session 中获取 Authentication 信息。

  • denyAll - 在任何情况下都不允许使用该方法;请注意,在这种情况下,将永远不会从 session 中检索 Authentication

  • hasAuthority - 该方法要求 AuthenticationGrantedAuthority 符合给定值。

  • hasRole - hasAuthority 的快捷方式,前缀为 ROLE_ 或任何配置为默认前缀的内容。

  • hasAnyAuthority - 该方法要求 AuthenticationGrantedAuthority 符合任何给定值。

  • hasAnyRole - hasAnyAuthority 的快捷方式,前缀为 ROLE_ 或任何配置为默认前缀的内容。

  • hasPermission - 用于对象级授权的 PermissionEvaluator 实例的钩子。

下面简要介绍最常见的字段:

  • authentication - 与此方法调用相关的 Authentication 实例。

  • principal - 与此方法调用相关的 Authentication#getPrincipal

在学习了模式、规则以及如何将它们搭配在一起之后,你应该能够理解这个更复杂的示例中发生了什么:

Authorize Requests
  • Java

  • Kotlin

  • Xml

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

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

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

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

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

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

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

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

    @PreAuthorize("@authz.check(#root)")
    fun shareResource(...): MyResource;
}
<sec:method-security>
    <protect-pointcut expression="execution(* com.mycompany.*Service.myDeprecatedMethod(..))" access="denyAll"/> (1)
    <protect-pointcut expression="execution(* com.mycompany.*Service.writeResource(..))" access="hasRole('ADMIN')"/> (2)
    <protect-pointcut expression="execution(* com.mycompany.*Service.deleteResource(..))" access="hasAuthority('db') and hasRole('ADMIN')"/> (3)
    <protect-pointcut expression="execution(* com.mycompany.*Service.readResource(..))" access="principal.claims['aud'] == 'my-audience'"/> (4)
    <protect-pointcut expression="execution(* com.mycompany.*Service.shareResource(..))" access="@authz.check(#root)"/> (5)
</sec:method-security>
1 任何人不得以任何理由调用该方法。
2 该方法只能由被授予 ROLE_ADMIN 权限的 Authentication 调用。
3 该方法只能由被授予 dbROLE_ADMIN 权限的 Authentication 调用。
4 本方法仅可由 aud claim 等于 "my-audience" 的 Princpal 调用。
5 只有当 Bean authzcheck 方法返回 true 时,才能调用该方法。

使用方法参数

此外,Spring Security 还提供了一种发现方法参数的机制,因此也可以在 SpEL 表达式中访问这些参数。

对于完整的参考,Spring Security 使用 DefaultSecurityParameterNameDiscoverer 来发现参数名。默认情况下,方法会尝试以下选项。

  1. 如果 Spring Security 的 @P 注解出现在方法的单个参数上,则使用该值。下面的示例使用了 @P 注解:

    • Java

    • Kotlin

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

    该表达式的目的是要求当前 Authentication 具有专门针对此 Contact 实例的 write 权限。

    在幕后,这是通过使用 AnnotationParameterNameDiscoverer 来实现的,你可以自定义 AnnotationParameterNameDiscoverer 以支持任何指定注解的值属性。

    • 如果 Spring Data@Param 注解存在于方法的至少一个参数上,则使用该值。下面的示例使用了 @Param 注解:

      • Java

      • Kotlin

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

      该表达式的目的是要求 name 等于 Authentication#getName 才能对调用进行授权。

      在幕后,这是通过使用 AnnotationParameterNameDiscoverer 来实现的,你可以自定义 AnnotationParameterNameDiscoverer 以支持任何指定注解的值属性。

    • 如果你使用 -parameters 参数编译代码,标准的JDK反射API将被用来发现参数名。这对类和接口都有效。

    • 最后,如果使用 debug 符号编译代码,参数名将通过 debug 符号被发现。这对接口无效,因为它们没有关于参数名的 debug 信息。对于接口,必须使用注解或 -parameters 方法。

@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 时,Spring Security 会自动利用这一点。

然而,假设你的代码继承了 DefaultMethodSecurityExpressionHandler 并重载了 createSecurityExpressionRoot(Authentication, MethodInvocation) 以返回自定义的 SecurityExpressionRoot 实例。这将不再工作,因为 @EnableMethodSecurity 设置的安排调用 createEvaluationContext(Supplier<Authentication>, MethodInvocation) 代替。

令人欣慰的是,这种级别的自定义通常是不必要的。相反,你可以创建一个具有你所需的授权方法的自定义Bean。

例如,假设你希望自定义评估 @PostAuthorize("hasAuthority('ADMIN')")。你可以创建一个像这样的自定义 @Bean

  • Java

  • Kotlin

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

然后像这样在注解中引用它:

  • Java

  • Kotlin

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

我仍然倾向于继承 DefaultMethodSecurityExpressionHandler

如果你必须继续继承 DefaultMethodSecurityExpressionHandler,你仍然可以这样做。相反,像这样覆盖 createEvaluationContext(Supplier<Authentication>, MethodInvocation) 方法:

  • Java

  • Kotlin

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

延伸阅读

现在你已经确保了应用程序 请求的安全,如果还没有,请确保其请求的安全。你还可以进一步阅读有关 测试应用程序 或将 Spring Security 与应用程序的其他方面(如 数据层跟踪和度量)集成的内容。