方法安全

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

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

  • Java

  • Kotlin

  • Xml

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

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

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

方法安全还支持许多其他用例,包括AspectJ 支持自定义注解 和几个配置点。考虑了解以下用例

方法安全的工作原理

Spring Security 的方法授权支持对于以下情况非常有用:

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

  • 在服务层强制执行安全

  • 在风格上更倾向于基于注解的配置而不是基于 HttpSecurity 的配置

由于方法安全是使用 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. 符合 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 { ... }
}

当方法安全被激活时,对MyCustomerService#readCustomer 的给定调用可能看起来像这样

methodsecurity
  1. Spring AOP 调用其代理方法以执行readCustomer。在代理的其他顾问中,它调用一个AuthorizationManagerBeforeMethodInterceptor,该顾问与@PreAuthorize 切入点匹配

  2. 拦截器调用PreAuthorizeAuthorizationManager#check

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

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

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

  6. 否则,拦截器发布 AuthorizationDeniedEvent 并抛出AccessDeniedException,该异常被ExceptionTranslationFilter 捕获并向响应返回 403 状态代码

  7. 方法返回后,Spring AOP 调用一个AuthorizationManagerAfterMethodInterceptor,该顾问与@PostAuthorize 切入点匹配,其操作方式与上述相同,但使用的是PostAuthorizeAuthorizationManager

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

  9. 如果不是,拦截器会发布一个 AuthorizationDeniedEvent 并抛出一个 AccessDeniedExceptionExceptionTranslationFilter 会捕获它并返回 403 状态码给响应。

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

多个注解按顺序计算

如上所示,如果方法调用涉及多个 方法安全注解,每个注解都会被逐个处理。这意味着它们可以被认为是“与”在一起的。换句话说,为了使调用被授权,所有注解检查都需要通过授权。

不支持重复注解

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

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

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

每个注解都有自己的切入点实例,它会在整个对象层次结构中查找该注解或其 元注解 对应项,从 方法及其封闭类 开始。

你可以在 AuthorizationMethodPointcuts 中看到这方面的具体内容。

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

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

方法拦截器如下所示

一般来说,您可以将以下列表视为 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();
}

优先使用授权而不是复杂的 SpEL 表达式

通常情况下,您可能会倾向于引入以下复杂的 SpEL 表达式

  • Java

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

但是,您可以改为向具有 ROLE_ADMIN 的用户授予 permission:read 权限。一种方法是使用 RoleHierarchy,如下所示

  • Java

  • Kotlin

  • Xml

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

然后 MethodSecurityExpressionHandler 实例中设置它。这样,您就可以使用更简单的 @PreAuthorize 表达式,例如

  • Java

  • Kotlin

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

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

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

您应该何时优先使用方法级授权而不是 请求级授权?部分取决于个人喜好;但是,请考虑以下每个方面的优势列表,以帮助您做出决定。

请求级

方法级

授权类型

粗粒度

细粒度

配置位置

在配置类中声明

方法声明的局部变量

配置风格

DSL

注解

授权定义

编程方式

SpEL

主要权衡似乎在于您希望授权规则驻留的位置。

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

使用注解授权

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

使用 @PreAuthorize 授权方法调用

方法安全处于活动状态 时,您可以使用 @PreAuthorize 注解来注解方法,如下所示

  • Java

  • Kotlin

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

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

然后,您可以 测试该类 以确认它正在执行授权规则,如下所示

  • Java

  • Kotlin

@Autowired
BankService bankService;

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

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

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

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

使用 @PostAuthorize 授权方法结果

当方法安全处于活动状态时,您可以使用 @PostAuthorize 注解来注解方法,如下所示

  • Java

  • Kotlin

@Component
public class BankService {
	@PostAuthorize("returnObject.owner == authentication.name")
	public Account readAccount(Long id) {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}
@Component
open class BankService {
	@PostAuthorize("returnObject.owner == authentication.name")
	fun readAccount(val id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}

这表示只有在提供的表达式 returnObject.owner == authentication.name 通过时,该方法才能返回值。returnObject 代表要返回的 Account 对象。

然后,您可以 测试该类 以确认它正在执行授权规则

  • Java

