对于最新的稳定版本,请使用 Spring Security 6.3.1Spring中文文档

对于最新的稳定版本,请使用 Spring Security 6.3.1Spring中文文档

除了在请求级别建模授权外,Spring Security 还支持在方法级别建模。Spring中文文档

您可以通过使用任何 XML 配置文件注释任何类或添加到任何 XML 配置文件来激活它,如下所示:@Configuration@EnableMethodSecurity<method-security>Spring中文文档

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

然后,您可以立即使用 @PreAuthorize@PostAuthorize@PreFilter@PostFilter 注释任何 Spring 托管的类或方法,以授权方法调用,包括输入参数和返回值。Spring中文文档

默认情况下,Spring Boot Starter Security 不会激活方法级授权。

Method Security 还支持许多其他用例,包括 AspectJ 支持自定义注释和多个配置点。 请考虑了解以下用例:Spring中文文档

默认情况下,Spring Boot Starter Security 不会激活方法级授权。

方法安全性的工作原理

Spring Security 的方法授权支持非常方便:Spring中文文档

由于 Method Security 是使用 Spring AOP 构建的,因此您可以根据需要访问其所有表达能力来覆盖 Spring Security 的默认值。Spring中文文档

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

此注释和 XML 元素分别取代 和 。 它们提供以下改进:@EnableGlobalMethodSecurity<sec:global-method-security/>Spring中文文档

  1. 使用简化的 API,而不是元数据源、配置属性、决策管理器和投票者。 这简化了重用和自定义。AuthorizationManagerSpring中文文档

  2. 支持直接基于 Bean 的配置,而不需要扩展来定制 BeanGlobalMethodSecurityConfigurationSpring中文文档

  3. 使用原生 Spring AOP 构建,删除抽象并允许您使用 Spring AOP 构建块进行自定义Spring中文文档

  4. 检查冲突的注释,以确保明确的安全配置Spring中文文档

  5. 符合JSR-250Spring中文文档

  6. 默认启用 、 、 和@PreAuthorize@PostAuthorize@PreFilter@PostFilterSpring中文文档

如果您使用的是 ,则这些工具现在已弃用,我们鼓励您迁移。@EnableGlobalMethodSecurity<global-method-security/>Spring中文文档

方法授权是方法前授权和方法后授权的组合。 考虑按以下方式注释的服务 Bean:Spring中文文档

@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#readCustomerSpring中文文档

方法安全性
  1. Spring AOP 调用其代理方法。在代理的其他顾问中,它调用与@PreAuthorize点切口匹配的 AuthorizationManagerBeforeMethodInterceptorreadCustomerSpring中文文档

  2. 拦截器调用 PreAuthorizeAuthorizationManager#checkSpring中文文档

  3. 授权管理器使用 a 来解析注释的 SpEL 表达式,并从包含 Supplier<Authentication> 和 的 a 构造相应的表达式。MethodSecurityExpressionHandlerEvaluationContextMethodSecurityExpressionRootMethodInvocationSpring中文文档

  4. 侦听器使用此上下文来计算表达式;具体来说,它从中读取身份验证,并检查它是否在其权限集合中Supplierpermission:readSpring中文文档

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

  6. 如果没有,拦截器将发布并引发 AccessDeniedException,ExceptionTranslationFilter 会捕获该 ExceptionTranslationFilter 并将 403 状态代码返回给响应AuthorizationDeniedEventSpring中文文档

  7. 该方法返回后,Spring AOP 调用@PostAuthorize pointcut 匹配的 AuthorizationManagerAfterMethodInterceptor,其操作与上述相同,但使用 PostAuthorizeAuthorizationManagerSpring中文文档

  8. 如果评估通过(在本例中,返回值属于登录用户),则处理将继续正常进行Spring中文文档

  9. 如果没有,拦截器将发布并引发 AccessDeniedException,ExceptionTranslationFilter 将捕获该 ExceptionTranslationFilter 并将 403 状态代码返回给响应AuthorizationDeniedEventSpring中文文档

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

多个注释串联计算

如上所述,如果方法调用涉及多个方法安全性注释,则每次处理一个。 这意味着它们可以被统称为“和”在一起。 换言之,要授权调用,所有注释检查都需要通过授权。Spring中文文档

