对于最新的稳定版本,请使用 Spring Security 7.0.4spring-doc.cadn.net.cn

方法安全

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

您可以通过在任意 @Configuration 类上添加 @EnableMethodSecurity 注解,或在任意 XML 配置文件中加入 <method-security> 来在您的应用程序中启用它,如下所示:spring-doc.cadn.net.cn

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

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

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

方法安全还支持许多其他使用场景,包括AspectJ 支持自定义注解以及多个配置点。 请考虑了解以下使用场景:spring-doc.cadn.net.cn

方法安全的工作原理

Spring Security 的方法授权支持对于以下场景非常有用:spring-doc.cadn.net.cn

并且由于方法安全是使用Spring AOP构建的,因此您可以利用其全部表达能力,在需要时覆盖 Spring Security 的默认配置。spring-doc.cadn.net.cn

如前所述,您首先需要在 @EnableMethodSecurity 类中添加 @Configuration 注解,或在 Spring XML 配置文件中添加 <sec:method-security/> 元素。spring-doc.cadn.net.cn

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

  1. 使用简化的 AuthorizationManager API,取代元数据源、配置属性、决策管理器和投票器。 这简化了重用和自定义。spring-doc.cadn.net.cn

  2. 倾向于直接基于 Bean 的配置,而无需通过继承 GlobalMethodSecurityConfiguration 来自定义 Beanspring-doc.cadn.net.cn

  3. 基于原生 Spring AOP 构建,移除了抽象层,使您可以使用 Spring AOP 的构建模块进行自定义spring-doc.cadn.net.cn

  4. 检查是否存在冲突的注解,以确保安全配置明确无歧义spring-doc.cadn.net.cn

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

  6. 默认启用 @PreAuthorize@PostAuthorize@PreFilter@PostFilterspring-doc.cadn.net.cn

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

方法授权是方法执行前和方法执行后授权的结合。 考虑一个以如下方式标注的服务 Bean:spring-doc.cadn.net.cn

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

当方法安全被激活时,对#activate-method-security的某次调用可能看起来类似于如下所示:spring-doc.cadn.net.cn

methodsecurity
  1. Spring AOP 为其代理方法调用 readCustomer。在代理的其他通知器中,它会调用一个与 @PreAuthorize 切入点 匹配的 AuthorizationManagerBeforeMethodInterceptorspring-doc.cadn.net.cn

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

  3. 授权管理器使用 MethodSecurityExpressionHandler 来解析注解的 SpEL 表达式,并从包含 一个 Supplier<Authentication>MethodInvocationMethodSecurityExpressionRoot 构建相应的 EvaluationContextspring-doc.cadn.net.cn

  4. 拦截器使用此上下文来评估表达式;具体来说,它从Supplier中读取Authentication,并检查其权限集合中是否包含permission:readspring-doc.cadn.net.cn

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

  6. 如果没有,拦截器会发布一个 AuthorizationDeniedEvent 并抛出一个 AccessDeniedException,该异常由 ExceptionTranslationFilter 捕获,并向响应返回 403 状态码spring-doc.cadn.net.cn

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

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

  9. 如果没有,拦截器会发布一个 AuthorizationDeniedEvent 并抛出一个 AccessDeniedException,该异常由 ExceptionTranslationFilter 捕获,并向响应返回 403 状态码spring-doc.cadn.net.cn

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

多个注解按顺序计算

如上所示,如果一个方法调用涉及多个方法安全注解(Method Security annotations),那么这些注解将依次被逐一处理。 这意味着它们整体上可以被视为以“逻辑与(AND)”的方式组合在一起。 换句话说,要使一次调用获得授权,所有注解的检查都必须通过授权验证。spring-doc.cadn.net.cn

不支持重复注解

话虽如此,但在同一个方法上重复使用相同的注解是不被支持的。 例如,你不能在同一个方法上放置两次 @PreAuthorizespring-doc.cadn.net.cn

相反,请使用 SpEL 的布尔值支持,或使用其委托给单独 Bean 的功能。spring-doc.cadn.net.cn

每个注解都有其专属的切入点

每个注解都有其自己的切入点(pointcut)实例,该实例会从方法及其所属类开始,在整个对象层次结构中查找该注解或其元注解对应的注解。spring-doc.cadn.net.cn

每个注解都有其对应的方法拦截器

每个注解都有其专用的方法拦截器。 这样做是为了使功能更具组合性。 例如,如果需要,您可以禁用 Spring Security 的默认设置,并仅发布@PostAuthorize方法拦截器spring-doc.cadn.net.cn

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

一般来说,当你添加 @EnableMethodSecurity 时,可以将以下列表视为 Spring Security 所发布的拦截器的代表:spring-doc.cadn.net.cn

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

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

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

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

优先使用授权机构而非复杂的 SpEL 表达式

很多时候,人们可能会忍不住引入一个像下面这样复杂的 SpEL 表达式:spring-doc.cadn.net.cn

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