  • Kotlin

@Autowired
BankService bankService;

@WithMockUser(username="owner")
@Test
void readAccountWhenOwnedThenReturns() {
    Account account = this.bankService.readAccount("12345678");
    // ... assertions
}

@WithMockUser(username="wrong")
@Test
void readAccountWhenNotOwnedThenAccessDenied() {
    assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(
        () -> this.bankService.readAccount("12345678"));
}
@WithMockUser(username="owner")
@Test
fun readAccountWhenOwnedThenReturns() {
    val account: Account = this.bankService.readAccount("12345678")
    // ... assertions
}

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

@PostAuthorize 在防御 不安全的直接对象引用 时特别有用。事实上,它可以像这样定义为 元注解

  • Java

  • Kotlin

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

允许您以以下方式注解服务

  • Java

  • Kotlin

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

结果是,上述方法只会返回其owner属性与登录用户name匹配的Account。如果不匹配,Spring Security 将抛出AccessDeniedException并返回 403 状态码。

使用@PreFilter过滤方法参数

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

当方法安全生效时,您可以使用@PreFilter注解来注释方法,如下所示

  • Java

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

这旨在过滤掉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 也可以是元注解,可以在类或接口级别定义,并使用SpEL 授权表达式

@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属性与登录用户name匹配的Account实例。

使用@PostFilter过滤方法结果

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

当方法安全生效时,您可以使用@PostFilter注解来注释方法,如下所示

  • Java

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

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

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

  • Java

@Autowired
BankService bankService;

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

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

例如,上面的 readAccounts 声明将与以下另外三个声明具有相同的功能

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

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

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

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

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

使用 @Secured 授权方法调用

@Secured 是授权调用的传统选项。 @PreAuthorize 替代了它,建议使用它。

要使用 @Secured 注解,您应该首先更改您的方法安全声明以启用它,如下所示

  • Java

  • Kotlin

  • Xml

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

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

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

如果您想使用 JSR-250 注解,Spring Security 也支持该功能。 @PreAuthorize 具有更强的表达能力,因此建议使用它。

要使用 JSR-250 注解,您应该首先更改您的方法安全声明以启用它们,如下所示

  • Java

  • Kotlin

  • Xml

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

这将导致 Spring Security 发布 相应的 method interceptor,该 interceptor 授权使用 @RolesAllowed@PermitAll@DenyAll 注解的方法、类和接口。

在类或接口级别声明注解

在类和接口级别使用方法安全注解也是支持的。

如果它在类级别,如下所示

  • Java

  • Kotlin

@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
public class MyController {
    @GetMapping("/endpoint")
    public String endpoint() { ... }
}
@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
open class MyController {
    @GetMapping("/endpoint")
    fun endpoint(): String { ... }
}

那么所有方法都将继承类级别的行为。

或者,如果它在类和方法级别都声明为以下内容

  • Java

  • Kotlin

@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
public class MyController {
    @GetMapping("/endpoint")
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public String endpoint() { ... }
}
@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
open class MyController {
    @GetMapping("/endpoint")
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    fun endpoint(): String { ... }
}

那么声明注解的方法将覆盖类级别的注解。

接口也是如此,唯一的例外是,如果一个类从两个不同的接口继承注解,那么启动将失败。这是因为 Spring Security 无法确定您要使用哪个注解。

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

使用元注解

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

例如,您可以将 @PreAuthorize("hasRole('ADMIN')") 简化为 @IsAdmin,如下所示

  • Java

  • Kotlin

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
public @interface IsAdmin {}
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
annotation class IsAdmin

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

  • Java

  • Kotlin

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

这将导致更易读的方法定义。

模板元注解表达式

您也可以选择使用元注解模板,它允许更强大的注解定义。

首先,发布以下 Bean

  • Java

  • Kotlin

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

现在,您可以创建比 @IsAdmin 更强大的东西,例如 @HasRole,如下所示

  • Java

  • Kotlin

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('{value}')")
public @interface HasRole {
	String value();
}
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('{value}')")
annotation class 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')")

启用特定注解

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

您可以通过以下方式实现

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

上面的代码片段通过首先禁用方法安全性的预配置,然后发布 @PostAuthorize 拦截器 本身来实现这一点。

使用 <intercept-methods> 进行授权

虽然建议使用 Spring Security 的 基于注解的支持 来进行方法安全,但您也可以使用 XML 来声明 Bean 授权规则。

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

