1. 前言

博主在持续更新Spring Security教程过程中,有一些小伙伴总会私信问到之前关于ABAC属性权限模型那一个章节中自定义策略条件表达式的问题,如下图:

自定义策略表达式

温馨提示:
没了解ABAC属性权限模型的可以访问:最新Spring Security实战教程(六)基于数据库的ABAC属性权限模型实战开发进行了解。

大家最关心的问题就是:为什么在EvaluationContext上下文中传入了对应的自定义策略条件表达式,Spring Security就能进行更细粒度控制?

针对这些问题,博主觉得还是有必要再补充一下SpELSpring Security中的一些应用技巧。


2. SpEL在Spring Security中的基本应用

Spring Expression Language(SpEL)作为Spring生态的通用表达式引擎,在Spring Security中扮演着权限控制的灵魂角色。通过SpEL表达式让权限判断更灵活、更具有可配置性。我们不仅可以对用户角色进行细粒度控制,还可以在方法参数、请求信息等多维度上做出动态判断,满足复杂业务场景下的安全需求。

2.1 基础权限校验

Spring Security的注解中,经常会看到@PreAuthorize@PostAuthorize@PreFilter@PostFilter等注解,实际上这些注解内部都使用了SpEL来编写权限表达式:

1
2
3
4
5
@PreAuthorize("hasAuthority('USER_CREATE')")
public void createUser(User user) { /*...*/ }

@PreAuthorize("hasRole('ADMIN') or hasRole('AUDITOR')")
public void auditSystem() { /*...*/ }

在上面的例子中,hasRole('ADMIN')就是基于SpEL的表达式。Spring Security内部会将该表达式编译并执行,根据认证对象中的角色信息决定是否允许访问。

2.2 复杂对象属性校验

下面举例:Project.owner(项目所有者必须是当前认证用户并且项目状态 = EDITABLE):

1
2
3
4
5
6
7
public record Project(Long id, String owner, String status) {}

// 校验项目所有者且项目状态为可编辑
@PreAuthorize("#project.owner == authentication.name and #project.status == 'EDITABLE'")
public void updateProject(Project project) {
// 更新逻辑
}

2.3 方法参数动态处理

下面再看看方法上的动态处理:

1
2
3
4
5
6
7
8
9
10
11
// 校验用户ID与当前认证用户匹配
@PreAuthorize("#userId == authentication.principal.id")
public User getUserProfile(Long userId) {
return userService.findById(userId);
}

// 集合参数过滤
@PostFilter("filterObject.department == authentication.principal.department")
public List<Document> getAllDocuments() {
return documentRepository.findAll();
}

通过上述的几个例子,就可以知道Spring Security常见的几个注解就是基于SpEL的表达式。


3. 常用的SpEL表达式及变量

Spring Security中,SpEL表达式可以访问以下几个常见的内置变量及方法,从而实现灵活的权限控制:

❶ authentication

获取当前用户的身份认证信息,包含用户名、密码、授权信息等。

示例:

1
2
3
// 判断传递用户名是否当前认证信息中的用户
@PreAuthorize("authentication.principal.username == #username")
public void getUserDetails(String username) { /*...*/ }

❷ principal

当前经过认证的用户对象,通常是一个实现了UserDetails接口的对象。

示例:

1
2
3
// 判断当前用户是否启用
@PreAuthorize("principal.enabled")
public void sensitiveOperation() { /*...*/ }

❸ permitAll / denyAll

快速通过/拒绝所有访问。

示例:

1
2
@PreAuthorize("permitAll") // 等价于 @PermitAll
public String publicApi() { /*...*/ }

❹ #this

当前上下文中的对象,即当前方法调用对象(非静态方法)。

示例:

1
2
@PreAuthorize("#this.isFeatureEnabled()")
public void usePremiumFeature() { /*...*/ }

❺ #参数名

直接访问方法参数。

示例:

1
2
@PreAuthorize("#user.id == principal.id")
public void updateUser(User user) { /*...*/ }

❻ returnObject