然而,你也可以改为向拥有 permission:read 的用户授予 ROLE_ADMIN 权限。 实现这一点的一种方法是使用如下所示的 RoleHierarchyspring-doc.cadn.net.cn

@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 表达式,例如:spring-doc.cadn.net.cn

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

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

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

何时应优先选择方法级授权而非请求级授权? 这在一定程度上取决于个人偏好;但请参考以下各自的优点列表,以帮助您做出决定。spring-doc.cadn.net.cn

request-levelspring-doc.cadn.net.cn

method-levelspring-doc.cadn.net.cn

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

coarse-grainedspring-doc.cadn.net.cn

fine-grainedspring-doc.cadn.net.cn

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

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

方法声明的局部作用域spring-doc.cadn.net.cn

配置风格spring-doc.cadn.net.cn

DSLspring-doc.cadn.net.cn

注解spring-doc.cadn.net.cn

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

编程式spring-doc.cadn.net.cn

SpELspring-doc.cadn.net.cn

主要的权衡似乎在于你希望将授权规则放在哪里。spring-doc.cadn.net.cn

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

使用注解进行授权

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

使用 Spring 框架授权方法调用@PreAuthorize

方法安全已启用 时,您可以像这样使用 @PreAuthorize 注解来标注一个方法:spring-doc.cadn.net.cn

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

这表示只有在提供的表达式 hasRole('ADMIN') 通过时,该方法才能被调用。spring-doc.cadn.net.cn

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

@Autowired
BankService bankService;

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

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

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

虽然 @PreAuthorize 在声明所需权限方面非常有用,但它也可以用于评估涉及方法参数的更复杂的表达式spring-doc.cadn.net.cn

授权方法结果与@PostAuthorize

当方法安全功能启用时,您可以使用 @PreAuthorize 注解来标注方法,示例如下:spring-doc.cadn.net.cn

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

这表示该方法仅在所提供的表达式 returnObject.owner == authentication.name 成立时才能返回该值。 returnObject 代表要返回的 Account 对象。spring-doc.cadn.net.cn

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

@Autowired
BankService bankService;

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

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

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

@PostAuthorize 在防范不安全的直接对象引用(Insecure Direct Object Reference)时特别有用。 事实上,它可以像下面这样定义为一个元注解(meta-annotation):spring-doc.cadn.net.cn

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

允许你以如下方式对服务进行注解:spring-doc.cadn.net.cn

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

其结果是,上述方法仅在 Accountowner 属性与当前登录用户的 name 匹配时才会返回该 AccessDeniedException。 否则,Spring Security 将抛出一个 4 异常,并返回 403 状态码。spring-doc.cadn.net.cn

请注意,对于执行数据库写入的类,不推荐使用 @PostAuthorize,因为这通常意味着在检查安全不变量之前已进行了数据库更改。 一个常见的示例是,如果在同一个方法上同时存在 @Transactional@PostAuthorize。 相反,应首先使用 @PostAuthorize 读取值,并在确认该读取操作已授权后,再执行数据库写入。 如果必须执行此类操作,您可以确保 @EnableTransactionManagement@EnableMethodSecurity 之前执行spring-doc.cadn.net.cn

过滤方法参数与@PreFilter

当方法安全功能启用时,您可以使用 @PreAuthorize 注解来标注方法,示例如下:spring-doc.cadn.net.cn

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

这是为了从 accounts 中过滤掉所有不满足表达式 filterObject.owner == authentication.name 的值。 filterObject 代表 account 中的每个 accounts,用于对每个 account 进行测试。spring-doc.cadn.net.cn

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

@Autowired
BankService bankService;

@WithMockUser(username="owner")
@Test
void updateAccountsWhenOwnedThenReturns() {
    Account ownedBy = ...
    Account notOwnedBy = ...
    Collection<Account> updated = this.bankService.updateAccounts(ownedBy, notOwnedBy);
    assertThat(updated).containsOnly(ownedBy);
}
@Autowired
lateinit var bankService: BankService

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

@PreFilter 支持数组、集合、映射(Map)和流(Stream)(前提是该流仍处于打开状态)。spring-doc.cadn.net.cn

例如,上述 updateAccounts 声明将与其他以下四个声明以相同的方式工作:spring-doc.cadn.net.cn

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

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

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

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

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

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

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

其结果是,上述方法将仅返回那些 Account 实例,其中它们的 owner 属性与当前登录用户的 name 相匹配。spring-doc.cadn.net.cn

使用过滤器方法结果@PostFilter

当方法安全功能启用时,您可以使用 @PreAuthorize 注解来标注方法,示例如下:spring-doc.cadn.net.cn

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

这是为了从返回值中过滤掉所有不满足表达式 filterObject.owner == authentication.name 的值。 filterObject 代表 account 中的每一个 accounts,并用于对每个 account 进行测试。spring-doc.cadn.net.cn

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

@Autowired
BankService bankService;

@WithMockUser(username="owner")
@Test
void readAccountsWhenOwnedThenReturns() {
    Collection<Account> accounts = this.bankService.updateAccounts("owner", "not-owner");
    assertThat(accounts).hasSize(1);
    assertThat(accounts.get(0).getOwner()).isEqualTo("owner");
}
@Autowired
lateinit var bankService: BankService

