此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Security 6.4.3spring-doc.cadn.net.cn

方法安全性

除了在请求级别对授权进行建模外, Spring Security 还支持在方法级别进行建模。spring-doc.cadn.net.cn

您可以在应用程序中通过注释任何@Configurationclass 替换为@EnableMethodSecurity或添加<method-security>添加到任何 XML 配置文件中,如下所示:spring-doc.cadn.net.cn

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

然后,您可以立即使用@PreAuthorize,@PostAuthorize,@PreFilter@PostFilter授权方法调用,包括输入参数和返回值。spring-doc.cadn.net.cn

Method Security 还支持许多其他用例,包括 AspectJ 支持自定义注释和几个配置点。 考虑了解以下使用案例:spring-doc.cadn.net.cn

方法安全性的工作原理

Spring Security 的方法授权支持对于以下方面非常方便:spring-doc.cadn.net.cn

由于 Method Security 是使用 Spring AOP 构建的,因此您可以访问其所有表达能力,以根据需要覆盖 Spring Security 的默认值。spring-doc.cadn.net.cn

如前所述,您首先要添加@EnableMethodSecurity更改为@Configurationclass 或<sec:method-security/>在 Spring XML 配置文件中。spring-doc.cadn.net.cn

此 annotation 和 XML 元素取代@EnableGlobalMethodSecurity<sec:global-method-security/>分别。 它们提供了以下改进:spring-doc.cadn.net.cn

  1. 使用简化的AuthorizationManagerAPI 而不是元数据源、配置属性、决策管理器和投票者。 这简化了重用和定制。spring-doc.cadn.net.cn

  2. 支持直接基于 Bean 的配置,而不是要求扩展GlobalMethodSecurityConfiguration自定义 beanspring-doc.cadn.net.cn

  3. 使用原生 Spring AOP 构建,去除抽象并允许您使用 Spring AOP 构建块进行自定义spring-doc.cadn.net.cn

  4. 检查冲突的注释以确保安全配置明确spring-doc.cadn.net.cn

  5. 符合 JSR-250spring-doc.cadn.net.cn

  6. 使@PreAuthorize,@PostAuthorize,@PreFilter@PostFilter默认情况下spring-doc.cadn.net.cn

如果您正在使用@EnableGlobalMethodSecurity<global-method-security/>,这些选项现已弃用,建议您迁移。spring-doc.cadn.net.cn

方法授权是方法授权之前和方法之后授权的组合。 考虑以下列方式注释的服务 Bean:spring-doc.cadn.net.cn

@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-doc.cadn.net.cn

方法安全性
  1. Spring AOP 调用其代理方法readCustomer.在代理的其他顾问程序中,它会调用AuthorizationManagerBeforeMethodInterceptor匹配@PreAuthorize切入点spring-doc.cadn.net.cn

  2. 拦截器调用PreAuthorizeAuthorizationManager#checkspring-doc.cadn.net.cn

  3. 授权管理器使用MethodSecurityExpressionHandler解析注释的 SPEL 表达式并构造相应的EvaluationContextMethodSecurityExpressionRoot一个Supplier<Authentication>MethodInvocation.spring-doc.cadn.net.cn

  4. 侦听器使用此上下文来评估表达式;具体来说,它写道AuthenticationSupplier并检查它是否具有permission:read在其权威集合中spring-doc.cadn.net.cn

  5. 如果评估通过,则 Spring AOP 将继续调用该方法。spring-doc.cadn.net.cn

  6. 否则,拦截器会发布一个AuthorizationDeniedEvent并抛出一个AccessDeniedExceptionExceptionTranslationFilter捕获并向响应返回 403 状态代码spring-doc.cadn.net.cn

  7. 在方法返回后, Spring AOP 会调用AuthorizationManagerAfterMethodInterceptor匹配@PostAuthorize切入点,作与上述相同,但使用PostAuthorizeAuthorizationManagerspring-doc.cadn.net.cn

  8. 如果评估通过(在本例中,返回值属于已登录用户),则处理将继续正常进行spring-doc.cadn.net.cn

  9. 否则,拦截器会发布一个AuthorizationDeniedEvent并抛出一个AccessDeniedExceptionExceptionTranslationFilter捕获并向响应返回 403 状态代码spring-doc.cadn.net.cn

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