  • Xml

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

以编程方式授权方法

正如您已经看到的,您可以使用 方法安全 SpEL 表达式 指定多种非平凡的授权规则。

您可以通过多种方式允许您的逻辑基于 Java 而不是基于 SpEL。这使您可以使用整个 Java 语言来提高可测试性和流程控制。

在 SpEL 中使用自定义 Bean

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

首先,声明一个具有方法的 Bean,该方法接受一个 MethodSecurityExpressionOperations 实例,如下所示

  • Java

  • Kotlin

@Component("authz")
public class AuthorizationLogic {
    public boolean decide(MethodSecurityExpressionOperations operations) {
        // ... authorization logic
    }
}
@Component("authz")
open class AuthorizationLogic {
    fun decide(val operations: MethodSecurityExpressionOperations): boolean {
        // ... authorization logic
    }
}

然后,以以下方式在您的注释中引用该 Bean

  • Java

  • Kotlin

@Controller
public class MyController {
    @PreAuthorize("@authz.decide(#root)")
    @GetMapping("/endpoint")
    public String endpoint() {
        // ...
    }
}
@Controller
open class MyController {
    @PreAuthorize("@authz.decide(#root)")
    @GetMapping("/endpoint")
    fun String endpoint() {
        // ...
    }
}

Spring Security 将为每次方法调用调用该 Bean 上的给定方法。

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

除了返回 Boolean 之外,您还可以返回 null 来指示代码放弃做出决定。

如果您想包含有关决策性质的更多信息,您可以改为返回一个自定义的 AuthorizationDecision,如下所示

  • Java

  • Kotlin

@Component("authz")
public class AuthorizationLogic {
    public AuthorizationDecision decide(MethodSecurityExpressionOperations operations) {
        // ... authorization logic
        return new MyAuthorizationDecision(false, details);
    }
}
@Component("authz")
open class AuthorizationLogic {
    fun decide(val operations: MethodSecurityExpressionOperations): AuthorizationDecision {
        // ... authorization logic
        return MyAuthorizationDecision(false, details)
    }
}

或者抛出一个自定义的 AuthorizationDeniedException 实例。但是请注意,返回对象是首选,因为这不会产生生成堆栈跟踪的开销。

然后,您可以在 自定义授权结果的处理方式 时访问自定义详细信息。

使用自定义授权管理器

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

首先,声明一个授权管理器实例,可能类似于以下实例

  • Java

  • Kotlin

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

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

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

然后,使用与您希望该 AuthorizationManager 运行时相对应的切入点发布方法拦截器。例如,您可以替换 @PreAuthorize@PostAuthorize 的工作方式,如下所示

仅 @PreAuthorize 和 @PostAuthorize 配置
  • Java

  • Kotlin

  • Xml

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

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

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

<aop:config/>

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

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

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

自定义表达式处理

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

自定义 MethodSecurityExpressionHandler
  • Java

  • Kotlin

  • Xml

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

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

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

您也可以子类化DefaultMessageSecurityExpressionHandler,以添加您自己的自定义授权表达式,超出默认值。

使用 AspectJ 进行授权

使用自定义切点匹配方法

基于 Spring AOP 构建,您可以声明与注释无关的模式,类似于请求级授权。这有可能将方法级授权规则集中化。

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

  • Java

  • Kotlin

  • Xml

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

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

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

与 AspectJ 字节码编织集成

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

在设置 AspectJ 后,您可以在@EnableMethodSecurity注释或<method-security>元素中简单地声明您正在使用 AspectJ

  • Java

  • Kotlin

  • Xml

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

结果是 Spring Security 将发布其方面作为 AspectJ 建议,以便它们可以相应地编织。

指定顺序

如前所述,每个注释都有一个 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,因此设置为零意味着事务建议将在所有 Spring Security 建议之前运行。

使用 SpEL 表达授权

您已经看到了几个使用 SpEL 的示例,现在让我们更深入地了解一下 API。

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

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

首先,它为您的 SpEL 表达式提供了一组增强的授权字段和方法。以下是最常用方法的简要概述

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

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

  • hasAuthority - 该方法要求 Authentication 具有与给定值匹配的 GrantedAuthority

  • hasRole - hasAuthority 的快捷方式,它在前面添加 ROLE_ 或配置为默认前缀的任何内容

  • hasAnyAuthority - 该方法要求 Authentication 具有与任何给定值匹配的 GrantedAuthority

  • hasAnyRole - hasAnyAuthority 的快捷方式,它在前面添加 ROLE_ 或配置为默认前缀的任何内容

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

以下是几个最常用的字段

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

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

现在您已经了解了模式、规则以及它们如何组合在一起,您应该能够理解这个更复杂的示例中发生了什么

授权请求
  • Java