@WithMockUser(username="owner")
@Test
fun readAccountsWhenOwnedThenReturns() {
    val accounts: Collection<Account> = bankService.updateAccounts("owner", "not-owner")
    assertThat(accounts).hasSize(1)
    assertThat(accounts[0].owner).isEqualTo("owner")
}
@PostFilter 也可以作为元注解,在类或接口级别进行定义,并使用SpEL 授权表达式

@PostFilter 支持数组、集合、映射(Map)和流(Stream)(前提是该流仍处于打开状态)。spring-doc.cadn.net.cn

例如,上述readAccounts声明将与以下其他三个声明功能相同:spring-doc.cadn.net.cn

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

@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)
@PostFilter("filterObject.owner == authentication.name")
fun readAccounts(vararg ids: String): Collection<Account>

@PostFilter("filterObject.owner == authentication.name")
fun readAccounts(vararg ids: String): Array<Account>

@PostFilter("filterObject.owner == authentication.name")
fun readAccounts(vararg ids: String): Map<String, Account>

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

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

内存中过滤显然可能代价高昂,因此请慎重考虑是否改为在数据层对数据进行过滤更为合适。

使用 Spring 框架授权方法调用@Secured

@Secured 是用于授权调用的遗留选项。 @PreAuthorize 已取代它,并推荐使用后者。spring-doc.cadn.net.cn

要使用 @Secured 注解,您首先应修改方法安全声明以启用它,如下所示:spring-doc.cadn.net.cn

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

这将使 Spring Security 发布相应的方法拦截器,用于对使用 @Secured 注解标注的方法、类和接口进行授权。spring-doc.cadn.net.cn

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

如果您希望使用 JSR-250 注解,Spring Security 也支持该功能。 @PreAuthorize 具有更强的表达能力,因此推荐使用。spring-doc.cadn.net.cn

要使用 JSR-250 注解,您首先应更改方法安全声明以启用它们,如下所示:spring-doc.cadn.net.cn

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

这将使 Spring Security 发布相应的方法拦截器,用于对使用 @RolesAllowed@PermitAll@DenyAll 注解的方法、类和接口进行授权。spring-doc.cadn.net.cn

在类或接口级别声明注解

也支持在类和接口级别使用方法安全注解。spring-doc.cadn.net.cn

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

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

那么所有方法都将继承类级别的行为。spring-doc.cadn.net.cn

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

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

那么声明该注解的方法将覆盖类级别的注解。spring-doc.cadn.net.cn

对于接口也是如此,但有一个例外:如果一个类从两个不同的接口继承了该注解,那么应用启动将会失败。 这是因为 Spring Security 无法判断你想要使用哪一个。spring-doc.cadn.net.cn

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

使用元注解

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

例如,你可以将 @PreAuthorize("hasRole('ADMIN')") 简化为 @IsAdmin,如下所示:spring-doc.cadn.net.cn

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

现在,您可以在受保护的方法上执行以下操作来代替原先的方式:spring-doc.cadn.net.cn

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

这使得方法定义更具可读性。spring-doc.cadn.net.cn

模板化元注解表达式

你也可以选择使用元注解模板,它能够实现更强大的注解定义。spring-doc.cadn.net.cn

首先,发布以下 Bean:spring-doc.cadn.net.cn

@Bean
static AnnotationTemplateExpressionDefaults templateExpressionDefaults() {
	return new AnnotationTemplateExpressionDefaults();
}
companion object {
    @Bean
    fun templateExpressionDefaults(): AnnotationTemplateExpressionDefaults {
        return AnnotationTemplateExpressionDefaults()
    }
}

现在,你可以创建更强大的注解,例如 @IsAdmin,而不只是使用 @HasRole,如下所示:spring-doc.cadn.net.cn

@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 HasRole(val value: String)

现在,您可以在受保护的方法上执行以下操作来代替原先的方式:spring-doc.cadn.net.cn

@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(id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}

请注意,这也适用于方法变量和所有注解类型,但您需要注意正确处理引号,以确保生成的 SpEL 表达式是正确的。spring-doc.cadn.net.cn

例如,请考虑以下 @HasAnyRole 注解:spring-doc.cadn.net.cn

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

在这种情况下,您会注意到不应在表达式中使用引号,而应像下面这样在参数值中使用:spring-doc.cadn.net.cn

@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(id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}

以便在替换后,该表达式变为 @PreAuthorize("hasAnyRole('USER', 'ADMIN')")spring-doc.cadn.net.cn

启用某些注解

您可以关闭 @EnableMethodSecurity 的预配置,并替换为您自己的配置。 如果您希望 自定义 AuthorizationManagerPointcut,可以选择这样做。 或者,您可能仅希望启用特定的注解,例如 @PostAuthorizespring-doc.cadn.net.cn

你可以通过以下方式实现这一点:spring-doc.cadn.net.cn

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