多个注释是按顺序计算的

如上所述,如果方法调用涉及多个 Method Security 注释,则一次处理一个 Method Security 注释。 这意味着它们可以统称为“和”在一起。 换句话说,要使调用获得授权,所有 Comments 检查都需要通过授权。spring-doc.cadn.net.cn

不支持重复的注释

也就是说,不支持在同一方法上重复相同的注释。 例如,您不能将@PreAuthorize两次。spring-doc.cadn.net.cn

相反,请使用 SPEL 的布尔支持或其对委托给单独 bean 的支持。spring-doc.cadn.net.cn

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

每个 Annotation 都有自己的 pointcut 实例,该实例从方法及其封闭类开始,在整个对象层次结构中查找该 Annotation 或其元 Annotation 对应项。spring-doc.cadn.net.cn

每个 Annotation 都有自己的 Method Interceptor

每个 Comments 都有其自己的专用方法拦截器。 这样做的原因是为了让事情更具可组合性。 例如,如果需要,您可以禁用 Spring Security 默认值和仅发布@PostAuthorizemethod intercept器.spring-doc.cadn.net.cn

方法拦截器如下:spring-doc.cadn.net.cn

一般来说,你可以认为下面的清单代表了 Spring Security 在你添加@EnableMethodSecurity:spring-doc.cadn.net.cn

@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-doc.cadn.net.cn

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

但是,您可以改为授予permission:read对于那些ROLE_ADMIN. 一种方法是使用RoleHierarchy这样:spring-doc.cadn.net.cn

@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-doc.cadn.net.cn

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

或者,在可能的情况下,在登录时将特定于应用程序的授权逻辑调整为授予的权限。spring-doc.cadn.net.cn

比较请求级和方法级授权

何时应优先使用方法级授权而不是请求级授权? 其中一些归结为品味;但是,请考虑以下每个优势列表来帮助您做出决定。spring-doc.cadn.net.cn

请求级别spring-doc.cadn.net.cn

方法级别spring-doc.cadn.net.cn

授权类型spring-doc.cadn.net.cn

粗粒度spring-doc.cadn.net.cn

细粒度spring-doc.cadn.net.cn

配置位置spring-doc.cadn.net.cn

在 Config 类中声明spring-doc.cadn.net.cn

local to 方法声明spring-doc.cadn.net.cn

配置样式spring-doc.cadn.net.cn

DSL (英语)spring-doc.cadn.net.cn

附注spring-doc.cadn.net.cn

授权定义spring-doc.cadn.net.cn

编程spring-doc.cadn.net.cn

斯佩尔spring-doc.cadn.net.cn

主要的权衡似乎是您希望授权规则所在的位置。spring-doc.cadn.net.cn

请务必记住,当您使用基于注释的方法安全性时,未注释的方法将不安全。 为了防止这种情况,请在您的HttpSecurity实例。

使用注释进行授权

Spring Security 启用方法级授权支持的主要方式是通过可以添加到方法、类和接口的 Comments。spring-doc.cadn.net.cn

授权方法调用@PreAuthorize

Method Security 处于活动状态时,您可以使用@PreAuthorize注解,如下所示:spring-doc.cadn.net.cn

@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-doc.cadn.net.cn

然后,您可以测试该类以确认它正在强制执行授权规则,如下所示:spring-doc.cadn.net.cn

@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对于声明所需的权限非常有帮助,它也可以用于评估涉及方法参数的更复杂的表达式spring-doc.cadn.net.cn

授权方法结果@PostAuthorize