  • Kotlin

  • Xml

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

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

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

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

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

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

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

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

    @PreAuthorize("@authz.check(#root)")
    fun shareResource(...): MyResource
}
<sec:method-security>
    <protect-pointcut expression="execution(* com.mycompany.*Service.myDeprecatedMethod(..))" access="denyAll"/> (1)
    <protect-pointcut expression="execution(* com.mycompany.*Service.writeResource(..))" access="hasRole('ADMIN')"/> (2)
    <protect-pointcut expression="execution(* com.mycompany.*Service.deleteResource(..))" access="hasAuthority('db') and hasRole('ADMIN')"/> (3)
    <protect-pointcut expression="execution(* com.mycompany.*Service.readResource(..))" access="principal.claims['aud'] == 'my-audience'"/> (4)
    <protect-pointcut expression="execution(* com.mycompany.*Service.shareResource(..))" access="@authz.check(#root)"/> (5)
</sec:method-security>
1 任何人都不能出于任何原因调用此方法
2 此方法只能由授予 ROLE_ADMIN 权限的 Authentication 调用
3 此方法只能由授予 dbROLE_ADMIN 权限的 Authentication 调用
4 此方法只能由 aud 声明等于 "my-audience" 的 Princpal 调用
5 如果 bean authzcheck 方法返回 true,则只能调用此方法

您可以使用上面这样的 bean authz添加编程授权.

使用方法参数

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

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

  1. 如果 Spring Security 的 @P 注解存在于方法的单个参数上,则使用该值。以下示例使用 @P 注解

    • Java

    • Kotlin

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

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

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

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

      • Java

      • Kotlin

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

      此表达式的目的是要求 name 等于 Authentication#getName,以便授权调用。

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

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

    • 最后,如果您使用调试符号编译代码,则通过使用调试符号来发现参数名称。这对于接口不起作用,因为它们没有关于参数名称的调试信息。对于接口,必须使用注解或 -parameters 方法。

授权任意对象

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

实现此目的的最简单方法是将任何返回要授权的对象的方法标记为 @AuthorizeReturnObject 注解。

例如,考虑以下 User

  • Java

  • Kotlin

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

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

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

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

给定像这样的接口

  • Java

  • Kotlin

public class UserRepository {
	@AuthorizeReturnObject
    Optional<User> findByName(String name) {
		// ...
    }
}
class UserRepository {
    @AuthorizeReturnObject
    fun findByName(name:String?): Optional<User?>? {
        // ...
    }
}

然后,从 findById 返回的任何 User 都将像其他 Spring Security 保护的组件一样被保护

  • Java

  • Kotlin

@Autowired
UserRepository users;

@Test
void getEmailWhenProxiedThenAuthorizes() {
    Optional<User> securedUser = users.findByName("name");
    assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> securedUser.get().getEmail());
}
import jdk.incubator.vector.VectorOperators.Test
import java.nio.file.AccessDeniedException
import java.util.*

@Autowired
var users:UserRepository? = null

@Test
fun getEmailWhenProxiedThenAuthorizes() {
    val securedUser: Optional<User> = users.findByName("name")
    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy{securedUser.get().getEmail()}
}

在类级别使用 @AuthorizeReturnObject

@AuthorizeReturnObject 可以放置在类级别。但是请注意,这意味着 Spring Security 将尝试代理任何返回对象,包括 StringInteger 和其他类型。这通常不是您想要的。

如果您想在返回值类型(如 intStringDouble 或这些类型的集合)的方法的类或接口上使用 @AuthorizeReturnObject,那么您还应该发布相应的 AuthorizationAdvisorProxyFactory.TargetVisitor,如下所示

  • Java

  • Kotlin

@Bean
static Customizer<AuthorizationAdvisorProxyFactory> skipValueTypes() {
    return (factory) -> factory.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes());
}
@Bean
open fun skipValueTypes() = Customizer<AuthorizationAdvisorProxyFactory> {
    it.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes())
}

您可以设置自己的 AuthorizationAdvisorProxyFactory.TargetVisitor 来自定义任何类型集的代理

以编程方式代理

您也可以以编程方式代理给定对象。

