1. 前言

在实际项目中,安全控制不仅体现在URL拦截层面,方法级安全控制也越来越受到重视。Spring Security提供了多种方式实现方法级安全,通过方法注解体系,这种细粒度控制使得我们能够在方法调用前、调用后,甚至返回值处理阶段实施安全检查,真正成为开发者保护服务接口的重要手段。

相信小伙伴们通过前面章节的学习,发现了博主在方法上进行角色和菜单资源验证时使用的一个注解:@PreAuthorize。那么本章节博主将带着大家剖析**@PreAuthorize**注解的核心原理、SpEL表达式机制,并通过示例代码演示如何在实际项目中灵活运用该注解实现细粒度的权限控制。


2. @PreAuthorize注解简介

@PreAuthorize注解可以在方法执行前对传入的参数、当前用户信息、认证状态等进行校验,从而决定是否允许方法执行。常见使用场景包括:

  • 限制某个接口或方法只允许特定角色访问;
  • 根据方法参数和认证信息动态判断权限;
  • 调用自定义的权限判断逻辑(例如上一章节中结合自定义PermissionEvaluator);

Spring Security内部通过AOP拦截被@PreAuthorize修饰的方法,并利用Spring Expression Language(SpEL)对注解中定义的表达式进行求值。只有当表达式求值结果为true时,方法才会执行,否则会抛出拒绝访问异常。


3. @PreAuthorize核心原理解析

Spring Security开启方法级安全控制实际上非常简单,只需要在@Configuration配置类中添加@EnableMethodSecurity

1
2
3
4
5
6
@Configuration
// 开启方法级的安全控制
@EnableMethodSecurity
public class AbacSecurityConfig {
//....
}

方法授权可以分为方法前授权方法后授权的组合,看下面的例子:

1
2
3
4
5
6
@Service
public class MyCustomerService {
@PreAuthorize("hasAuthority('permission:read')")
@PostAuthorize("returnObject.owner == authentication.name")
public Customer readCustomer(String id) { ... }
}

当方法安全性被激活时,对MyCustomerService#readCustomer的调用流程如下(官方流程图):

方法级安全控制流程

流程解析

  1. Spring AOPreadCustomer调用其代理方法。它调用与AuthorizationManagerBeforeMethodInterceptor切入点匹配的@PreAuthorize
  2. 拦截器调用PreAuthorizeAuthorizationManager#check
  3. 授权管理器使用MethodSecurityExpressionHandler解析注解的SpEL表达式,并从包含EvaluationContext和MethodSecurityExpressionRoot的Supplier构造对应的MethodInvocation
  4. 拦截器使用此上下文来评估表达式,它从Authentication读取Supplier,并检查其权限集合中是否有permission:read
  5. 如果评估通过,那么Spring AOP继续调用该方法。
  6. 如果不通过,拦截器发布一个AuthorizationDeniedEvent并抛出一个AccessDeniedExceptionExceptionTranslationFilter捕获并向响应返回一个403状态码。
  7. 方法返回后,Spring AOP调用与切入点匹配的AuthorizationManagerAfterMethodInterceptor,操作与上面相同,但是针对@PostAuthorize
  8. 如果评估通过(在这种情况下,返回值属于登录用户),则处理继续正常进行。
  9. 如果不通过,拦截器将发布一个AuthorizationDeniedEvent并抛出一个AccessDeniedException,然后由ExceptionTranslationFilter捕获并向响应返回403状态代码。

拦截与表达式求值

从上述官方介绍的工作流程来看,我们可以简单总结为:

Spring Security在启用方法级安全时,会在应用上下文中配置一个MethodSecurityInterceptor(基于AOP实现)。当被@PreAuthorize修饰的方法被调用时:

  • 拦截器捕获方法调用,并构造EvaluationContext,上下文中包含认证信息、方法参数等数据;
  • 使用MethodSecurityExpressionHandler将注解中的SpEL表达式求值,判断是否满足访问条件;
  • 如果表达式结果为false,则抛出AccessDeniedException;否则放行执行方法。

拦截流程示意图

SpEL表达式

@PreAuthorize注解的值是一个SpEL表达式,可以引用以下内置变量:

  • authentication:当前用户的认证对象。
  • principal:当前认证用户的主体信息(通常为UserDetails对象)。
  • #root:表达式根对象。
  • 方法参数:可以通过#paramName#p0访问方法参数。

如下代码:

1
2
@PreAuthorize("hasRole('ADMIN') and #id > 10")
public void deleteUser(Long id) { ... }

表示只有当前用户拥有ADMIN角色且方法参数id大于10时,才能执行该方法。

自定义扩展

通过自定义MethodSecurityExpressionHandler和PermissionEvaluator,可以扩展@PreAuthorize表达式功能。具体可以查阅上一章节ABAC属性权限模型实战开发。


4. 注解应用实战

下面通过一些简单示例,演示如何配置Spring Security方法级安全。

❶ 基础权限校验

1
2
3
4
5
6
7
8
9
@PreAuthorize("hasRole('ADMIN')")
public void deleteResource(Long resourceId) {
// 方法实现
}

@PreAuthorize("hasAuthority('RESOURCE_APPROVE')")
public void approveRequest(Request request) {
// 审批逻辑
}

