Method Security

除了 modeling authorization at the request level 之外,Spring Security 还支持在方法级别上进行建模。

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

  • Java

  • Kotlin

  • Xml

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

然后,您可以立即使用 <<`@PreAuthorize`,use-preauthorize>>、<<`@PostAuthorize`,use-postauthorize>>、<<`@PreFilter`,use-prefilter>> 和 <<`@PostFilter`,use-postfilter>> 为任何 Spring 管理的类或方法添加注释,以授权方法调用,包括输入参数和返回值。

Spring Boot Starter Security 默认情况下不会启动方法级授权。

方法安全还支持许多其他用例,包括 AspectJ supportcustom annotations 和多个配置点。考虑了解以下用例:

How Method Security Works

Spring Security 的方法授权支持适用于:

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

  • 在服务层实施安全性

  • 从风格上赞成基于注解的配置胜于基于 HttpSecurity 的配置

而且,由于方法安全是使用 {spring-framework-reference-url}core.html#aop-api[Spring AOP] 构建的,因此你可以访问所有 Spring Security 默认设置所需的所有表现力强的功能以进行替代。

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

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

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

  2. 赞成直接基于 Bean 的配置,而不是需要扩展 GlobalMethodSecurityConfiguration 来自定义 Bean

  3. 使用本机 Spring AOP 来构建,这消除了抽象,并允许您使用 Spring AOP 构建基块进行自定义

  4. 检查是否存在冲突的注解,以确保安全配置明确

  5. Complies with JSR-250

  6. 默认情况下启用 @PreAuthorize.@PostAuthorize.@PreFilter@PostFilter

如果您正在使用 @EnableGlobalMethodSecurity<global-method-security/>,则这些现在已弃用,建议您迁移。

方法授权是方法之前授权和方法之后授权的组合。考虑以下方式添加注释的服务 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 { ... }
}

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