要实现这一点,您可以自动装配提供的 AuthorizationProxyFactory 实例,该实例基于您配置的哪种方法安全拦截器。如果您使用的是 @EnableMethodSecurity,那么这意味着它默认情况下将拥有 @PreAuthorize@PostAuthorize@PreFilter@PostFilter 的拦截器。

您可以通过以下方式代理用户实例

  • Java

  • Kotlin

@Autowired
AuthorizationProxyFactory proxyFactory;

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

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

手动构建

如果您需要与 Spring Security 默认值不同的东西,也可以定义自己的实例。

例如,如果您像这样定义 AuthorizationProxyFactory 实例

  • Java

  • Kotlin

import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
import static org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize;
// ...

AuthorizationProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
// and if needing to skip value types
proxyFactory.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes());
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize

// ...

val proxyFactory: AuthorizationProxyFactory = AuthorizationProxyFactory(preAuthorize())
// and if needing to skip value types
proxyFactory.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes())

然后,您可以包装任何 User 实例,如下所示

  • Java

  • Kotlin

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

此功能尚不支持 Spring AOT

代理集合

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

这意味着,当代理对象列表时,以下操作也有效

  • Java

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

代理类

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

这在您需要提前构建代理类时很方便,例如使用 Spring AOT。

对所有方法安全注释的支持

AuthorizationProxyFactory 支持在您的应用程序中启用的任何方法安全注释。它基于作为 bean 发布的任何 AuthorizationAdvisor 类。

由于 @EnableMethodSecurity 默认情况下发布 @PreAuthorize@PostAuthorize@PreFilter@PostFilter 顾问,因此您通常无需执行任何操作即可激活该功能。

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

自定义建议

如果您有希望应用的安全建议,您可以像这样发布您自己的 AuthorizationAdvisor

  • Java

  • Kotlin

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

Spring Security 会将该顾问添加到 AuthorizationProxyFactory 在代理对象时添加的建议集中。

使用 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: Direct self-reference leading to cycle

这是由于 Jackson 如何处理 CGLIB 代理。要解决此问题,请将以下注释添加到 User 类的顶部

  • Java

  • Kotlin

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

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

最后,您需要发布一个 自定义拦截器 来捕获为每个字段抛出的 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"
}

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

在授权被拒绝时提供回退值

在某些情况下,您可能不希望在没有所需权限的情况下调用方法时抛出 AuthorizationDeniedException。相反,您可能希望返回一个后处理结果,例如屏蔽结果,或者在授权被拒绝发生在调用方法之前的情况下返回默认值。

Spring Security 通过使用 @HandleAuthorizationDenied 提供对处理方法调用时授权被拒绝的支持。该处理程序适用于在 @PreAuthorize@PostAuthorize 注释 以及 AuthorizationDeniedException 中发生的被拒绝授权,这些授权是从方法调用本身抛出的。

让我们考虑 上一节 中的示例,但不是创建 AccessDeniedExceptionInterceptor 来将 AccessDeniedException 转换为 null 返回值,我们将使用 @HandleAuthorizationDeniedhandlerClass 属性

  • Java

  • Kotlin

public class NullMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler { (1)

    @Override
    public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
        return null;
    }

}

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Bean (2)
    public NullMethodAuthorizationDeniedHandler nullMethodAuthorizationDeniedHandler() {
        return new NullMethodAuthorizationDeniedHandler();
    }

}

public class User {
    // ...

    @PreAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = NullMethodAuthorizationDeniedHandler.class)
    public String getEmail() {
        return this.email;
    }
}
class NullMethodAuthorizationDeniedHandler : MethodAuthorizationDeniedHandler { (1)

    override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
        return null
    }

}

@Configuration
@EnableMethodSecurity
class SecurityConfig {

    @Bean (2)
    fun nullMethodAuthorizationDeniedHandler(): NullMethodAuthorizationDeniedHandler {
        return MaskMethodAuthorizationDeniedHandler()
    }

}

class User (val name:String, @PreAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = NullMethodAuthorizationDeniedHandler::class) val email:String) (3)
1 创建 MethodAuthorizationDeniedHandler 的实现,该实现返回 null
2 NullMethodAuthorizationDeniedHandler 注册为一个 Bean
3 @HandleAuthorizationDenied 注解方法,并将 NullMethodAuthorizationDeniedHandler 传递给 handlerClass 属性

然后,您可以验证是否返回了 null 值而不是 AccessDeniedException

您也可以用 @Component 注解您的类,而不是创建 @Bean 方法

  • Java

  • Kotlin