<aop:config/>

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

上述代码片段通过首先禁用方法安全性的预配置,然后发布 @PostAuthorize 拦截器本身 来实现这一目标。spring-doc.cadn.net.cn

使用 Spring 框架进行授权<intercept-methods>

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

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

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

以编程方式授权方法

正如您已经看到的,有多种方式可以使用方法安全 SpEL 表达式来指定复杂的授权规则。spring-doc.cadn.net.cn

有多种方式可以让你的逻辑基于 Java 而非 SpEL。 这使你可以利用完整的 Java 语言,从而提升可测试性和流程控制能力。spring-doc.cadn.net.cn

在 SpEL 中使用自定义 Bean

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

首先,声明一个包含方法的 Bean,该方法接收一个 MethodSecurityExpressionOperations 实例,如下所示:spring-doc.cadn.net.cn

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

然后,以如下方式在您的注解中引用该 Bean:spring-doc.cadn.net.cn

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

Spring Security 将在每次方法调用时调用该 Bean 上指定的方法。spring-doc.cadn.net.cn

这样做的好处在于,你所有的授权逻辑都集中在一个独立的类中,可以单独进行单元测试,并验证其正确性。 同时,它还能充分利用完整的 Java 语言特性。spring-doc.cadn.net.cn

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

如果你想包含更多关于该决策性质的信息,可以改为返回一个自定义的 AuthorizationDecision,如下所示:spring-doc.cadn.net.cn

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

或者抛出一个自定义的 AuthorizationDeniedException 实例。 不过请注意,返回一个对象是更推荐的做法,因为这样不会产生生成堆栈跟踪(stacktrace)的开销。spring-doc.cadn.net.cn

然后,当你自定义授权结果的处理方式时,就可以访问这些自定义详情。spring-doc.cadn.net.cn

使用自定义授权管理器

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

首先,声明一个授权管理器实例,或许像下面这样:spring-doc.cadn.net.cn

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

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

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