方法返回值(仅限@PostAuthorize和@PostFilter)。

示例:

1
2
3
// 返回Document对象中owner = 当前用户名 才允许访问该接口
@PostAuthorize("returnObject.owner == principal.username")
public Document getConfidentialDoc() { /*...*/ }

❼ target / targetObject

AOP代理的原始目标对象(目标方法所属的对象)。如:当前有一个adminService对象,adminService对象下有adminOperation方法,target = adminService。

示例:

1
2
@PreAuthorize("target.isFeatureEnabled('ADVANCED_MODE')")
public void adminOperation() { /*...*/ }

❽ @beanName

访问Spring容器中的Bean,如Spring容器中有一个RiskEvaluator的bean,可以通过下面示例调用。

示例:

1
2
@PreAuthorize("@riskEvaluator.isLowRisk(#request)")
public void processRequest(Request request) { /*...*/ }

常用表达式速查表

表达式 说明
hasRole('ROLE') 是否拥有指定角色
hasAnyRole('ROLE1','ROLE2') 是否拥有任一指定角色
hasAuthority('AUTH') 是否拥有指定权限
hasAnyAuthority('AUTH1','AUTH2') 是否拥有任一指定权限
isAuthenticated() 是否已认证
isAnonymous() 是否为匿名用户
isRememberMe() 是否通过RememberMe认证
fullyAuthenticated() 是否为完整认证(非RememberMe)
permitAll() 始终允许
denyAll() 始终拒绝
#paramName 访问方法参数
@beanName.method() 调用Spring Bean的方法

4. 自定义表达式实战

有时内置的SpEL表达式无法满足业务需要,我们可以扩展Spring Security,增加自定义的安全表达式。例如,我们可以定义一个检查用户是否在某一部门的表达式。

4.1 注册自定义权限校验器

回顾一下我们之前第6章节中ABAC属性权限模型,自定义注解authz,通过调用authz注解以实现我们自己所需业务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Component("authz")
@RequiredArgsConstructor
public class AuthorizationLogic {

private final SpelExpressionParser parser = new SpelExpressionParser();
private final SysPolicyMapper sysPolicyMapper;

public boolean check(MethodSecurityExpressionOperations operations, String permission) {
SysUser userDetails = (SysUser) operations.getAuthentication().getPrincipal();

// 加载策略集
LambdaQueryWrapper<SysPolicy> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysPolicy::getTargetResource, permission);
List<SysPolicy> policies = sysPolicyMapper.selectList(queryWrapper);
if (policies.isEmpty()) {
return false;
}

// 构建评估上下文
EvaluationContext context = new StandardEvaluationContext();
// 将用户传入表达式上下文 如:#user.attrs['department'] == 'IT'
// 其中user前缀就是我们传入的user
context.setVariable("user", userDetails);
return policies.stream().allMatch(policy ->
parser.parseExpression(policy.getConditionExpression()).getValue(context, Boolean.class)
);
}
}

调用示例:

1
2
3
4
5
@PreAuthorize("hasRole('ADMIN') and @authz.check(authentication, 'admin:menu')")
@GetMapping("/admin/test")
public ResponseEntity<?> test() {
return ResponseEntity.ok("RBAC角色 + ABAC属性的混合校验");
}

代码解释:
通过调用@authz传递当前用户认证信息 + permission = “admin:menu”,查询出对应表达式(见文章开头的数据库截图),判断用户是否满足表达式条件。

4.2 自定义MethodSecurityExpressionHandler

在之前最新Spring Security实战教程(六)基于数据库的ABAC属性权限模型实战开发的章节中已经演示过,这里不再赘述,感兴趣的小伙伴可以回顾之前的章节代码:

1
2
3
4
5
6
// 调用示例
@PreAuthorize("hasRole('ADMIN') and @abacDecisionEngine.check(authentication, 'admin:menu')")
@GetMapping("/admin/test")
public ResponseEntity<?> test() {
return ResponseEntity.ok("RBAC角色 + ABAC属性的混合校验");
}