不支持重复批注

也就是说,不支持在同一方法上重复相同的注释。 例如,不能在同一方法上放置两次。@PreAuthorizeSpring中文文档

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

每个注释都有自己的切入点

每个批注都有自己的切口实例,该实例从方法及其封闭类开始,在整个对象层次结构中查找该批注或其元批注对应项。Spring中文文档

您可以在 AuthorizationMethodPointcuts 中看到此细节。Spring中文文档

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

每个注释都有自己的专用方法拦截器。 这样做的原因是使事情更易于组合。 例如,如果需要,您可以禁用 Spring Security 默认值并仅发布 @PostAuthorize 方法拦截器Spring中文文档

方法拦截器如下:Spring中文文档

一般来说,您可以将以下列表视为 Spring Security 在添加时发布的拦截器的代表:@EnableMethodSecuritySpring中文文档

@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 表达式可能很诱人,如下所示:Spring中文文档

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

但是,您可以改为授予那些具有 . 一种方法是像这样:permission:readROLE_ADMINRoleHierarchySpring中文文档

@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表达式,如下所示:Spring中文文档

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

或者,在可能的情况下,在登录时将特定于应用程序的授权逻辑调整为授予的授权机构。Spring中文文档

此注释和 XML 元素分别取代 和 。 它们提供以下改进:@EnableGlobalMethodSecurity<sec:global-method-security/>Spring中文文档

  1. 使用简化的 API,而不是元数据源、配置属性、决策管理器和投票者。 这简化了重用和自定义。AuthorizationManagerSpring中文文档

  2. 支持直接基于 Bean 的配置,而不需要扩展来定制 BeanGlobalMethodSecurityConfigurationSpring中文文档

  3. 使用原生 Spring AOP 构建,删除抽象并允许您使用 Spring AOP 构建块进行自定义Spring中文文档

  4. 检查冲突的注释,以确保明确的安全配置Spring中文文档

  5. 符合JSR-250Spring中文文档

  6. 默认启用 、 、 和@PreAuthorize@PostAuthorize@PreFilter@PostFilterSpring中文文档

如果您使用的是 ,则这些工具现在已弃用,我们鼓励您迁移。@EnableGlobalMethodSecurity<global-method-security/>Spring中文文档

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

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

什么时候应该优先使用方法级授权而不是请求级授权? 其中一些归结为味道;但是,请考虑以下每种优势列表,以帮助您做出决定。Spring中文文档

请求级别Spring中文文档

方法级Spring中文文档

授权类型Spring中文文档

粗粒度Spring中文文档

细粒度Spring中文文档

配置位置Spring中文文档

在 config 类中声明Spring中文文档

Local to 方法声明Spring中文文档

配置样式Spring中文文档

DSL的Spring中文文档

附注Spring中文文档

授权定义Spring中文文档

编程Spring中文文档

SpEL系列Spring中文文档

主要的权衡似乎是您希望授权规则所在的位置。Spring中文文档

请务必记住,当您使用基于批注的方法安全性时,未批注的方法将不受保护。 若要防止这种情况,请在 HttpSecurity 实例中声明一个 catch-all 授权规则

请求级别Spring中文文档

方法级Spring中文文档

授权类型Spring中文文档

粗粒度Spring中文文档

细粒度Spring中文文档

配置位置Spring中文文档

在 config 类中声明Spring中文文档

Local to 方法声明Spring中文文档

配置样式Spring中文文档

DSL的Spring中文文档

附注Spring中文文档

授权定义Spring中文文档

编程Spring中文文档

SpEL系列Spring中文文档

请务必记住,当您使用基于批注的方法安全性时,未批注的方法将不受保护。 若要防止这种情况,请在 HttpSecurity 实例中声明一个 catch-all 授权规则

使用注释进行授权

Spring Security 启用方法级授权支持的主要方式是通过可以添加到方法、类和接口的注释。Spring中文文档

授权方法调用@PreAuthorize

“方法安全性”处于活动状态时,可以使用@PreAuthorize注释对方法进行注释,如下所示:Spring中文文档