然后,使用一个切点将方法拦截器发布出来,在你需要运行AuthorizationManager的时候。 例如,你可以像这样替换@PreAuthorize@PostAuthorize的工作方式:spring-doc.cadn.net.cn

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

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

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	fun postAuthorize(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 方法拦截器之间。spring-doc.cadn.net.cn

自定义表达式处理

或者,第三,您可以自定义每个 SpEL 表达式的处理方式。 为此,您可以暴露一个自定义的 MethodSecurityExpressionHandler,如下所示:spring-doc.cadn.net.cn

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

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

我们通过一个 MethodSecurityExpressionHandler 方法暴露 static,以确保 Spring 在初始化 Spring Security 的方法安全 @Configuration 类之前就将其发布。spring-doc.cadn.net.cn

您也可以子类化 DefaultMessageSecurityExpressionHandler,以在默认表达式之外添加自定义的授权表达式。spring-doc.cadn.net.cn

使用 AOT

Spring Security 将扫描应用程序上下文中的所有 Bean,查找使用 @PreAuthorize@PostAuthorize 注解的方法。 当找到此类方法时,它会解析安全表达式中使用的任何 Bean,并为该 Bean 注册相应的运行时提示(runtime hints)。 如果发现某个方法使用了 @AuthorizeReturnObject 注解,它将递归地在其返回类型内部查找 @PreAuthorize@PostAuthorize 注解,并相应地进行注册。spring-doc.cadn.net.cn

例如,请考虑以下 Spring Boot 应用程序:spring-doc.cadn.net.cn

@Service
public class AccountService { (1)

    @PreAuthorize("@authz.decide()") (2)
    @AuthorizeReturnObject (3)
    public Account getAccountById(String accountId) {
        // ...
    }

}

public class Account {

    private final String accountNumber;

    // ...

    @PreAuthorize("@accountAuthz.canViewAccountNumber()") (4)
    public String getAccountNumber() {
        return this.accountNumber;
    }

    @AuthorizeReturnObject (5)
    public User getUser() {
        return new User("John Doe");
    }

}

public class User {

    private final String fullName;

    // ...

    @PostAuthorize("@myOtherAuthz.decide()") (6)
    public String getFullName() {
        return this.fullName;
    }

}
@Service
class AccountService { (1)

    @PreAuthorize("@authz.decide()") (2)
    @AuthorizeReturnObject (3)
    fun getAccountById(accountId: String): Account {
        // ...
    }

}

class Account(private val accountNumber: String) {

    @PreAuthorize("@accountAuthz.canViewAccountNumber()") (4)
    fun getAccountNumber(): String {
        return this.accountNumber
    }

    @AuthorizeReturnObject (5)
    fun getUser(): User {
        return User("John Doe")
    }

}

class User(private val fullName: String) {

    @PostAuthorize("@myOtherAuthz.decide()") (6)
    fun getFullName(): String {
        return this.fullName
    }

}
1 Spring Security 找到 AccountService bean
2 找到使用 @PreAuthorize 注解的方法时,它将解析表达式中使用的任何 Bean 名称(在本例中为 authz),并为该 Bean 的类注册运行时提示。
3 找到一个使用 @AuthorizeReturnObject 的方法时,它会检查该方法的返回类型中是否包含任何 @PreAuthorize@PostAuthorize 注解。
4 然后,它找到另一个带有 bean 名称 @PreAuthorizeaccountAuthz 注解;该 bean 类的运行时提示也会被注册。
5 找到另一个 @AuthorizeReturnObject 时,它将再次检查方法的返回类型
6 现在,发现了一个 @PostAuthorize 注解,其中使用了另一个 Bean 名称:myOtherAuthz;同时也会为该 Bean 的类注册运行时提示。

很多时候,Spring Security 无法提前确定方法的实际返回类型,因为该类型可能隐藏在一个被擦除的泛型类型中。spring-doc.cadn.net.cn

考虑以下服务:spring-doc.cadn.net.cn

@Service
public class AccountService {

    @AuthorizeReturnObject
    public List<Account> getAllAccounts() {
        // ...
    }

}
@Service
class AccountService {

    @AuthorizeReturnObject
    fun getAllAccounts(): List<Account> {
        // ...
    }

}

在这种情况下,泛型类型被擦除,因此 Spring Security 无法提前获知需要访问 Account 类以检查是否存在 @PreAuthorize@PostAuthorize 注解。spring-doc.cadn.net.cn

为解决此问题,您可以按如下方式发布 PrePostAuthorizeExpressionBeanHintsRegistrarspring-doc.cadn.net.cn

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static SecurityHintsRegistrar registerTheseToo() {
    return new PrePostAuthorizeExpressionBeanHintsRegistrar(Account.class);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun registerTheseToo(): SecurityHintsRegistrar {
    return PrePostAuthorizeExpressionBeanHintsRegistrar(Account::class.java)
}

使用 AspectJ 进行授权

使用自定义切点匹配方法

由于基于 Spring AOP 构建,您可以声明与注解无关的模式,类似于请求级授权。 这样做有可能将方法级授权规则集中管理。spring-doc.cadn.net.cn

例如,您可以使用 Advisor 发布您自己的内容,或使用 <protect-pointcut> 将 AOP 表达式与服务层的授权规则进行匹配,如下所示:spring-doc.cadn.net.cn

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 字节码中,从而提升性能。spring-doc.cadn.net.cn

设置好 AspectJ 后,你只需在 @EnableMethodSecurity 注解或 <method-security> 元素中简单地声明你正在使用 AspectJ 即可:spring-doc.cadn.net.cn

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

其结果是,Spring Security 将会将其通知器(advisors)发布为 AspectJ 通知(advice),以便能够相应地进行织入。spring-doc.cadn.net.cn

指定顺序

如前所述,每个注解都有一个对应的 Spring AOP 方法拦截器,而这些拦截器在 Spring AOP 通知器链中都有各自的位置。spring-doc.cadn.net.cn

具体来说,@PreFilter 方法拦截器的顺序是 100,@PreAuthorize 的顺序是 200,依此类推。spring-doc.cadn.net.cn

你可以使用 offset 注解上的 @EnableMethodSecurity 参数,批量移动所有拦截器,以在方法调用过程中更早或更晚地提供其通知(advice)。spring-doc.cadn.net.cn

使用 SpEL 表达授权

你已经看过几个使用 SpEL 的示例,现在让我们更深入地了解一下该 API。spring-doc.cadn.net.cn

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

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

这首先为您的 SpEL 表达式提供了一组增强的授权字段和方法。 以下是对最常用方法的快速概述:spring-doc.cadn.net.cn

以下是常见字段的简要介绍:spring-doc.cadn.net.cn

现在你已经了解了这些模式、规则以及它们如何组合使用,应该能够理解这个更复杂示例中所发生的事情了:spring-doc.cadn.net.cn

授权请求
@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 此方法只能由被授予 Authentication 权限的 ROLE_ADMIN 调用。
3 此方法只能由拥有 Authenticationdb 权限的 ROLE_ADMIN 调用
4 此方法只能由具有 Princpal 声明(claim)且其值等于 "my-audience" 的 aud 调用。
5 只有在 bean authzcheck 方法返回 true 时,才能调用此方法。

你可以像上面的 authz 这样的 bean 来添加编程式授权spring-doc.cadn.net.cn

使用方法参数

此外,Spring Security 还提供了一种机制用于发现方法参数,以便这些参数也可以在 SpEL 表达式中被访问。spring-doc.cadn.net.cn

作为完整参考,Spring Security 使用 DefaultSecurityParameterNameDiscoverer 来发现参数名称。 默认情况下,会为方法尝试以下选项。spring-doc.cadn.net.cn

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

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

    该表达式的意图是要求当前的Authentication对象对此write实例具有Contact权限。spring-doc.cadn.net.cn

    在底层,这是通过使用 AnnotationParameterNameDiscoverer 实现的,你可以对其进行自定义,以支持任意指定注解的 value 属性。spring-doc.cadn.net.cn

  2. 如果方法中至少有一个参数使用了 Spring Data@Param 注解,则会使用该注解的值。 以下示例使用了 @Param 注解:spring-doc.cadn.net.cn

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

    该表达式的意图是要求 name 必须等于 Authentication#getName,才能授权此次调用。spring-doc.cadn.net.cn

    在底层,这是通过使用 AnnotationParameterNameDiscoverer 实现的,你可以对其进行自定义,以支持任意指定注解的 value 属性。spring-doc.cadn.net.cn

  3. 如果你使用 -parameters 参数编译你的代码,将使用标准的 JDK 反射 API 来发现参数名称。 这在类和接口上都有效。spring-doc.cadn.net.cn

  4. 最后,如果你使用调试符号编译代码,参数名称将通过调试符号被自动发现。 这种方法对接口无效,因为接口不包含关于参数名称的调试信息。 对于接口,必须使用注解或 -parameters 方法。spring-doc.cadn.net.cn

授权任意对象

Spring Security 还支持包装任何其方法带有 Spring Security 注解的对象。spring-doc.cadn.net.cn

实现这一目标最简单的方法是,使用 @AuthorizeReturnObject 注解标记任何返回你希望进行授权的对象的方法。spring-doc.cadn.net.cn

例如,考虑以下 User 类:spring-doc.cadn.net.cn

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)

给定一个如下所示的接口:spring-doc.cadn.net.cn

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

那么,从 User 返回的任何 findById 对象都将像其他受 Spring Security 保护的组件一样受到安全保护:spring-doc.cadn.net.cn

@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 以及其他类型。 这通常并不是你想要的行为。spring-doc.cadn.net.cn

如果你想在某个类或接口上使用 @AuthorizeReturnObject,而该类或接口的方法返回的是值类型(例如 intStringDouble 或这些类型的集合),那么你还应按如下方式发布相应的 AuthorizationAdvisorProxyFactory.TargetVisitorspring-doc.cadn.net.cn

import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;

// ...

@Bean
static TargetVisitor skipValueTypes() {
    return TargetVisitor.defaultsSkipValueTypes();
}
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor

// ...

@Bean
open fun skipValueTypes() = TargetVisitor.defaultsSkipValueTypes()

您可以设置自己的 AuthorizationAdvisorProxyFactory.TargetVisitor 来为任意类型集合自定义代理行为。spring-doc.cadn.net.cn

以编程方式代理

你也可以以编程方式为给定对象创建代理。spring-doc.cadn.net.cn

为实现这一点,您可以自动装配所提供的 AuthorizationProxyFactory 实例,该实例基于您所配置的方法安全拦截器。 如果您使用的是 @EnableMethodSecurity,那么默认情况下,它将包含用于 @PreAuthorize@PostAuthorize@PreFilter@PostFilter 的拦截器。spring-doc.cadn.net.cn

你可以通过以下方式代理一个用户实例:spring-doc.cadn.net.cn

@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 默认配置的内容,也可以定义自己的实例。spring-doc.cadn.net.cn

例如,如果你像下面这样定义一个 AuthorizationProxyFactory 实例:spring-doc.cadn.net.cn

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 实例:spring-doc.cadn.net.cn

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

代理集合

AuthorizationProxyFactory 通过代理元素类型来支持 Java 集合、流、数组、Optional 和迭代器,并通过代理值类型来支持 Map。spring-doc.cadn.net.cn

这意味着在代理一个List对象列表时,以下方式也同样有效:spring-doc.cadn.net.cn

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

一个适用此功能的场景是当你需要提前构建代理类时,例如使用 Spring AOT 的情况。spring-doc.cadn.net.cn

支持所有方法安全注解

AuthorizationProxyFactory 支持您的应用程序中启用的任何方法安全注解。 它基于作为 Bean 发布的任意 AuthorizationAdvisor 类。spring-doc.cadn.net.cn

由于 @EnableMethodSecurity 默认会发布 @PreAuthorize@PostAuthorize@PreFilter@PostFilter 的通知器(advisors),因此通常情况下你无需额外操作即可启用这些功能。spring-doc.cadn.net.cn

使用 returnObjectfilterObject 的 SpEL 表达式位于代理之后,因此可以完全访问该对象。spring-doc.cadn.net.cn

自定义通知

如果你还有安全建议希望一并应用,你可以像下面这样发布你自己的 AuthorizationAdvisorspring-doc.cadn.net.cn

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

并且 Spring Security 会将该通知器(advisor)添加到 AuthorizationProxyFactory 在代理对象时所使用的通知(advice)集合中。spring-doc.cadn.net.cn

使用 Jackson

该功能的一个强大用法是从控制器返回一个受保护的值,如下所示:spring-doc.cadn.net.cn

@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)
    }
}
@Component
public class Null implements MethodAuthorizationDeniedHandler {
    @Override
    public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
        return null;
    }
}