❷ 参数级权限控制

1
2
3
4
5
6
7
8
9
10
11
// 校验创建者匹配
@PreAuthorize("#article.createdBy == authentication.name")
public void updateArticle(Article article) {
// 更新操作
}

// 参数过滤示例
@PreAuthorize("@permissionChecker.hasAccess(#userId, 'EDIT')")
public void editUserProfile(Long userId, Profile profile) {
// 编辑逻辑
}

❸ 动态业务规则集成

如没有了解的小伙伴,建议查阅博主上一章节内容(这里仅演示如何配置@PreAuthorize):
最新Spring Security实战教程(六)基于数据库的ABAC属性权限模型实战开发

1
2
3
4
5
6
7
8
9
10
11
12
public class DocumentPermissionEvaluator {
public boolean checkAccess(Long docId, String permission) {
// 自定义文档权限校验逻辑
return true; // 示例返回
}
}

// 在SpEL中调用自定义评估器
@PreAuthorize("@documentPermissionEvaluator.checkAccess(#docId, 'WRITE')")
public void updateDocument(Long docId, String content) {
// 文档更新
}

❹ 复合条件表达式

1
2
3
4
5
// 组合多个条件
@PreAuthorize("hasRole('ADMIN') or (#user.department == authentication.principal.department and hasAuthority('DEPT_ADMIN'))")
public void manageUser(User user) {
// 用户管理逻辑
}

❺ 返回值后校验

1
2
3
4
5
@PostAuthorize("returnObject.owner == authentication.name")
public Document getConfidentialDocument(Long id) {
// 获取文档逻辑
return new Document();
}

❻ 参数预处理

1
2
3
4
5
6
7
@PreFilter("filterObject.owner == authentication.name")
public void batchProcess(List<Document> documents) {
// 仅处理当前用户拥有的文档
documents.forEach(doc -> {
// 处理文档
});
}

❼ 结合Spring Bean的复杂校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class CustomSecurityEvaluator {

public boolean isWeekend() {
return LocalDate.now().getDayOfWeek().getValue() >= 6;
}

public boolean checkBusinessHours() {
int hour = LocalTime.now().getHour();
return hour >= 9 && hour <= 18;
}
}

// 使用自定义Bean进行校验
@PreAuthorize("@customSecurityEvaluator.checkBusinessHours() and hasRole('OPERATOR')")
public void performOperation() {
// 仅在工作时间可执行的操作
}

5. 常见表达式对照表

为了方便大家在实际开发中快速查阅,博主整理了常用的SpEL表达式对照表:

表达式示例 说明
hasRole('ADMIN') 当前用户是否拥有ADMIN角色
hasAnyRole('ADMIN','USER') 当前用户是否拥有任一指定角色
hasAuthority('sys:user:delete') 当前用户是否拥有指定权限
hasAnyAuthority('sys:user:add','sys:user:edit') 当前用户是否拥有任一指定权限
isAuthenticated() 用户是否已认证
isAnonymous() 用户是否为匿名用户
isRememberMe() 用户是否通过RememberMe认证
fullyAuthenticated() 用户是否为完整认证(非RememberMe)
#id == 1 方法参数id是否等于1
#user.name == authentication.name 参数user的name属性是否等于当前用户名
@bean.check(#param) 调用Spring Bean的自定义方法
returnObject != null 返回值不能为null(用于后处理)

6. 常见问题与解决方案

问题1:@PreAuthorize不生效

可能原因:

  • 忘记添加@EnableMethodSecurity注解
  • 方法不是由Spring容器管理的Bean
  • 在同一个类中调用被@PreAuthorize修饰的方法(AOP内部调用失效)

解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 确保配置类添加注解
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
// ...
}

// 避免内部调用,可通过代理调用
@Service
public class UserService {

@Autowired
private UserService self; // 注入自身代理

@PreAuthorize("hasRole('ADMIN')")
public void adminMethod() {
// 安全方法
}

public void normalMethod() {
// 正确方式:通过代理调用
self.adminMethod();
}
}

问题2:方法参数名称无法识别

原因: Java编译时未保留参数名称信息

解决方案: 在pom.xml中添加编译参数

1
2
3
4
5
6
7
8
9
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>

或者使用参数索引:

1
2
3
4
@PreAuthorize("#p0 == authentication.name") // 使用参数索引
public void updateUser(String username, User user) {
// ...
}

问题3:复杂表达式导致代码可读性差

解决方案: 将复杂逻辑提取到自定义Bean中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class ComplexPermissionChecker {

public boolean canApprove(Authentication auth, ApprovalRequest request) {
// 复杂的权限校验逻辑
return true;
}
}

// 使用简洁表达式
@PreAuthorize("@complexPermissionChecker.canApprove(authentication, #request)")
public void approve(ApprovalRequest request) {
// ...
}

结语

通过本章节方法级安全控制的介绍,相信大家已经能通过灵活运用表达式语言和自定义扩展,让我们可以在保证系统安全性的同时,维持代码的优雅与可维护性!希望这章文章对你在Spring Security方法级安全控制的实践中提供帮助和启发!

在后续的章节中,我们将继续深入探讨Spring Security的其他高级特性,敬请期待!