@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')Spring中文文档

然后,您可以测试该类以确认它正在强制执行授权规则,如下所示:Spring中文文档

@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 授权表达式

虽然对于声明所需的权限非常有帮助,但它也可用于评估涉及方法参数的更复杂的表达式@PreAuthorizeSpring中文文档

授权方法结果@PostAuthorize

当“方法安全性”处于活动状态时,可以使用@PostAuthorize注释对方法进行注释,如下所示:Spring中文文档

@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.namereturnObjectAccountSpring中文文档

然后,您可以测试该类以确认它正在强制执行授权规则:Spring中文文档

@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在防御不安全的直接对象引用时特别有用。 事实上,它可以被定义为一个元注释,如下所示:Spring中文文档

@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

允许您改为按以下方式批注服务:Spring中文文档

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

结果是,上述方法仅在其属性与登录用户的 . 如果没有,Spring Security 将抛出并返回 403 状态代码。AccountownernameAccessDeniedExceptionSpring中文文档

过滤方法参数@PreFilter

@PreFilter尚不支持特定于 Kotlin 的数据类型;因此,仅显示 Java 代码片段

当“方法安全性”处于活动状态时,可以使用@PreFilter注释对方法进行注释,如下所示:Spring中文文档

@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;
	}
}

这是为了从表达式失败的地方过滤掉任何值。 表示每个 in,并用于测试每个 。accountsfilterObject.owner == authentication.namefilterObjectaccountaccountsaccountSpring中文文档

然后,可以通过以下方式测试该类,以确认它正在强制执行授权规则:Spring中文文档

@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支持数组、集合、映射和流(只要流仍处于打开状态)。Spring中文文档

例如,上述声明的功能与以下四个声明相同:updateAccountsSpring中文文档

@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)

结果是,上述方法将仅具有其属性与登录用户的 .AccountownernameSpring中文文档

过滤方法结果@PostFilter

@PostFilter尚不支持特定于 Kotlin 的数据类型;因此,仅显示 Java 代码片段

当“方法安全性”处于活动状态时,可以使用@PostFilter注释对方法进行注释,如下所示:Spring中文文档

@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;
	}
}

这是为了从表达式失败的返回值中筛选出任何值。 表示每个 in,并用于测试每个 。filterObject.owner == authentication.namefilterObjectaccountaccountsaccountSpring中文文档

然后,您可以像这样测试类,以确认它正在强制执行授权规则:Spring中文文档

@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支持数组、集合、映射和流(只要流仍处于打开状态)。Spring中文文档

例如,上述声明的功能与以下三个声明相同:readAccountsSpring中文文档

@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)

结果是,上述方法将返回其属性与登录用户的 .AccountownernameSpring中文文档

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

授权方法调用@Secured

@Secured 是用于授权调用的旧选项。@PreAuthorize取代了它,而是推荐使用。Spring中文文档

若要使用注释,应首先更改方法安全性声明以启用它,如下所示:@SecuredSpring中文文档

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

这将导致 Spring Security 发布相应的方法拦截器,该拦截器授权用 .@SecuredSpring中文文档

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

如果您想使用 JSR-250 注解,Spring Security 也支持它。@PreAuthorize具有更强的表现力,因此值得推荐。Spring中文文档

要使用 JSR-250 注解,您应该首先更改 Method Security 声明以启用它们,如下所示:Spring中文文档

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

这将导致 Spring Security 发布相应的方法拦截器,该拦截器授权用 、 和 注释的方法、类和接口。@RolesAllowed@PermitAll@DenyAllSpring中文文档

在类或接口级别声明注释

还支持在类和接口级别使用方法安全性批注。Spring中文文档

如果它像这样在类级别:Spring中文文档

@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 { ... }
}

然后,所有方法都继承类级行为。Spring中文文档

或者,如果它在类和方法级别都声明如下所示:Spring中文文档

@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中文文档

接口也是如此,不同之处在于,如果一个类从两个不同的接口继承注释,则启动将失败。 这是因为 Spring Security 无法判断您要使用哪一个。Spring中文文档

在这种情况下,您可以通过将注释添加到具体方法来解决歧义。Spring中文文档