5. 高阶表达式技巧

下面演示几个高阶表达式技巧,小伙伴可以根据自己业务需求来调整使用。

5.1 多条件组合表达式

1
2
3
4
5
// 复杂逻辑组合
@PreAuthorize("(hasRole('ADMIN') and @env.isProduction()) or #forceOverride")
public void systemReboot(boolean forceOverride) {
// 系统重启操作
}

5.2 时间维度控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 限制工作时间访问
@PreAuthorize("@timeValidator.isWorkHour()")
public void accessFinancialSystem() {
// 财务系统操作
}

// 定时权限校验
public class TimeValidator {
public boolean isWorkHour() {
LocalTime now = LocalTime.now();
return now.isAfter(LocalTime.of(9, 0))
&& now.isBefore(LocalTime.of(18, 0));
}
}

5.3 安全审计集成

AuditLogger是我们Spring容器中的bean,调用logExportAttempt传递相关参数判断是否有相应权限:

1
2
3
4
5
// 权限校验与审计联动
@PreAuthorize("hasAuthority('DATA_EXPORT') and @auditLogger.logExportAttempt(#request)")
public void exportData(ExportRequest request) {
// 数据导出逻辑
}

5.4 参数过滤与返回结果过滤

Spring Security支持@PreFilter@PostFilter注解,对集合类型的参数或返回值进行过滤。这样可以确保传入的集合中只包含当前用户可以操作的文档,返回的集合中也仅包含公开或本人拥有的文档:

1
2
3
4
5
6
@PreFilter("filterObject.owner == authentication.principal.username")
@PostFilter("filterObject.visibility == 'PUBLIC' or filterObject.owner == authentication.principal.username")
@GetMapping("/documents")
public List<Document> getDocuments(@RequestBody List<Document> docs) {
return docs;
}

5.5 动态资源权限控制

结合Spring Bean实现动态资源权限判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component("resourceChecker")
public class ResourceChecker {

public boolean canAccess(Long resourceId, String operation) {
// 从数据库查询资源权限配置
// 根据资源ID、操作类型、当前用户等动态判断
return true; // 示例返回
}
}

// 使用示例
@PreAuthorize("@resourceChecker.canAccess(#resourceId, 'EDIT')")
public void editResource(Long resourceId) {
// 编辑资源逻辑
}

6. SpEL表达式执行原理简析

理解SpEL的执行原理有助于我们更好地运用它。SpEL表达式的执行主要包含以下几个步骤:

  1. 解析(Parsing):将表达式字符串解析为抽象语法树(AST)。
  2. 编译(Compilation):可选步骤,将表达式编译为字节码以提高性能。
  3. 评估(Evaluation):在给定的上下文中执行表达式,返回结果。

在Spring Security中,MethodSecurityExpressionHandler负责创建和管理表达式评估上下文,并将当前认证信息、方法参数等注入到上下文中,供表达式使用。


7. 性能优化建议

虽然SpEL非常强大,但过度使用或不当使用可能影响系统性能。以下是一些优化建议:

问题 建议
表达式重复解析 使用表达式缓存,避免重复解析相同表达式
复杂对象图导航 避免过深的对象属性访问链
频繁调用的方法 将复杂逻辑移到自定义Bean中,表达式只做简单调用
集合过滤 大数据量集合谨慎使用@PreFilter/@PostFilter

8. 结语

SpELSpring Security中的应用使得权限控制不仅仅局限于静态的角色和权限匹配,而可以根据请求参数、用户属性、业务逻辑等灵活判断访问权限。通过灵活组合内置功能与自定义扩展,开发者可以实现从简单角色校验到复杂业务规则的全场景覆盖。

希望这个章节的内容能够帮助小伙伴们更深入地理解Spring Security权限表达式的底层原理与应用技巧,在实际项目中设计出更灵活高效的安全策略。如果你在实践过程中有任何疑问或更好的扩展思路,欢迎在评论区留言,并三连给博主一点点鼓励!谢谢~