当 Method Security 处于活动状态时,您可以使用@PostAuthorize注解,如下所示:spring-doc.cadn.net.cn

@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对象。spring-doc.cadn.net.cn

然后,您可以测试该类以确认它正在强制执行授权规则:spring-doc.cadn.net.cn

@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在防御不安全的 Direct Object Reference 时特别有用。 事实上,它可以被定义为元注释,如下所示:spring-doc.cadn.net.cn

@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-doc.cadn.net.cn

@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. 否则,Spring Security 将抛出一个AccessDeniedException并返回 403 状态代码。spring-doc.cadn.net.cn

过滤方法参数@PreFilter

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

当 Method Security 处于活动状态时,您可以使用@PreFilter注解,如下所示:spring-doc.cadn.net.cn

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

这是为了从accounts其中表达式filterObject.owner == authentication.name失败。filterObject表示每个accountaccounts,用于测试每个account.spring-doc.cadn.net.cn

然后,您可以通过以下方式测试该类,以确认它正在强制执行授权规则:spring-doc.cadn.net.cn

@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-doc.cadn.net.cn

例如,上面的updateAccountsdeclaration 的功能与其他四个相同:spring-doc.cadn.net.cn

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

结果是上述方法将只有Account实例中,其owner属性与已登录用户的name.spring-doc.cadn.net.cn

过滤方法结果与@PostFilter

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

当 Method Security 处于活动状态时,您可以使用@PostFilter注解,如下所示:spring-doc.cadn.net.cn

@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表示每个accountaccounts,用于测试每个account.spring-doc.cadn.net.cn

然后,您可以像这样测试该类,以确认它正在强制执行授权规则:spring-doc.cadn.net.cn

@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-doc.cadn.net.cn

例如,上面的readAccounts声明的功能与其他三种方法相同:spring-doc.cadn.net.cn

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

结果是上述方法将返回Account实例中,其owner属性与已登录用户的name.spring-doc.cadn.net.cn

内存筛选显然可能很昂贵,因此要考虑是否更适合筛选数据层中的数据

授权方法调用@Secured

@Secured是用于授权调用的旧选项。@PreAuthorize取代它,而是推荐使用。spring-doc.cadn.net.cn

要使用@Secured注解,您应该首先更改 Method Security 声明以启用它,如下所示:spring-doc.cadn.net.cn

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

这将导致 Spring Security 发布相应的方法拦截器,该拦截器授权带有 Comments 的方法、类和接口@Secured.spring-doc.cadn.net.cn

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

如果您想使用 JSR-250 注释,Spring Security 也支持它。@PreAuthorize具有更强的表现力,因此推荐使用。spring-doc.cadn.net.cn

要使用 JSR-250 注释,您应该首先更改 Method Security 声明以启用它们,如下所示:spring-doc.cadn.net.cn

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

这将导致 Spring Security 发布相应的方法拦截器,该拦截器授权带有 Comments 的方法、类和接口@RolesAllowed,@PermitAll@DenyAll.spring-doc.cadn.net.cn

在类或接口级别声明注释

还支持在类和接口级别具有 Method Security 注释。spring-doc.cadn.net.cn

如果它是在类级别,如下所示:spring-doc.cadn.net.cn

@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-doc.cadn.net.cn

或者,如果它在类和方法级别都声明如下:spring-doc.cadn.net.cn

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

然后,声明 Comments 的方法将覆盖类级 Comments。spring-doc.cadn.net.cn

接口也是如此,不同之处在于,如果一个类从两个不同的接口继承 Comments,则启动将失败。 这是因为 Spring Security 无法判断你想使用哪一个。spring-doc.cadn.net.cn

在这种情况下,您可以通过将 Annotation 添加到具体方法来解决歧义。spring-doc.cadn.net.cn

使用 Meta 注释

Method Security 支持元注释。 这意味着您可以采用任何注释,并根据特定于应用程序的用例提高可读性。spring-doc.cadn.net.cn