@Autowired
UserRepository users;

@Test
void getEmailWhenProxiedThenNullEmail() {
    Optional<User> securedUser = users.findByName("name");
    assertThat(securedUser.get().getEmail()).isNull();
}
@Autowired
var users:UserRepository? = null

@Test
fun getEmailWhenProxiedThenNullEmail() {
    val securedUser: Optional<User> = users.findByName("name")
    assertThat(securedUser.get().getEmail()).isNull()
}

使用方法调用中拒绝的结果

在某些情况下,您可能希望返回从拒绝结果派生的安全结果。例如,如果用户无权查看电子邮件地址,您可能希望对原始电子邮件地址进行一些掩盖,例如,[email protected] 将变为 use******@example.com

对于这些情况,您可以覆盖 MethodAuthorizationDeniedHandler 中的 handleDeniedInvocationResult,它将 MethodInvocationResult 作为参数。让我们继续前面的示例,但不是返回 null,我们将返回电子邮件的掩盖值

  • Java

  • Kotlin

public class EmailMaskingMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler { (1)

    @Override
    public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
        return "***";
    }

    @Override
    public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult) {
        String email = (String) methodInvocationResult.getResult();
        return email.replaceAll("(^[^@]{3}|(?!^)\\G)[^@]", "$1*");
    }

}

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Bean (2)
    public EmailMaskingMethodAuthorizationDeniedHandler emailMaskingMethodAuthorizationDeniedHandler() {
        return new EmailMaskingMethodAuthorizationDeniedHandler();
    }

}

public class User {
    // ...

    @PostAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = EmailMaskingMethodAuthorizationDeniedHandler.class)
    public String getEmail() {
        return this.email;
    }
}
class EmailMaskingMethodAuthorizationDeniedHandler : MethodAuthorizationDeniedHandler {

    override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
        return "***"
    }

    override fun handleDeniedInvocationResult(methodInvocationResult: MethodInvocationResult, authorizationResult: AuthorizationResult): Any {
        val email = methodInvocationResult.result as String
        return email.replace("(^[^@]{3}|(?!^)\\G)[^@]".toRegex(), "$1*")
    }

}

@Configuration
@EnableMethodSecurity
class SecurityConfig {

    @Bean
    fun emailMaskingMethodAuthorizationDeniedHandler(): EmailMaskingMethodAuthorizationDeniedHandler {
        return EmailMaskingMethodAuthorizationDeniedHandler()
    }

}

class User (val name:String, @PostAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = EmailMaskingMethodAuthorizationDeniedHandler::class) val email:String) (3)
1 创建 MethodAuthorizationDeniedHandler 的实现,它返回未授权结果值的掩盖值
2 EmailMaskingMethodAuthorizationDeniedHandler 注册为一个 Bean
3 @HandleAuthorizationDenied 注解方法,并将 EmailMaskingMethodAuthorizationDeniedHandler 传递给 handlerClass 属性

然后,您可以验证是否返回了掩盖的电子邮件而不是 AccessDeniedException

由于您可以访问原始拒绝值,请确保您正确处理它,不要将其返回给调用者。

  • Java

  • Kotlin

@Autowired
UserRepository users;

@Test
void getEmailWhenProxiedThenMaskedEmail() {
    Optional<User> securedUser = users.findByName("name");
    // email is [email protected]
    assertThat(securedUser.get().getEmail()).isEqualTo("use******@example.com");
}
@Autowired
var users:UserRepository? = null

@Test
fun getEmailWhenProxiedThenMaskedEmail() {
    val securedUser: Optional<User> = users.findByName("name")
    // email is [email protected]
    assertThat(securedUser.get().getEmail()).isEqualTo("use******@example.com")
}

在实现 MethodAuthorizationDeniedHandler 时,您可以选择返回的类型

  • null 值。

  • 非空值,尊重方法的返回类型。

  • 抛出异常,通常是 AuthorizationDeniedException 的实例。这是默认行为。

  • 用于响应式应用程序的 Mono 类型。

请注意,由于处理程序必须在您的应用程序上下文中注册为 Bean,因此如果您需要更复杂的逻辑,可以向其中注入依赖项。除此之外,您还可以使用 MethodInvocationMethodInvocationResult,以及 AuthorizationResult,以获取有关授权决定的更多详细信息。

根据可用参数决定要返回的内容