使用元注释

方法安全性支持元注释。 这意味着您可以根据特定于应用程序的用例进行任何注释并提高可读性。Spring中文文档

例如,您可以简化为:@PreAuthorize("hasRole('ADMIN')")@IsAdminSpring中文文档

@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

结果是,在您的安全方法上,您现在可以执行以下操作:Spring中文文档

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

这样可以使方法定义更具可读性。Spring中文文档

启用某些注释

您可以关闭 的预配置并将其替换为您自己的预配置。 如果要自定义 AuthorizationManager 或 . 或者,您可能只想启用特定的注释,例如 .@EnableMethodSecurityPointcut@PostAuthorizeSpring中文文档

您可以通过以下方式执行此操作:Spring中文文档

仅@PostAuthorize配置
@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"/>

上面的代码片段通过首先禁用 Method Security 的预配置,然后发布@PostAuthorize拦截器本身来实现此目的。Spring中文文档

@PreAuthorize也可以是元注释在类或接口级别定义,并使用 SpEL 授权表达式
@PostAuthorize也可以是元注释在类或接口级别定义,并使用 SpEL 授权表达式
@PreFilter尚不支持特定于 Kotlin 的数据类型;因此,仅显示 Java 代码片段
@PreFilter也可以是元注释在类或接口级别定义,并使用 SpEL 授权表达式
@PostFilter尚不支持特定于 Kotlin 的数据类型;因此,仅显示 Java 代码片段
@PostFilter也可以是元注释在类或接口级别定义,并使用 SpEL 授权表达式
内存中过滤显然可能很昂贵,因此请考虑是否最好在数据层中过滤数据

授权方式<intercept-methods>

虽然使用 Spring Security 的基于注释的支持是方法安全性的首选,但您也可以使用 XML 来声明 Bean 授权规则。Spring中文文档

如果需要在 XML 配置中声明它,可以使用 <intercept-methods>如下所示:Spring中文文档

<bean class="org.mycompany.MyController">
    <intercept-methods>
        <protect method="get*" access="hasAuthority('read')"/>
        <protect method="*" access="hasAuthority('write')"/>
    </intercept-methods>
</bean>
这仅支持按前缀或名称匹配方法。 如果您的需求比这更复杂,请改用注释支持
这仅支持按前缀或名称匹配方法。 如果您的需求比这更复杂,请改用注释支持

以编程方式授权方法

如您所见,有几种方法可以使用方法安全性 SpEL 表达式指定重要的授权规则。Spring中文文档

有许多方法可以改为允许逻辑基于 Java 而不是基于 SpEL。 这样就可以访问整个 Java 语言,以提高可测试性和流控制。Spring中文文档

在 SpEL 中使用自定义 Bean

以编程方式授权方法的第一种方法是两步过程。Spring中文文档

首先,声明一个 Bean,该 Bean 具有采用如下实例的方法:MethodSecurityExpressionOperationsSpring中文文档

@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:Spring中文文档

@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 上调用给定的方法。Spring中文文档

这样做的好处是,您的所有授权逻辑都在一个单独的类中,可以独立地进行单元测试和验证正确性。 它还可以访问完整的 Java 语言。Spring中文文档

使用自定义授权管理器

以编程方式授权方法的第二种方法是创建自定义 AuthorizationManagerSpring中文文档

首先,声明一个授权管理器实例,可能如下所示:Spring中文文档

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

然后,使用与要运行的时间相对应的切入点来发布方法侦听器。 例如,您可以替换 how and work like this:AuthorizationManager@PreAuthorize@PostAuthorizeSpring中文文档

仅@PreAuthorize和@PostAuthorize配置
@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>

您可以使用 中指定的顺序常量将拦截器放置在 Spring Security 方法拦截器之间。AuthorizationInterceptorsOrderSpring中文文档

自定义表达式处理

或者,第三,您可以自定义每个 SpEL 表达式的处理方式。 为此,可以公开自定义 MethodSecurityExpressionHandler,如下所示:Spring中文文档

自定义 MethodSecurityExpressionHandler
@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>