例如,您可以简化@PreAuthorize("hasRole('ADMIN')")@IsAdmin这样:spring-doc.cadn.net.cn

@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-doc.cadn.net.cn

@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-doc.cadn.net.cn

启用某些注释

您可以关闭@EnableMethodSecurity的预配置,并将其替换为您自己的配置。 如果您愿意,您可以选择这样做自定义AuthorizationManagerPointcut. 或者您可能只想启用特定的注释,例如@PostAuthorize.spring-doc.cadn.net.cn

您可以通过以下方式执行此作:spring-doc.cadn.net.cn

仅 @PostAuthorize 配置
@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拦截 器本身。spring-doc.cadn.net.cn

授权方式<intercept-methods>

虽然使用 Spring Security 的基于 Comments 的支持是方法安全性的首选,但您也可以使用 XML 来声明 Bean 授权规则。spring-doc.cadn.net.cn

如果需要在 XML 配置中声明它,可以使用<intercept-methods>这样:spring-doc.cadn.net.cn

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

以编程方式授权方法

如您所见,有几种方法可以使用方法安全性 SPEL 表达式指定重要的授权规则。spring-doc.cadn.net.cn

有多种方法可以让你的逻辑基于 Java 而不是基于 SPEL。 这为整个 Java 语言提供了 use 访问权限,以提高可测试性和流控制。spring-doc.cadn.net.cn

在 SPEL 中使用自定义 Bean

以编程方式授权方法的第一种方法是一个两步过程。spring-doc.cadn.net.cn

首先,声明一个 bean,该 bean 具有一个方法,该方法采用MethodSecurityExpressionOperations实例,如下所示:spring-doc.cadn.net.cn

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

然后,按以下方式在 Comments 中引用该 bean:spring-doc.cadn.net.cn

@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-doc.cadn.net.cn

这样做的好处是,您的所有授权逻辑都位于一个单独的类中,该类可以独立进行单元测试和正确性验证。 它还可以访问完整的 Java 语言。spring-doc.cadn.net.cn

使用自定义授权管理器

以编程方式授权方法的第二种方法是创建自定义AuthorizationManager.spring-doc.cadn.net.cn

首先,声明一个授权 Management 器实例,可能像这样:spring-doc.cadn.net.cn

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

然后,使用与所需时间相对应的切入点发布方法 interceptorAuthorizationManager运行。 例如,您可以将@PreAuthorize@PostAuthorize像这样工作:spring-doc.cadn.net.cn

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

你可以使用 Order 常量中指定的 order 常量将拦截器放置在 Spring Security 方法拦截器之间AuthorizationInterceptorsOrder.spring-doc.cadn.net.cn

自定义表达式处理

或者,第三种,您可以自定义每个 SPEL 表达式的处理方式。 为此,您可以公开自定义MethodSecurityExpressionHandler这样:spring-doc.cadn.net.cn

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

我们揭露MethodSecurityExpressionHandler使用static方法,以确保 Spring 在初始化 Spring Security 的方法安全性之前发布它@Configurationspring-doc.cadn.net.cn

您还可以亚纲DefaultMessageSecurityExpressionHandler以添加您自己的自定义授权表达式,而不是默认值。spring-doc.cadn.net.cn

使用 AspectJ 进行授权

使用自定义切入点匹配方法

由于 Spring AOP 构建,您可以声明与 Comments 无关的模式,类似于请求级授权。 这具有集中方法级授权规则的潜在优势。spring-doc.cadn.net.cn

例如,您可以使用 publish your ownAdvisor或使用<protect-pointcut>将 AOP 表达式与服务层的授权规则匹配,如下所示:spring-doc.cadn.net.cn

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>

Integrate with AspectJ Byte-weaving

Performance can at times be enhanced by using AspectJ to weave Spring Security advice into the byte code of your beans.spring-doc.cadn.net.cn

After setting up AspectJ, you can quite simply state in the @EnableMethodSecurity annotation or <method-security> element that you are using AspectJ:spring-doc.cadn.net.cn

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