考虑这样一种情况,可能存在针对不同方法的多个掩盖值,如果我们必须为每个方法创建一个处理程序,那么效率就不高,尽管这样做完全没问题。在这种情况下,我们可以使用通过参数传递的信息来决定要做什么。例如,我们可以创建一个自定义的 @Mask 注解和一个检测该注解以决定要返回哪个掩盖值的处理程序

  • Java

  • Kotlin

import org.springframework.core.annotation.AnnotationUtils;

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Mask {

    String value();

}

public class MaskAnnotationDeniedHandler implements MethodAuthorizationDeniedHandler {

    @Override
    public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
        Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class);
        return mask.value();
    }

}

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public MaskAnnotationDeniedHandler maskAnnotationDeniedHandler() {
        return new MaskAnnotationDeniedHandler();
    }

}

@Component
public class MyService {

    @PreAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
    @Mask("***")
    public String foo() {
        return "foo";
    }

    @PreAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
    @Mask("???")
    public String bar() {
        return "bar";
    }

}
import org.springframework.core.annotation.AnnotationUtils

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Mask(val value: String)

class MaskAnnotationDeniedHandler : MethodAuthorizationDeniedHandler {

    override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
        val mask = AnnotationUtils.getAnnotation(methodInvocation.method, Mask::class.java)
        return mask.value
    }

}

@Configuration
@EnableMethodSecurity
class SecurityConfig {

    @Bean
    fun maskAnnotationDeniedHandler(): MaskAnnotationDeniedHandler {
        return MaskAnnotationDeniedHandler()
    }

}

@Component
class MyService {

    @PreAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
    @Mask("***")
    fun foo(): String {
        return "foo"
    }

    @PreAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
    @Mask("???")
    fun bar(): String {
        return "bar"
    }

}

现在,拒绝访问时的返回值将根据 @Mask 注解决定

  • Java

  • Kotlin

@Autowired
MyService myService;

@Test
void fooWhenDeniedThenReturnStars() {
    String value = this.myService.foo();
    assertThat(value).isEqualTo("***");
}

@Test
void barWhenDeniedThenReturnQuestionMarks() {
    String value = this.myService.foo();
    assertThat(value).isEqualTo("???");
}
@Autowired
var myService: MyService

@Test
fun fooWhenDeniedThenReturnStars() {
    val value: String = myService.foo()
    assertThat(value).isEqualTo("***")
}

@Test
fun barWhenDeniedThenReturnQuestionMarks() {
    val value: String = myService.foo()
    assertThat(value).isEqualTo("???")
}

与元注解支持结合

您也可以将 @HandleAuthorizationDenied 与其他注解组合使用,以减少和简化方法中的注解。让我们考虑一下 上一节中的示例,并将 @HandleAuthorizationDenied@Mask 合并。

  • Java

  • Kotlin

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
public @interface Mask {

    String value();

}

@Mask("***")
public String myMethod() {
    // ...
}
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
annotation class Mask(val value: String)

@Mask("***")
fun myMethod(): String {
    // ...
}

现在,您不必记住在方法中需要掩码行为时添加这两个注解。请务必阅读 元注解支持 部分,以了解有关用法的更多详细信息。

@EnableGlobalMethodSecurity 迁移

如果您使用的是 @EnableGlobalMethodSecurity,则应迁移到 @EnableMethodSecurity

方法安全 替换 全局方法安全

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

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

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

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

作为性能优化,在 MethodSecurityExpressionHandler 中引入了一种新方法,该方法接受 Supplier<Authentication> 而不是 Authentication

这允许 Spring Security 延迟 Authentication 的查找,并且当您使用 @EnableMethodSecurity 而不是 @EnableGlobalMethodSecurity 时,会自动利用此优势。

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

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

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

  • Java

  • Kotlin

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

然后在注释中引用它,如下所示

  • Java

  • Kotlin

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

我仍然更喜欢子类化DefaultMethodSecurityExpressionHandler

如果您必须继续子类化DefaultMethodSecurityExpressionHandler,您仍然可以这样做。相反,覆盖createEvaluationContext(Supplier<Authentication>, MethodInvocation)方法,如下所示

  • Java

  • Kotlin

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

进一步阅读

现在您已经保护了应用程序的请求,如果您还没有,请保护其请求。您还可以进一步阅读有关测试您的应用程序或将 Spring Security 与应用程序的其他方面集成,例如数据层跟踪和指标