// ...

@HandleAuthorizationDenied(handlerClass = Null.class)
public class User {
	...
}
@Component
class Null : MethodAuthorizationDeniedHandler {
    override fun handleDeniedInvocation(methodInvocation: MethodInvocation?, authorizationResult: AuthorizationResult?): Any? {
        return null
    }
}

// ...

@HandleAuthorizationDenied(handlerClass = Null.class)
open class User {
	...
}

然后,您将看到基于用户授权级别的不同 JSON 序列化结果。 如果用户没有 user:read 权限,那么他们将看到:spring-doc.cadn.net.cn

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

如果他们确实拥有该权限,将会看到:spring-doc.cadn.net.cn

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

你也可以添加 Spring Boot 属性 spring.jackson.default-property-inclusion=non_null,在序列化时排除 null 值,如果你也不希望向未授权用户暴露该 JSON 键的话。spring-doc.cadn.net.cn

使用 AOT

Spring Security 将扫描应用程序上下文中的所有 Bean,查找使用 @AuthorizeReturnObject 注解的方法。 当找到此类方法时,它会提前创建并注册相应的代理类。 它还会递归地搜索其他同样使用 @AuthorizeReturnObject 注解的嵌套对象,并相应地进行注册。spring-doc.cadn.net.cn

例如,请考虑以下 Spring Boot 应用程序:spring-doc.cadn.net.cn