And the result will be that Spring Security will publish its advisors as AspectJ advice so that they can be woven in accordingly.spring-doc.cadn.net.cn

Specifying Order

As already noted, there is a Spring AOP method interceptor for each annotation, and each of these has a location in the Spring AOP advisor chain.spring-doc.cadn.net.cn

Namely, the @PreFilter method interceptor’s order is 100, @PreAuthorize's is 200, and so on.spring-doc.cadn.net.cn

The reason this is important to note is that there are other AOP-based annotations like @EnableTransactionManagement that have an order of Integer.MAX_VALUE. In other words, they are located at the end of the advisor chain by default.spring-doc.cadn.net.cn

At times, it can be valuable to have other advice execute before Spring Security. For example, if you have a method annotated with @Transactional and @PostAuthorize, you might want the transaction to still be open when @PostAuthorize runs so that an AccessDeniedException will cause a rollback.spring-doc.cadn.net.cn

To get @EnableTransactionManagement to open a transaction before method authorization advice runs, you can set @EnableTransactionManagement's order like so:spring-doc.cadn.net.cn

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

Since the earliest method interceptor (@PreFilter) is set to an order of 100, a setting of zero means that the transaction advice will run before all Spring Security advice.spring-doc.cadn.net.cn

Expressing Authorization with SpEL

You’ve already seen several examples using SpEL, so now let’s cover the API a bit more in depth.spring-doc.cadn.net.cn

Spring Security encapsulates all of its authorization fields and methods in a set of root objects. The most generic root object is called SecurityExpressionRoot and it forms the basis for MethodSecurityExpressionRoot. Spring Security supplies this root object to MethodSecurityEvaluationContext when preparing to evaluate an authorization expression.spring-doc.cadn.net.cn

Using Authorization Expression Fields and Methods

The first thing this provides is an enhanced set of authorization fields and methods to your SpEL expressions. What follows is a quick overview of the most common methods:spring-doc.cadn.net.cn

  • permitAll - The method requires no authorization to be invoked; note that in this case, the Authentication is never retrieved from the sessionspring-doc.cadn.net.cn

  • denyAll - The method is not allowed under any circumstances; note that in this case, the Authentication is never retrieved from the sessionspring-doc.cadn.net.cn

  • hasAuthority - The method requires that the Authentication have a GrantedAuthority that matches the given valuespring-doc.cadn.net.cn

  • hasRole - A shortcut for hasAuthority that prefixes ROLE_ or whatever is configured as the default prefixspring-doc.cadn.net.cn

  • hasAnyAuthority - The method requires that the Authentication have a GrantedAuthority that matches any of the given valuesspring-doc.cadn.net.cn

  • hasAnyRole - A shortcut for hasAnyAuthority that prefixes ROLE_ or whatever is configured as the default prefixspring-doc.cadn.net.cn

  • hasPermission - A hook into your PermissionEvaluator instance for doing object-level authorizationspring-doc.cadn.net.cn

And here is a brief look at the most common fields:spring-doc.cadn.net.cn

Having now learned the patterns, rules, and how they can be paired together, you should be able to understand what is going on in this more complex example:spring-doc.cadn.net.cn

Authorize Requests
@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 This method may not be invoked by anyone for any reason
2 This method may only be invoked by Authentications granted the ROLE_ADMIN authority
3 This method may only be invoked by Authentications granted the db and ROLE_ADMIN authorities
4 This method may only be invoked by Princpals with an aud claim equal to "my-audience"
5 This method may only be invoked if the bean authz's check method returns true

Using Method Parameters

Additionally, Spring Security provides a mechanism for discovering method parameters so they can also be accessed in the SpEL expression as well.spring-doc.cadn.net.cn