methodsecurity
  1. Spring AOP 为 readCustomer 调用其代理方法。在该代理的其他顾问中,它调用一个与 the @PreAuthorize pointcut 匹配的 {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor/html[AuthorizationManagerBeforeMethodInterceptor]

  2. The interceptor invokes {security-api-url}org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.html[PreAuthorizeAuthorizationManager#check]

  3. 授权管理器使用 MethodSecurityExpressionHandler 来解析注解的 SpEL expression 并根据包含 a Supplier&lt;Authentication&gt;MethodInvocationMethodSecurityExpressionRoot 构建对应的 EvaluationContext

  4. 拦截器使用此上下文来评估表达式;具体来说,它从 Supplier 读取 the Authentication 并检查其 authorities 集合中是否包含 permission:read

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

  6. 如果没有,则拦截器发布 AuthorizationDeniedEvent 并抛出 {security-api-url}org/springframework/security/access/AccessDeniedException.html[AccessDeniedException] ,其中 the ExceptionTranslationFilter 捕获该异常并将 403 状态代码返回到响应

  7. 在该方法返回后,Spring AOP 调用与 the @PostAuthorize pointcut 匹配的 {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.html[AuthorizationManagerAfterMethodInterceptor] ,其操作与上面相同,但使用 {security-api-url}org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.html[PostAuthorizeAuthorizationManager]

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

  9. 如果没有,则拦截器发布 AuthorizationDeniedEvent 并抛出 {security-api-url}org/springframework/security/access/AccessDeniedException.html[AccessDeniedException] ,其中 the ExceptionTranslationFilter 捕获该异常并将 403 状态代码返回到响应

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

Multiple Annotations Are Computed In Series

如上所示,如果方法调用涉及多个 Method Security annotations,则其中每个方法将被一次处理。这意味着它们可以被共同视为“与”在一起。换句话说,对于要授权的调用,所有注释检查都需要通过授权。

Repeated Annotations Are Not Supported

也就是说,不支持在同一方法上重复使用相同的注释。例如,您无法对同一方法两次放置 @PreAuthorize

相反,使用 SpEL 的布尔支持或使用其支持委派到单独的 Bean。

Each Annotation Has Its Own Pointcut

每个注释都有自己的切入点实例,用于在整个对象层次结构中查找该注释或其 meta-annotation 对应项,从 the method and its enclosing class 开始。

你可以在 {security-api-url}org/springframework/security/authorization/method/AuthorizationMethodPointcuts.html[AuthorizationMethodPointcuts] 中看到此问题的具体内容。

Each Annotation Has Its Own Method Interceptor

每个注释都有自己专用的方法拦截器。这样做的原因是让事情变得更易于组合。例如,如果需要,您可以禁用 Spring Security 默认设置和 publish only the @PostAuthorize method interceptor

方法拦截器如下:

  • 对于 <<`@PreAuthorize`,use-preauthorize>>,Spring Security 使用 {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[AuthorizationManagerBeforeMethodInterceptor#preAuthorize] ,后者反过来又使用 {security-api-url}org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.html[PreAuthorizeAuthorizationManager]

  • 对于 <<`@PostAuthorize`,use-postauthorize>>,Spring Security 使用 {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.html[AuthorizationManagerBeforeMethodInterceptor#postAuthorize] ,后者反过来又使用 {security-api-url}org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.html[PostAuthorizeAuthorizationManager]

  • 对于 <<`@PreFilter`,use-prefilter>>,Spring Security 使用 {security-api-url}org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.html[PreFilterAuthorizationMethodInterceptor]

  • 对于 <<`@PostFilter`,use-postfilter>>,Spring Security 使用 {security-api-url}org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.html[PostFilterAuthorizationMethodInterceptor]

  • 对于 <<`@Secured`,use-secured>>, Spring Security 使用 {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[@1],它反过来使用 {security-api-url}org/springframework/security/authorization/method/SecuredAuthorizationManager.html[@2]

  • 对于 JSR-250 注释,Spring Security 使用 {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[@3],它反过来使用 {security-api-url}org/springframework/security/authorization/method/Jsr250AuthorizationManager.html[@4]

通常,您可以将下列内容视为 Spring Security 在添加 @EnableMethodSecurity 时发布的拦截器的代表:

  • 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();
}

Favor Granting Authorities Over Complicated SpEL Expressions

通常经常会忍不住引入像以下内容这样的复杂 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 RoleHierarchyImpl.fromHierarchy("ROLE_ADMIN > permission:read");
}
companion object {
    @Bean
    fun roleHierarchy(): RoleHierarchy {
        return RoleHierarchyImpl.fromHierarchy("ROLE_ADMIN > permission:read")
    }
}
<bean id="roleHierarchy"
        class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl" factory-method="fromHierarchy">
    <constructor-arg value="ROLE_ADMIN > permission:read"/>
</bean>

然后 set that in a MethodSecurityExpressionHandler instance。这样,您可以拥有更简单的 <<`@PreAuthorize`,use-preauthorize>> 表达式,如下所示:

  • Java

  • Kotlin

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

或者,在可能的情况下,在登录时将特定应用的授权逻辑改编成权限。

Comparing Request-level vs Method-level Authorization

你应该何时优先考虑方法级授权而不是 request-level authorization?这在一定程度上取决于品味;但是,请考虑通过以下优势列表来帮助你做出决定。

request-level

method-level

authorization type

coarse-grained

fine-grained

configuration location

在一个 config 类中声明

local to method declaration

configuration style

DSL

Annotations

authorization definitions

programmatic

SpEL

主要取舍在于您希望将授权规则置于何处。

值得记住的是,当你使用基于注释的方法安全性时,未注释的方法是不安全的。为保护此类方法,在你的 HttpSecurity 实例中声明 a catch-all authorization rule

Authorizing with Annotations

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

Authorizing Method Invocation with @PreAuthorize

Method Security is active 时,你可以使用 {security-api-url}org/springframework/security/access/prepost/PreAuthorize.html[@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') 通过时,才能调用方法。

然后,你可以 test the class 以确认其正在强制执行授权规则,如下所示:

  • 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 还可以是 meta-annotation,在 at the class or interface level 中定义,并使用 SpEL Authorization Expressions

虽然 @PreAuthorize 对于声明所需的权限很有帮助,但也可以用于评估更复杂的 expressions that involve the method parameters

Authorization Method Results with @PostAuthorize

方法安全处于活动状态时,你可以使用 {security-api-url}org/springframework/security/access/prepost/PostAuthorize.html[@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 对象。

然后,你可以 test the class 以确认其正在强制执行授权规则:

  • 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 还可以是 meta-annotation,在 at the class or interface level 中定义,并使用 SpEL Authorization Expressions

@PostAuthorize 在防御 Insecure Direct Object Reference 时特别有用。事实上,它可以定义为 meta-annotation,如下所示:

  • 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 状态代码。

Filtering Method Parameters with @PreFilter

不支持特定于 Kotlin 的数据类型,因此只显示 Java 代码段

方法安全处于活动状态时,你可以使用 {security-api-url}org/springframework/security/access/prepost/PreFilter.html[@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;
	}
}

这是为了筛选来自 accounts 的所有值,其中表达式 filterObject.owner == authentication.name 失败。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 还可以是 meta-annotation,在 at the class or interface level 中定义,并使用 SpEL Authorization Expressions

@PreFilter 支持数组、集合、映射和流(只要流仍然处于打开状态)。

例如,上述 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 属性与已登录用户 nameAccount 实例相匹配。

Filtering Method Results with @PostFilter

不支持特定于 Kotlin 的数据类型,因此只显示 Java 代码段

方法安全处于活动状态时,你可以使用 {security-api-url}org/springframework/security/access/prepost/PostFilter.html[@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 还可以是 meta-annotation,在 at the class or interface level 中定义,并使用 SpEL Authorization Expressions

@PostFilter 支持数组、集合、映射和流(只要流仍然处于打开状态)。

例如,上述 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 属性与已登录用户 nameAccount 实例相匹配。

内存中过滤显然会耗费资源,所以要仔细考虑是否最好转而进行 filter the data in the data layer

Authorizing Method Invocation with @Secured

{security-api-url}org/springframework/security/access/annotation/Secured.html[@Secured] 是用于授权调用的传统选项。<<`@PreAuthorize`,use-preauthorize>> 取代了它,并建议改为使用它。

要使用 @Secured 注释,你应该首先更改方法安全性声明以像这样启用它:

  • Java

  • Kotlin

  • Xml

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

这将导致 Spring Security 发布 the corresponding method interceptor 以授权带 @Secured 注释的方法、类和接口。

Authorizing Method Invocation with JSR-250 Annotations

如果你想使用 JSR-250 注解,Spring Security 也支持这样做。<<`@PreAuthorize`,use-preauthorize>> 具有更强的表现力,因此建议使用它。

要使用 JSR-250 注释,你应该首先更改方法安全性声明以像这样启用它们:

  • Java

  • Kotlin

  • Xml

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

这将导致 Spring Security 发布 the corresponding method interceptor 以授权带 @RolesAllowed@PermitAll@DenyAll 注释的方法、类和接口。

Declaring Annotations at the Class or Interface Level

还支持在类和接口级别使用方法安全性注释。

如果它像这样处于类级别:

  • 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 无法得知您想要使用哪一个。

在这样的情况下,您可以通过将注解添加到具体方法来解决歧义。

Using Meta Annotations

方法安全性支持元注解。这意味着您可以使用任何注解,并根据您的特定应用程序用例来提高易读性。

例如,您可以将 @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
	}
}

这会得到更具可读性的方法定义。

Templating Meta-Annotation Expressions

您还可以选择使用元注解模板,这允许获得更强大的注解定义。

首先,发布以下 bean:

  • Java

  • Kotlin

@Bean
static PrePostTemplateDefaults prePostTemplateDefaults() {
	return new PrePostTemplateDefaults();
}
companion object {
    @Bean
    fun prePostTemplateDefaults(): PrePostTemplateDefaults {
        return PrePostTemplateDefaults()
    }
}

现在,您可以创建更强大的 @HasRole,而不是 @IsAdmin,如下所示:

  • Java

  • Kotlin

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('{value}')")
public @interface HasRole {
	String value();
}
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('{value}')")
annotation class IsAdmin(val value: String)

结果是现在您可以对您的安全方法执行以下操作:

  • Java

  • Kotlin

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

请注意,这也能与方法变量和所有注解类型一起使用,不过您会希望小心地正确处理引号,以便得到正确的 SpEL 表达式。

例如,考虑以下 @HasAnyRole 注解:

  • Java

  • Kotlin

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole({roles})")
public @interface HasAnyRole {
	String[] roles();
}
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole({roles})")
annotation class HasAnyRole(val roles: Array<String>)

在那种情况下,您会注意到您不应该在表达式中使用引号,而应该像使用参数值一样:

  • Java

  • Kotlin

@Component
public class BankService {
	@HasAnyRole(roles = { "'USER'", "'ADMIN'" })
	public Account readAccount(Long id) {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}
@Component
open class BankService {
	@HasAnyRole(roles = arrayOf("'USER'", "'ADMIN'"))
	fun readAccount(val id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}

这样,一旦被替换,该表达式就变成了 @PreAuthorize("hasAnyRole('USER', 'ADMIN')")

Enabling Certain Annotations

您可以关闭 @EnableMethodSecurity’s pre-configuration and replace it with you own. You may choose to do this if you want to [customize the `AuthorizationManager、custom-authorization-managers]Pointcut。或者您可能只是想要仅启用某个特定注解,例如 @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"/>

以上代码段通过首先禁用方法安全性的预配置,然后发布 the @PostAuthorize interceptor 本身,来实现此目的。

Authorizing with <intercept-methods>

虽然首选使用 Spring Security 的 annotation-based support 来进行方法安全性,但您也可以使用 XML 声明 bean 授权规则。

如果您需要在您的 XML 配置中声明它,您可以像这样使用 “@21”:

  • Xml

<bean class="org.mycompany.MyController">
    <intercept-methods>
        <protect method="get*" access="hasAuthority('read')"/>
        <protect method="*" access="hasAuthority('write')"/>
    </intercept-methods>
</bean>

这仅支持按前缀或名称匹配方法。如果您的需求比这更复杂,则 use annotation support

Authorizing Methods Programmatically

正如您已经看到的,使用Method Security SpEL expressions 来指定非平凡的授权规则有几种方法。

您可以使用多种方法将您的逻辑改为基于 Java 而非基于 SpEL。这样可供您使用整个 Java 语言来提高可测试性和流程控制。

Using a Custom Bean in SpEL

对方法进行编程授权的第一种方法是两步流程。

首先,声明一个 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 语言。

Using a Custom Authorization Manager

通过编程授权方法的第二种方式是创建自定义 “@23”。

首先,声明一个授权管理器实例,例如该实例:

  • Java

  • Kotlin

@Component
public class MyAuthorizationManager implements AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
        // ... authorization logic
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocationResult invocation) {
        // ... authorization logic
    }
}
@Component
class MyAuthorizationManager : AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
    override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocation): AuthorizationDecision {
        // ... authorization logic
    }

    override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocationResult): AuthorizationDecision {
        // ... authorization logic
    }
}

然后,发布方法拦截器,其中一点切对应于您要运行 AuthorizationManager 的时间。例如,您可以替换 @PreAuthorize@PostAuthorize 的工作方式,如下所示:

Only @PreAuthorize and @PostAuthorize Configuration
  • Java

  • Kotlin

  • Xml

@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
    @Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	Advisor preAuthorize(MyAuthorizationManager manager) {
		return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	Advisor postAuthorize(MyAuthorizationManager manager) {
		return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager);
	}
}
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
   	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	fun preAuthorize(val manager: MyAuthorizationManager) : Advisor {
		return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager)
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	fun postAuthorize(val manager: MyAuthorizationManager) : Advisor {
		return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager)
	}
}
<sec:method-security pre-post-enabled="false"/>

<aop:config/>

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

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

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

Customizing Expression Handling

或者第三,您可以自定义处理每个 SpEL 表达式的方式。为此,您可以公开一个自定义的 {security-api-url}org.springframework.security.access.expression.method.MethodSecurityExpressionHandler.html[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 类之前发布它

您还可以 subclass DefaultMessageSecurityExpressionHandler 来添加您自己的自定义授权表达式,而不仅仅是默认表达式。

Authorizing with AspectJ

Matching Methods with Custom Pointcuts

由于构建于 Spring AOP 之上,您可以声明与注释无关的模式,类似于 request-level authorization.这样做具有集中化方法级授权规则的潜在优势。

例如,您可以发布您自己的 “@24” 或使用 “@26” 将 AOP 表达式与服务层的授权规则匹配,如下所示:

  • Java

  • Kotlin

  • Xml

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

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor protectServicePointcut() {
    AspectJExpressionPointcut pattern = new AspectJExpressionPointcut()
    pattern.setExpression("execution(* com.mycompany.*Service.*(..))")
    return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER"))
}
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole

companion object {
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    fun protectServicePointcut(): Advisor {
        val pattern = AspectJExpressionPointcut()
        pattern.setExpression("execution(* com.mycompany.*Service.*(..))")
        return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER"))
    }
}
<sec:method-security>
    <protect-pointcut expression="execution(* com.mycompany.*Service.*(..))" access="hasRole('USER')"/>
</sec:method-security>

Integrate with AspectJ Byte-weaving

有时,可以通过使用 AspectJ 将 Spring Security 建议编织到 bean 的字节码中来提高性能。

设置 AspectJ 之后,您只需在 @EnableMethodSecurity 注释或 <method-security> 元素中简单地说明您正在使用 AspectJ:

  • Java

  • Kotlin

  • Xml

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

结果是 Spring Security 会将其顾问发布为 AspectJ 建议,以便可以相应地将其编织进去。

Specifying Order

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

例如,@PreFilter 方法拦截器的顺序为 100,@PreAuthorize 的顺序为 200,依此类推。

需要注意这一点的原因在于,还有其他基于 AOP 的注解(如 @EnableTransactionManagement),其顺序为 Integer.MAX_VALUE。换句话说,它们默认位于顾问链的末尾。

有时,在 Spring Security 之前执行其他建议会很有价值。例如,如果你有一个使用 @Transactional@PostAuthorize 进行注解的方法,你可能希望在 @PostAuthorize 运行时事务仍然处于打开状态,以便 AccessDeniedException 会导致回滚。

要让 @EnableTransactionManagement 在方法授权建议运行之前打开事务,可以像这样设置 @EnableTransactionManagement 的顺序:

  • Java

  • Kotlin

  • Xml

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

由于最早的方法拦截器(@PreFilter)的顺序设置为 100,因此将事务建议的顺序设置为 0 意味着它将在所有 Spring Security 建议之前运行。

Expressing Authorization with SpEL

你已经看到了几个使用 SpEL 的示例,现在让我们稍微深入地介绍一下 API。

Spring Security 将其所有授权字段和方法封装到一组根对象中。最通用的根对象称为 SecurityExpressionRoot,它是 MethodSecurityExpressionRoot 的基础。在准备评估授权表达式时,Spring Security 会向 MethodSecurityEvaluationContext 提供此根对象。

Using Authorization Expression Fields and Methods

此提供的第一个内容是为 SpEL 表达式提供了一组增强型授权字段和方法。以下是常用方法的快速概览:

  • @5 - 该方法不需要任何授权即可调用;请注意,在这种情况下,@7 永远不会从会话中检索出来

  • @8 - 在任何情况下都不允许该方法;请注意,在这种情况下,@9 永远不会从会话中检索出来

  • @10 - 该方法要求 @11 具有与给定值匹配的 @13

  • @14 - @15 的快捷方式,它添加前缀 @16 或配置为默认前缀的任何内容

  • @17 - 该方法要求 @18 具有与给定值匹配的任何 @19

  • @20 - @21 的快捷方式,它添加前缀 @22 或配置为默认前缀的任何内容

  • @23 - 用于执行对象级别授权,您可以挂接到 @24 实例

以下是常用字段的简要介绍:

  • @25 - 与此方法调用相关联的 @26 实例

  • @27 - 与此方法调用相关联的 @28

现在已经学习了模式、规则以及它们如何组合在一起,你应该能够理解此更复杂的示例中发生的事情:

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 此方法只能由 @29 权限调用
3 此方法只能由 @30 和 @31 权限调用
4 此方法只能由等于 "my-audience" 的 @32 声明调用
5 仅当 Bean @33 方法返回 @34 时,才能调用此方法

Using Method Parameters

此外,Spring Security 提供了发现方法参数的机制,以便也可以在 SpEL 表达式中访问它们。

为了获得一个完整的引用,Spring Security 使用 DefaultSecurityParameterNameDiscoverer 来发现参数名称。默认情况下,一个方法会尝试以下选项。

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

Java
import org.springframework.security.access.method.P;

...

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

...

@PreAuthorize("hasPermission(#c, 'write')")
fun doSomething(@P("c") contact: Contact?)

此表达式的目的是要求当前 Authentication 针对此 Contact 实例专门具有 write 权限。 在后台,这是通过使用 AnnotationParameterNameDiscoverer 来实现的,你可以对其进行自定义以支持任何指定注解的值属性。

  • 如果 @39 @37 注释出现在方法的至少一个参数上,则使用该值。以下示例使用了 @38 注释:

Java
import org.springframework.data.repository.query.Param;

...

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

...

@PreAuthorize("#n == authentication.name")
fun findContactByName(@Param("n") name: String?): Contact?

此表达式的目的是要求 name 等于 Authentication#getName,才能授权调用。 在后台,这是通过使用 AnnotationParameterNameDiscoverer 来实现的,你可以对其进行自定义以支持任何指定注解的值属性。 * 如果您使用 @40 参数编译代码,则使用标准 JDK 反射 API 发现参数名称。这同时适用于类和接口。 * 最后,如果您使用调试符号编译代码,则使用调试符号发现参数名称。这不适用于接口,因为它们没有参数名称的调试信息。对于接口,必须使用注释或 @41 方法。

Authorizing Arbitrary Objects

Spring Security 还支持包装对其方法安全注解进行注解的任何对象。

为实现此目的,你可以自动装配提供的 AuthorizationProxyFactory 实例,该实例取决于你已配置哪些方法安全拦截器。如果你使用的是 @EnableMethodSecurity,则这意味着它将默认具有 @PreAuthorize@PostAuthorize@PreFilter@PostFilter 的拦截器。

例如,考虑以下 User 类:

  • Java

  • Kotlin

public class User {
	private String name;
	private String email;

	public User(String name, String email) {
		this.name = name;
		this.email = email;
	}

	public String getName() {
		return this.name;
	}

    @PreAuthorize("hasAuthority('user:read')")
    public String getEmail() {
		return this.email;
    }
}
class User (val name:String, @get:PreAuthorize("hasAuthority('user:read')") val email:String)

你可以通过以下方式代理用户的实例:

  • Java

  • Kotlin

@Autowired
AuthorizationProxyFactory proxyFactory;

@Test
void getEmailWhenProxiedThenAuthorizes() {
    User user = new User("name", "email");
    assertThat(user.getEmail()).isNotNull();
    User securedUser = proxyFactory.proxy(user);
    assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail);
}
@Autowired
var proxyFactory:AuthorizationProxyFactory? = null

@Test
fun getEmailWhenProxiedThenAuthorizes() {
    val user: User = User("name", "email")
    assertThat(user.getEmail()).isNotNull()
    val securedUser: User = proxyFactory.proxy(user)
    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy(securedUser::getEmail)
}

Manual Construction

如果你需要与 Spring Security 默认值不同的内容,你还可以定义自己的实例。

例如,如果你定义的 AuthorizationProxyFactory 实例如:

  • Java

  • Kotlin

import static org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize;

// ...

AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize());
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize

// ...

val proxyFactory: AuthorizationProxyFactory = AuthorizationProxyFactory(preAuthorize())

然后你可以封装 User 的任何实例如下:

  • Java

  • Kotlin

@Test
void getEmailWhenProxiedThenAuthorizes() {
	AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize());
    User user = new User("name", "email");
    assertThat(user.getEmail()).isNotNull();
    User securedUser = proxyFactory.proxy(user);
    assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail);
}
@Test
fun getEmailWhenProxiedThenAuthorizes() {
    val proxyFactory: AuthorizationProxyFactory = AuthorizationProxyFactory(preAuthorize())
    val user: User = User("name", "email")
    assertThat(user.getEmail()).isNotNull()
    val securedUser: User = proxyFactory.proxy(user)
    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy(securedUser::getEmail)
}

该特性当前不支持 Spring AOT

Proxying Collections

AuthorizationProxyFactory 通过代理元素类型来支持 Java 集合、流、数组、可选值和迭代器,通过代理值类型来支持映射。

这意味着在代理对象 List 数组时,以下操作也可以使用:

  • Java

@Test
void getEmailWhenProxiedThenAuthorizes() {
	AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize());
    List<User> users = List.of(ada, albert, marie);
    List<User> securedUsers = proxyFactory.proxy(users);
	securedUsers.forEach((securedUser) ->
        assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail));
}

Proxying Classes

在有限的情况下,代理 Class 本身可能有价值,AuthorizationProxyFactory 也支持这一点。这大约等同于 Spring Framework 中用来创建代理的支持中的 ProxyFactory#getProxyClass

预先构造代理类的某些情况下很有用,比如使用 Spring AOT 的时候。

Support for All Method Security Annotations

AuthorizationProxyFactory 支持应用程序中启用的任何方法安全性标注。它是根据作为 bean 发布的任何 AuthorizationAdvisor 类。

由于 @EnableMethodSecurity 默认情况下发布 @PreAuthorize@PostAuthorize@PreFilter@PostFilter 顾问,你通常不需要执行任何操作来激活该功能。

使用 returnObjectfilterObject 的 SpEL 表达式位于代理之后,因此可完全访问该对象。

Custom Advice

如果你有同样希望应用的安全建议,你可以发布自己的 AuthorizationAdvisor 如下:

  • Java

  • Kotlin

@EnableMethodSecurity
class SecurityConfig {
    @Bean
    static AuthorizationAdvisor myAuthorizationAdvisor() {
        return new AuthorizationAdvisor();
    }
}
@EnableMethodSecurity
internal class SecurityConfig {
    @Bean
    fun myAuthorizationAdvisor(): AuthorizationAdvisor {
        return AuthorizationAdvisor()
    }
]

在 Spring 安全性添加代理对象时,Spring 安全性将该顾问添加到 AuthorizationProxyFactory 添加的顾问中。

Working with Jackson

该特性的一大强大用途是从控制器返回安全值,如下所示:

  • Java

  • Kotlin

@RestController
public class UserController {
	@Autowired
    AuthorizationProxyFactory proxyFactory;

	@GetMapping
    User currentUser(@AuthenticationPrincipal User user) {
        return this.proxyFactory.proxy(user);
    }
}
@RestController
class UserController  {
    @Autowired
    var proxyFactory: AuthorizationProxyFactory? = null

    @GetMapping
    fun currentUser(@AuthenticationPrincipal user:User?): User {
        return proxyFactory.proxy(user)
    }
}

但是如果你使用 Jackson,这可能会导致类似以下的序列化错误:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException:导致循环的直接自引用

这是因为 Jackson 如何处理 CGLIB 代理。为了解决这个问题,在 User 类的开头添加以下标注:

  • Java

  • Kotlin

@JsonSerialize(as = User.class)
public class User {

}
@JsonSerialize(`as` = User::class)
class User

最后,你需要发布 custom interceptor 来负责捕获每个字段抛出的 AccessDeniedException,可以如下所示:

  • Java

  • Kotlin

@Component
public class AccessDeniedExceptionInterceptor implements AuthorizationAdvisor {
    private final AuthorizationAdvisor advisor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize();

	@Override
	public Object invoke(MethodInvocation invocation) throws Throwable {
		try {
			return invocation.proceed();
		} catch (AccessDeniedException ex) {
			return null;
		}
	}

	@Override
	public Pointcut getPointcut() {
		return this.advisor.getPointcut();
	}

	@Override
	public Advice getAdvice() {
		return this;
	}

	@Override
	public int getOrder() {
		return this.advisor.getOrder() - 1;
	}
}
@Component
class AccessDeniedExceptionInterceptor: AuthorizationAdvisor {
    var advisor: AuthorizationAdvisor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize()

    @Throws(Throwable::class)
    fun invoke(invocation: MethodInvocation): Any? {
        return try  {
            invocation.proceed()
        } catch (ex:AccessDeniedException) {
            null
        }
    }

     val pointcut: Pointcut
     get() = advisor.getPointcut()

     val advice: Advice
     get() = this

     val order: Int
     get() = advisor.getOrder() - 1
}

然后,你会看到基于用户授权级别的不同 JSON 序列化。如果没有 user:read 权限,则会看到:

{
    "name" : "name",
    "email" : null
}

而如果具有该权限,则会看到:

{
    "name" : "name",
    "email" : "email"
}

如果你也不想向未授权用户透露 JSON 密钥,还可以添加 Spring Boot 属性 spring.jackson.default-property-inclusion=non_null 来排除空值。

Migrating from @EnableGlobalMethodSecurity

如果您正在使用 @EnableGlobalMethodSecurity,则应迁移到 @EnableMethodSecurity

Replace global method security with method security

{security-api-url}org/springframework/security/config/annotation/method/configuration/EnableGlobalMethodSecurity.html[@27] 和 “@32” 已弃用,支持 {security-api-url}org/springframework/security/config/annotation/method/configuration/EnableMethodSecurity.html[@29] 和 “@33”。新注解和 XML 元素默认激活 Spring 的 “@34”,并在内部使用 “@31”。

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

  • Java

  • Kotlin

  • Xml

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

及:

  • Java

  • Kotlin

  • Xml

@EnableMethodSecurity
@EnableMethodSecurity
<method-security/>

对于不使用前后注释的应用程序,请务必关闭它以避免激活不需要的行为。

例如,像这样的列表:

  • 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"/>

Use a Custom @Bean instead of subclassing DefaultMethodSecurityExpressionHandler

作为一种性能优化,新方法被引入到 MethodSecurityExpressionHandler 中,它采用 Supplier<Authentication> 而不是 Authentication

这允许 Spring Security 推迟对 Authentication 的查找,并在使用 @EnableMethodSecurity 代替 @EnableGlobalMethodSecurity 时自动利用它。

但是,假设您的代码扩展了 DefaultMethodSecurityExpressionHandler 并覆盖了 createSecurityExpressionRoot(Authentication, MethodInvocation) 以返回自定义 SecurityExpressionRoot 实例。这将不再起作用,因为 @EnableMethodSecurity 设置的排列改为调用 createEvaluationContext(Supplier<Authentication>, MethodInvocation)

幸运的是,这种级别的自定义通常是不必要的。相反,您可以使用所需的授权方法创建一个自定义 bean。

例如,假设您想要对 @PostAuthorize("hasAuthority('ADMIN')") 进行自定义评估。您可以像这样创建一个自定义 @Bean

  • Java

  • Kotlin

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

然后在注释中这样引用它:

  • Java

  • Kotlin

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

I’d still prefer to subclass 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
    }
}

Further Reading

现在您已经保护了应用程序的请求,如果您尚未执行此操作,请 secure its requests.您还可以进一步阅读 testing your application或 Spring Security 与应用程序其他方面的集成,如 the data layertracing and metrics