@SpringBootApplication
public class MyApplication {
	@RestController
    public static class MyController { (1)
		@GetMapping
        @AuthorizeReturnObject
        Message getMessage() { (2)
			return new Message(someUser, "hello!");
        }
    }

	public static class Message { (3)
		User to;
		String text;

		// ...

        @AuthorizeReturnObject
        public User getTo() { (4)
			return this.to;
        }

		// ...
	}

	public static class User { (5)
		// ...
	}

	public static void main(String[] args) {
		SpringApplication.run(MyApplication.class);
	}
}
@SpringBootApplication
open class MyApplication {
	@RestController
    open class MyController { (1)
		@GetMapping
        @AuthorizeReturnObject
        fun getMessage():Message { (2)
			return Message(someUser, "hello!")
        }
    }

	open class Message { (3)
		val to: User
		val test: String

		// ...

        @AuthorizeReturnObject
        fun getTo(): User { (4)
			return this.to
        }

		// ...
	}

	open class User { (5)
		// ...
	}

	fun main(args: Array<String>) {
		SpringApplication.run(MyApplication.class)
	}
}
1 - 首先,Spring Security 找到 MyController bean
2 - 找到使用 @AuthorizeReturnObject 注解的方法时,它会代理返回值 Message,并将该代理类注册到 RuntimeHints
3 - 然后,它遍历 Message,以检查是否使用了 @AuthorizeReturnObject
4 - 找到使用 @AuthorizeReturnObject 注解的方法时,它会代理返回值 User,并将该代理类注册到 RuntimeHints
5 - 最后,它遍历 User 以检查是否使用了 @AuthorizeReturnObject;未找到任何内容,算法完成

很多时候,Spring Security 无法提前确定代理类,因为它可能隐藏在一个被擦除的泛型类型中。spring-doc.cadn.net.cn

考虑对 MyController 进行如下修改:spring-doc.cadn.net.cn

@RestController
public static class MyController {
    @GetMapping
    @AuthorizeReturnObject
    List<Message> getMessages() {
        return List.of(new Message(someUser, "hello!"));
    }
}
@RestController
static class MyController {
    @AuthorizeReturnObject
    @GetMapping
    fun getMessages(): Array<Message> = arrayOf(Message(someUser, "hello!"))
}

在这种情况下,泛型类型会被擦除,因此 Spring Security 无法在运行前明确得知 Message 在运行时需要被代理。spring-doc.cadn.net.cn

