用 + SpEL 实现优雅的细粒度权限控制
在企业级应用开发中,权限控制是绕不开的核心需求。从简单的“管理员/普通用户”角色区分,到复杂的“部门+岗位+资源+操作”多维权限模型,传统硬编码方式(如大量 if-else 或 switch)不仅难以维护,还极易引入安全漏洞。
而 Spring Boot 结合 SpEL(Spring Expression Language) ,提供了一种声明式、可读性强、高度灵活的权限控制方案——只需一行注解,就能实现动态、细粒度的访问控制,真正做到了“代码即策略”。
本文将带你深入实战,看如何用 @PreAuthorize + SpEL 轻松搞定复杂权限场景。
一、为什么 SpEL 是权限控制的“天选之子”?
SpEL 是 Spring 框架内嵌的表达式语言,支持:
- 访问方法参数(
#userId) - 调用 Bean 方法(
@authService.canEdit(#docId)) - 逻辑运算(
and/or/!) - 集合操作(
hasRole('ADMIN')) - 安全上下文(
authentication.principal)
这些能力,恰好覆盖了权限判断所需的全部要素:谁(Who) 、对什么(What) 、能不能做(Can/Cannot) 。
二、基础用法:基于角色的简单控制
@RestController
public class UserController {
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/users")
public List<User> getAllUsers() {
return userService.list();
}
}2
3
4
5
6
7
8
9
开启方法级安全只需在启动类加 @EnableMethodSecurity(Spring Security 6+):
@SpringBootApplication
@EnableMethodSecurity
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}2
3
4
5
6
7
✅ 无需写一行权限判断代码,框架自动拦截非法请求。
三、进阶实战:动态数据权限(行级控制)
场景:用户只能编辑自己创建的文档。
@PreAuthorize("@documentService.isOwner(authentication.name, #docId)")
@PutMapping("/documents/{docId}")
public void updateDocument(@PathVariable Long docId, @RequestBody DocUpdateDTO dto) {
documentService.update(docId, dto);
}2
3
4
5
其中 documentService.isOwner() 是一个自定义业务方法:
@Service
public class DocumentService {
public boolean isOwner(String username, Long docId) {
Document doc = findById(docId);
return doc != null && doc.getCreator().equals(username);
}
}2
3
4
5
6
7
✅ 权限逻辑与业务解耦,可单元测试,可复用。
四、复杂组合权限:多条件 + 自定义函数
场景:HR 可查看本部门员工,管理员可查看所有,且敏感字段需额外权限。
@PreAuthorize(
"(hasRole('HR') and @deptService.inSameDept(authentication.name, #empId)) " +
"or hasRole('ADMIN')"
)
@GetMapping("/employees/{empId}")
public Employee getEmployee(@PathVariable Long empId) {
return employeeService.getById(empId);
}2
3
4
5
6
7
8
甚至可以封装自定义 SpEL 函数:
@Configuration
public class SecurityConfig {
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(new CustomPermissionEvaluator());
return handler;
}
}2
3
4
5
6
7
8
9
10
然后在 SpEL 中使用:
@PreAuthorize("hasPermission(#docId, 'document', 'edit')")五、性能与安全提醒
- 避免在 SpEL 中执行耗时操作(如远程调用),建议缓存或预加载;
- 不要直接拼接用户输入到 SpEL 表达式,防止表达式注入(Spring 已对 #param 做安全处理,但自定义解析需谨慎);
- 统一权限入口,避免部分接口用注解、部分用代码,造成策略碎片化。
结语:优雅,是一种生产力
借助 Spring Boot + SpEL,我们把原本散落在各处的权限判断,收归为清晰、可审计、可配置的声明式规则。这不仅提升了代码质量,更强化了系统安全性。
当你的同事还在用 if (user.getRole() != ADMIN) throw ... 时,你已经用一行 @PreAuthorize("...") 完成了更强大、更灵活的控制。
真正的工程优雅,不是炫技,而是让复杂问题看起来简单。
🌟 小贴士:结合 Spring Security 的 @PostAuthorize、@PostFilter,还能实现返回结果过滤(如隐藏敏感字段),进一步提升数据安全水位。
参数校验这活儿,写 后端的同学肯定没少干。@NotNull、@Size、@NotBlank 一把梭,搞定大部分场景没啥问题。
但总有一些场景,这些基本注解是真搞不定的。
比如你想根据某个字段的值来决定另一个字段要不要校验,或者校验一个枚举值是不是合法的,再或者校验的时候需要查一下数据库……
遇到这种情况怎么办?要么在 Service 层写一堆 if/else,要么自定义 ConstraintValidator,代码散落在各处,改起来还容易漏。
所以今天来给大家安利一个一直在维护的开源参数校验组件——SpEL Validator,基于 Spring 表达式,把校验规则直接写在注解里,而且最近还推出了配套的 IDEA 插件,开发体验直接拉满。
SpEL Validator 是什么?
简单来说,SpEL Validator 是一个基于 Spring Expression Language 的参数校验框架,它不是要替代你熟悉的那些 @NotNull、@NotBlank,而是在 Jakarta Validation 的基础上做增强,把原来搞不定的那些场景给补上。
用过 @NotNull 的同学,上手 SpEL Validator 几乎零成本。直接看几个例子感受下:
条件式校验:根据 switchAudio 的值决定是否校验 audioContent,只有当 condition 的表达式返回true时才会开启校验
@NotNull
private Boolean switchAudio;
@SpelNotNull(condition = "#this.switchAudio == true", message = "语音内容不能为空")
private Object audioContent;2
3
4
5
枚举值校验:调用静态方法判断枚举值是否存在,这里是个断言条件,当 assertTrue 的表达式返回false时校验不通过
@SpelAssert(assertTrue = " T(cn.sticki.enums.ExampleEnum).getByCode(#this.testEnum) != null ",
message = "枚举值不合法")
private Integer testEnum;2
3
调用 Spring Bean:直接在表达式中调用已注入的 Bean
@SpelAssert(assertTrue = "@exampleService.getUser(#this.userId) != null", message = "用户不存在")
private Integer userId;2
怎么样,够直观吧?不用再到处翻 Validator 了,打开类就能看到所有的校验规则。
不过,你可能也注意到了——这些 SpEL 表达式是写在字符串里的,这意味着它们没有语法高亮、没有字段补全、字段名拼错了也没人提醒你,得等到运行时才能发现。
但是,注意我要说但是了。
一个配套的 IDEA 插件:SpEL Validator Support 。装上之后,编写 SpEL 表达式也会有语法高亮、智能补全、Ctrl+Click 跳转、重命名同步、错误实时提醒,全都有。
安装方式:打开 IDEA → Settings → Plugins → 搜索 "SpEL Validator Support" → Install
下面我用 spel-validator-example 项目来演示框架 + 插件的完整开发体验。
实战演示
准备工作
引入依赖,Spring Boot 3.x 用 jakarta 版:
<dependency>
<groupId>cn.sticki</groupId>
<artifactId>spel-validator-jakarta</artifactId>
<version>Latest Version</version>
</dependency>2
3
4
5
Spring Boot 2.x 的同学把 jakarta 换成 javax 就行。
然后去 IDEA 的 Plugins 里搜索安装 SpEL Validator Support 插件,重启 IDEA。
案例一:条件式校验 + 智能补全
这是示例项目中最基础的一个场景——SimpleExampleParamVo:
@Data
@SpelValid
public class SimpleExampleParamVo {
@NotNull
private Boolean switchAudio;
@SpelNotNull(condition = "#this.switchAudio == true", message = "语音内容不能为空")
private Object audioContent;
@SpelAssert(assertTrue = " T(cn.sticki.validator.spel.example.enums.ExampleEnum).getByCode(#this.testEnum) != null ",
message = "枚举值不合法")
private Integer testEnum;
@SpelAssert(assertTrue = "@exampleService.getUser(#this.userId) != null", message = "用户不存在")
private Integer userId;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这个类里就包含了三种之前不好处理的校验场景:switchAudio 为 true 时才校验 audioContent;通过静态方法校验枚举值是否合法;调用 Spring Bean 查一下用户是否存在。全都写在注解里,没有一行 if/else。
装了插件之后,你会发现 SpEL 表达式有了语法高亮,字段名、方法调用、类型引用都有颜色区分。输入 #this. 的时候,IDEA 会自动弹出当前类的所有字段。
发起请求看一下校验效果:
// switchAudio=true 但 audioContent 为空 → 校验不通过
{"switchAudio": true, "audioContent": null}
// 响应:{"code": 400, "message": "audioContent 语音内容不能为空"}
// switchAudio=false → audioContent 不校验,通过
{"switchAudio": false, "audioContent": null}
// 响应:{"code": 200, "message": "成功"}2
3
4
5
6
7
案例二:分组校验 + 引用导航
再看另一个更有意思的例子,根据 type 分组校验不同的字段。
在某些内容比较多的表单场景下,可能会根据用户选择的不同类型,然后展示不同的字段,这时候就需要根据类型来分组校验不同的字段。
@Data
@SpelValid(spelGroups = "#this.type")
public class GroupExampleParamVo {
@NotNull
@Pattern(regexp = "^text|audio$")
private String type;
@SpelNotNull(group = Group.TEXT)
private Object textContent;
@SpelNotNull(group = Group.AUDIO)
private Object audioContent;
@SpelNotNull // 未指定分组时,默认被校验
private Integer other;
static class Group {
private static final String TEXT = "'text'";
private static final String AUDIO = "'audio'";
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
当 type = "text" 时,只有 textContent 和 other 会被校验;当 type = "audio" 时,只有 audioContent 和 other 会被校验。分组逻辑写在注解里就完事了,齐活。
有了插件,你可以 Ctrl+Click 表达式中的字段名,直接跳转到字段定义。也可以在字段上右键 Find Usages,查看它在哪些 SpEL 表达式中被引用了。
案例三:调用 Spring Bean
有时候校验逻辑不是简单的判空或比大小,可能需要查一下数据库才能确定参数合不合法。这时候就需要在表达式里调用 Spring Bean 了。默认情况下,SpEL Validator 不支持调用 Spring Bean,需要先在启动类上加上 @EnableSpelValidatorBeanRegistrar 注解开启 Bean 支持:
@EnableSpelValidatorBeanRegistrar
@SpringBootApplication
public class RestApplication {
public static void main(String[] args) {
SpringApplication.run(RestApplication.class, args);
}
}2
3
4
5
6
7
然后在你的 Service 里写个查询方法(这里简单模拟一下):
@Service
public class ExampleService {
public User getUser(int id) {
User user = new User();
user.setId(id);
user.setName("阿杆");
return user;
}
}2
3
4
5
6
7
8
9
10
现在就可以在校验注解中直接引用这个 Bean 了:
@SpelAssert(assertTrue = "@exampleService.getUser(#this.userId) != null", message = "用户不存在")
private Integer userId;2
@exampleService 就是 Spring 容器里那个 Bean 的名字,SpEL 原生语法。
这里只是用简单例子演示一下调用方式。实际开发中你可以调用任何已注入的 Bean 方法来做校验,查缓存查数据库都行。当然了,校验里别搞太重的操作,查个缓存差不多得了,别搞成查了三张表还调了两个远程接口😅。
有了插件的加持,@exampleService 同样能被识别和高亮。如果你不小心写错了 Bean 名或字段名,插件也会给你标红提醒。
更多能力速览
除了上面演示的场景,SpEL Validator 还提供了丰富的约束注解:
| 注解 | 说明 | 对标 Jakarta |
|---|---|---|
@SpelAssert | 逻辑断言 | — |
@SpelNotNull / @SpelNull | 空值校验 | @NotNull / @Null |
@SpelNotEmpty / @SpelNotBlank | 非空校验 | @NotEmpty / @NotBlank |
@SpelSize | 长度校验 | @Size |
@SpelMin / @SpelMax | 数值范围 | @Min / @Max |
@SpelDigits | 数字精度 | @Digits |
@SpelFuture / @SpelPast 等 | 时间校验 | @Future / @Past |
所有注解都支持 condition(条件开关)和 group(分组),也支持国际化消息,message = "{key}" 即可根据 Accept-Language 自动切换语言。
如果有自定义约束注解的需求,只要加上 @SpelConstraint 元注解,IDEA 插件同样能识别并提供智能支持。
最后
OK,差不多就介绍到这里了。总结起来就是:SpEL Validator 负责把校验能力补齐,IDEA 插件负责让你写表达式的时候不再抓瞎,两个搭一起用体验还是很舒服的。
如果你的项目里经常碰到跨字段校验、条件式校验这类场景,不妨试试看,接入成本很低的。