我们公开使用一种方法来确保 Spring 在初始化 Spring Security 的方法安全类之前发布它MethodSecurityExpressionHandlerstatic@ConfigurationSpring中文文档

您还可以将 DefaultMessageSecurityExpressionHandler 子类化,以在默认值之外添加您自己的自定义授权表达式。Spring中文文档

您可以使用 中指定的顺序常量将拦截器放置在 Spring Security 方法拦截器之间。AuthorizationInterceptorsOrderSpring中文文档

我们公开使用一种方法来确保 Spring 在初始化 Spring Security 的方法安全类之前发布它MethodSecurityExpressionHandlerstatic@ConfigurationSpring中文文档

使用 AspectJ 进行授权

使用自定义切点匹配方法

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

例如,您可以使用发布自己的表达式或使用 <protect-pointcut> 将 AOP 表达式与服务图层的授权规则相匹配,如下所示:AdvisorSpring中文文档

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 Byte-weaving 集成

有时可以通过使用 AspectJ 将 Spring Security 建议编织到 Bean 的字节码中来增强性能。Spring中文文档

设置 AspectJ 后,您可以非常简单地在注释或元素中说明您正在使用 AspectJ:@EnableMethodSecurity<method-security>Spring中文文档

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

其结果将是 Spring Security 将发布其顾问作为 AspectJ 建议,以便它们可以相应地融入其中。Spring中文文档

指定顺序

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

即,方法拦截器的阶数是 100,'s 是 200,依此类推。@PreFilter@PreAuthorizeSpring中文文档

需要注意的原因是,还有其他基于 AOP 的注释,其顺序为 。 换句话说,默认情况下,它们位于顾问链的末端。@EnableTransactionManagementInteger.MAX_VALUESpring中文文档

有时,在 Spring Security 之前执行其他建议可能很有价值。 例如,如果有一个用 和 注释的方法,您可能希望事务在运行时仍处于打开状态,以便 an 将导致回滚。@Transactional@PostAuthorize@PostAuthorizeAccessDeniedExceptionSpring中文文档

要在方法授权建议运行之前打开交易,您可以按如下方式设置 的顺序:@EnableTransactionManagement@EnableTransactionManagementSpring中文文档

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

由于最早的方法拦截器 () 设置为 100 阶,因此设置为零意味着事务通知将在所有 Spring Security 建议之前运行。@PreFilterSpring中文文档

使用 SpEL 表达授权

您已经看到了几个使用 SpEL 的示例,所以现在让我们更深入地介绍一下 API。Spring中文文档

Spring Security 将其所有授权字段和方法封装在一组根对象中。 调用最通用的根对象,它构成了 的基础。 Spring Security 在准备计算授权表达式时提供此根对象。SecurityExpressionRootMethodSecurityExpressionRootMethodSecurityEvaluationContextSpring中文文档

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

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

  • permitAll- 该方法不需要授权即可调用;请注意,在这种情况下,永远不会从会话中检索身份验证Spring中文文档

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

  • hasAuthority- 该方法要求具有与给定值匹配的 GrantedAuthorityAuthenticationSpring中文文档

  • hasRole- 该前缀的快捷方式或任何配置为默认前缀的快捷方式hasAuthorityROLE_Spring中文文档

  • hasAnyAuthority- 该方法要求具有与任何给定值匹配的AuthenticationGrantedAuthoritySpring中文文档

  • hasAnyRole- 该前缀的快捷方式或任何配置为默认前缀的快捷方式hasAnyAuthorityROLE_Spring中文文档

  • hasPermission- 挂接到实例中,用于执行对象级授权PermissionEvaluatorSpring中文文档

以下是对最常见字段的简要介绍:Spring中文文档

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

授权请求
@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 此方法只能由被授予权限的 s 调用AuthenticationROLE_ADMIN
3 此方法只能由授予 和 权限的 s 调用AuthenticationdbROLE_ADMIN
4 此方法只能由声明等于“my-audience”的 s 调用Princpalaud
5 只有当 Bean 的方法返回时,才能调用此方法authzchecktrue

使用方法参数

此外,Spring Security 提供了一种用于发现方法参数的机制,因此也可以在 SpEL 表达式中访问它们。Spring中文文档