For a complete reference, Spring Security uses DefaultSecurityParameterNameDiscoverer to discover the parameter names. By default, the following options are tried for a method.spring-doc.cadn.net.cn

  1. If Spring Security’s @P annotation is present on a single argument to the method, the value is used. The following example uses the @P annotation:spring-doc.cadn.net.cn

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

    The intention of this expression is to require that the current Authentication have write permission specifically for this Contact instance.spring-doc.cadn.net.cn

    Behind the scenes, this is implemented by using AnnotationParameterNameDiscoverer, which you can customize to support the value attribute of any specified annotation.spring-doc.cadn.net.cn

    • If Spring Data’s @Param annotation is present on at least one parameter for the method, the value is used. The following example uses the @Param annotation:spring-doc.cadn.net.cn

      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?

      The intention of this expression is to require that name be equal to Authentication#getName for the invocation to be authorized.spring-doc.cadn.net.cn

      Behind the scenes, this is implemented by using AnnotationParameterNameDiscoverer, which you can customize to support the value attribute of any specified annotation.spring-doc.cadn.net.cn

    • If you compile your code with the -parameters argument, the standard JDK reflection API is used to discover the parameter names. This works on both classes and interfaces.spring-doc.cadn.net.cn

    • Finally, if you compile your code with debug symbols, the parameter names are discovered by using the debug symbols. This does not work for interfaces, since they do not have debug information about the parameter names. For interfaces, either annotations or the -parameters approach must be used.spring-doc.cadn.net.cn

Migrating from @EnableGlobalMethodSecurity

If you are using @EnableGlobalMethodSecurity, you should migrate to @EnableMethodSecurity.spring-doc.cadn.net.cn

Replace global method security with method security

@EnableGlobalMethodSecurity and <global-method-security> are deprecated in favor of @EnableMethodSecurity and <method-security>, respectively. The new annotation and XML element activate Spring’s pre-post annotations by default and use AuthorizationManager internally.spring-doc.cadn.net.cn

This means that the following two listings are functionally equivalent:spring-doc.cadn.net.cn

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

For applications not using the pre-post annotations, make sure to turn it off to avoid activating unwanted behavior.spring-doc.cadn.net.cn

For example, a listing like:spring-doc.cadn.net.cn

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

should change to:spring-doc.cadn.net.cn

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

Use a Custom @Bean instead of subclassing DefaultMethodSecurityExpressionHandler

As a performance optimization, a new method was introduced to MethodSecurityExpressionHandler that takes a Supplier<Authentication> instead of an Authentication.spring-doc.cadn.net.cn

This allows Spring Security to defer the lookup of the Authentication, and is taken advantage of automatically when you use @EnableMethodSecurity instead of @EnableGlobalMethodSecurity.spring-doc.cadn.net.cn

However, let’s say that your code extends DefaultMethodSecurityExpressionHandler and overrides createSecurityExpressionRoot(Authentication, MethodInvocation) to return a custom SecurityExpressionRoot instance. This will no longer work because the arrangement that @EnableMethodSecurity sets up calls createEvaluationContext(Supplier<Authentication>, MethodInvocation) instead.spring-doc.cadn.net.cn

Happily, such a level of customization is often unnecessary. Instead, you can create a custom bean with the authorization methods that you need.spring-doc.cadn.net.cn

For example, let’s say you are wanting a custom evaluation of @PostAuthorize("hasAuthority('ADMIN')"). You can create a custom @Bean like this one:spring-doc.cadn.net.cn

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

and then refer to it in the annotation like so:spring-doc.cadn.net.cn

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

I’d still prefer to subclass DefaultMethodSecurityExpressionHandler

If you must continue subclassing DefaultMethodSecurityExpressionHandler, you can still do so. Instead, override the createEvaluationContext(Supplier<Authentication>, MethodInvocation) method like so:spring-doc.cadn.net.cn

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

Further Reading

Now that you have secured your application’s requests, please secure its requests if you haven’t already. You can also read further on testing your application or on integrating Spring Security with other aspects of you application like the data layer or tracing and metrics.spring-doc.cadn.net.cn