为了解决这个问题,你可以像下面这样发布 AuthorizeProxyFactoryHintsRegistrarspring-doc.cadn.net.cn

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static SecurityHintsRegsitrar registerTheseToo(AuthorizationProxyFactory proxyFactory) {
	return new AuthorizeReturnObjectHintsRegistrar(proxyFactory, Message.class);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun registerTheseToo(proxyFactory: AuthorizationProxyFactory?): SecurityHintsRegistrar {
    return AuthorizeReturnObjectHintsRegistrar(proxyFactory, Message::class.java)
}

Spring Security 将注册该类,然后像之前一样遍历其类型。spring-doc.cadn.net.cn

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

在某些场景下,当某个方法在没有所需权限的情况下被调用时,您可能不希望抛出 AuthorizationDeniedException 异常。 相反,您可能希望返回一个经过后处理的结果,例如屏蔽后的结果,或在授权被拒绝(发生在方法调用之前)的情况下返回一个默认值。spring-doc.cadn.net.cn

Spring Security 通过使用 @HandleAuthorizationDenied 提供对方法调用时被拒绝授权的处理支持。 该处理器适用于在 @PreAuthorize@PostAuthorize 注解 中发生的被拒绝授权,以及由方法调用本身抛出的 AuthorizationDeniedExceptionspring-doc.cadn.net.cn

让我们考虑上一节中的示例,但这次我们不再创建AccessDeniedExceptionInterceptor来将AccessDeniedException转换为null返回值,而是使用handlerClass注解中的@HandleAuthorizationDenied属性:spring-doc.cadn.net.cn

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 值,而不是 AccessDeniedExceptionspring-doc.cadn.net.cn

你也可以使用 @Component 注解来标注你的类,而不必创建一个 @Bean 方法。spring-doc.cadn.net.cn

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

对于这些场景,您可以从 MethodAuthorizationDeniedHandler 覆盖 handleDeniedInvocationResult,其参数为带有 MethodInvocationResult。 让我们继续前面的示例,但这次我们不返回 null,而是返回电子邮件的掩码值:spring-doc.cadn.net.cn

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 属性

然后你可以验证返回的是一个掩码后的电子邮件,而不是一个 AccessDeniedExceptionspring-doc.cadn.net.cn

由于您可以访问原始被拒绝的值,请确保正确处理该值,不要将其返回给调用方。spring-doc.cadn.net.cn

@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 时,你有几种可选的返回类型:spring-doc.cadn.net.cn

请注意,由于处理器必须在您的应用上下文中注册为 Bean,因此如果需要更复杂的逻辑,您可以向其中注入依赖项。 此外,您还可以使用 MethodInvocationMethodInvocationResult,以及 AuthorizationResult 来获取与授权决策相关的更多详细信息。spring-doc.cadn.net.cn

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

考虑这样一种场景:不同方法可能对应多个掩码(mask)值。如果我们为每个方法都创建一个处理器,虽然这样做完全可行,但效率并不高。 在这种情况下,我们可以利用通过参数传递的信息来决定如何处理。 例如,我们可以创建一个自定义的 @Mask 注解,以及一个能够检测该注解的处理器,从而决定返回哪个掩码值:spring-doc.cadn.net.cn

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 注解来决定:spring-doc.cadn.net.cn

@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 合并:spring-doc.cadn.net.cn

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

现在,当您需要在方法中使用掩码行为时,无需再记住同时添加这两个注解了。 请务必阅读元注解支持部分,以了解更多关于用法的详细信息。spring-doc.cadn.net.cn

从...迁移@EnableGlobalMethodSecurity

如果你正在使用 @EnableGlobalMethodSecurity,你应该迁移到 @EnableMethodSecurityspring-doc.cadn.net.cn

替换全局方法安全使用方法安全

@EnableGlobalMethodSecurity<global-method-security> 已弃用,分别推荐使用 @EnableMethodSecurity<method-security>。 新的注解和 XML 元素默认激活 Spring 的 pre-post 注解,并在内部使用 AuthorizationManagerspring-doc.cadn.net.cn

这意味着以下两个代码清单在功能上是等价的:spring-doc.cadn.net.cn

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

对于未使用 pre-post 注解的应用程序,请确保将其关闭,以避免激活不必要的行为。spring-doc.cadn.net.cn

例如,如下所示的列表:spring-doc.cadn.net.cn

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

应更改为:spring-doc.cadn.net.cn

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

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

作为一种性能优化,向 MethodSecurityExpressionHandler 引入了一个新方法,该方法接收一个 Supplier<Authentication> 而不是 Authenticationspring-doc.cadn.net.cn

这使得 Spring Security 能够延迟查找 Authentication 对象,并且当你使用 @EnableMethodSecurity 而非 @EnableGlobalMethodSecurity 时,该特性会被自动利用。spring-doc.cadn.net.cn

然而,假设你的代码继承了 DefaultMethodSecurityExpressionHandler 并重写了 createSecurityExpressionRoot(Authentication, MethodInvocation) 方法,以返回一个自定义的 SecurityExpressionRoot 实例。 这将不再有效,因为 @EnableMethodSecurity 所设置的机制会转而调用 createEvaluationContext(Supplier<Authentication>, MethodInvocation) 方法。spring-doc.cadn.net.cn

幸运的是,通常不需要如此高级别的自定义。 相反,你可以创建一个包含所需授权方法的自定义 Bean。spring-doc.cadn.net.cn

例如,假设你希望对 @PostAuthorize("hasAuthority('ADMIN')") 进行自定义评估。 你可以创建一个如下所示的自定义 @Beanspring-doc.cadn.net.cn

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

然后在注解中像这样引用它:spring-doc.cadn.net.cn

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

我仍然更倾向于子类化DefaultMethodSecurityExpressionHandler

如果你必须继续继承 DefaultMethodSecurityExpressionHandler,你仍然可以这样做。 相反,请像下面这样重写 createEvaluationContext(Supplier<Authentication>, MethodInvocation) 方法:spring-doc.cadn.net.cn

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

进一步阅读

现在已经确保了您应用程序的请求,请如果还没有的话,确保其请求的安全性。 您也可以继续阅读关于测试您的应用程序的内容,或者了解如何将Spring Security与其他方面结合使用,例如数据层跟踪和指标spring-doc.cadn.net.cn