为了获得完整的参考,Spring Security 用于发现参数名称。 默认情况下,对方法尝试以下选项。DefaultSecurityParameterNameDiscovererSpring中文文档

  1. 如果 Spring Security 的注释存在于该方法的单个参数上,则使用该值。 以下示例使用批注:@P@PSpring中文文档

    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?)

    此表达式的目的是要求当前具有专门针对此实例的权限。AuthenticationwriteContactSpring中文文档

    在后台,这是通过使用 实现的,您可以自定义它以支持任何指定注释的 value 属性。AnnotationParameterNameDiscovererSpring中文文档

    • 如果 Spring Data 的注释存在于该方法的至少一个参数上,则使用该值。 以下示例使用批注:@Param@ParamSpring中文文档

      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?

      此表达式的目的是要求 等于 才能授权调用。nameAuthentication#getNameSpring中文文档

      在后台,这是通过使用 实现的,您可以自定义它以支持任何指定注释的 value 属性。AnnotationParameterNameDiscovererSpring中文文档

    • 如果使用参数编译代码,则使用标准 JDK 反射 API 来发现参数名称。 这适用于类和接口。-parametersSpring中文文档

    • 最后,如果使用调试符号编译代码,则使用调试符号来发现参数名称。 这不适用于接口,因为它们没有有关参数名称的调试信息。 对于接口,必须使用注释或方法。-parametersSpring中文文档

1 任何人都不得出于任何原因调用此方法
2 此方法只能由被授予权限的 s 调用AuthenticationROLE_ADMIN
3 此方法只能由授予 和 权限的 s 调用AuthenticationdbROLE_ADMIN
4 此方法只能由声明等于“my-audience”的 s 调用Princpalaud
5 只有当 Bean 的方法返回时,才能调用此方法authzchecktrue

迁移自@EnableGlobalMethodSecurity

如果使用 ,则应迁移到 。@EnableGlobalMethodSecurity@EnableMethodSecuritySpring中文文档

全局方法安全性替换为方法安全性

@EnableGlobalMethodSecurity<global-method-security> 分别被弃用,取而代之的是 @EnableMethodSecurity<method-security>。 默认情况下,新的注解和 XML 元素会激活 Spring 的前后注解并在内部使用。AuthorizationManagerSpring中文文档

这意味着以下两个列表在功能上是等效的:Spring中文文档

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

对于不使用前后批注的应用程序,请确保将其关闭以避免激活不需要的行为。Spring中文文档

例如,像这样的列表:Spring中文文档

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

应更改为:Spring中文文档

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

使用自定义而不是子类化@BeanDefaultMethodSecurityExpressionHandler

作为性能优化,引入了一种新方法,该方法采用 a 而不是 .MethodSecurityExpressionHandlerSupplier<Authentication>AuthenticationSpring中文文档

这允许 Spring Security 延迟查找 ,并在使用而不是 时自动利用。Authentication@EnableMethodSecurity@EnableGlobalMethodSecuritySpring中文文档

但是,假设您的代码扩展并重写以返回自定义实例。 这将不再起作用,因为设置呼叫的安排。DefaultMethodSecurityExpressionHandlercreateSecurityExpressionRoot(Authentication, MethodInvocation)SecurityExpressionRoot@EnableMethodSecuritycreateEvaluationContext(Supplier<Authentication>, MethodInvocation)Spring中文文档

令人高兴的是,这种级别的定制通常是不必要的。 相反,您可以使用所需的授权方法创建自定义 Bean。Spring中文文档

例如,假设您想要 的自定义计算。 您可以创建如下自定义项:@PostAuthorize("hasAuthority('ADMIN')")@BeanSpring中文文档

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

然后在注释中引用它,如下所示:Spring中文文档

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

我仍然更喜欢子类DefaultMethodSecurityExpressionHandler

如果必须继续子类化,您仍然可以这样做。 相反,重写该方法,如下所示:DefaultMethodSecurityExpressionHandlercreateEvaluationContext(Supplier<Authentication>, MethodInvocation)Spring中文文档

@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 与应用程序的其他方面(如数据层跟踪和指标)集成的信息。Spring中文文档