🚀 SpEL 安全加固宝典:变量注入防护全场景实战解析
摘要:
- SpEL(Spring Expression Language)灵活强大,却易遭“注入”风险;
- 把用户输入当作“变量”传入,断绝表达式本体被篡改;
- 四大场景分析:硬编码、YAML 配置、数据库、注解;
- 深入解析:表达式从哪儿来、变量如何生效、为何安全;
- 拓展防护:
SimpleEvaluationContext能否一同保驾护航?
🧐 一、SpEL 注入风险简述
- SpEL 是“表达式语言”,支持运行时动态计算,如: parser.parseExpression(“T(java.lang.Runtime).getRuntime().exec(‘calc’)”).getValue();
- 如果将未经过滤的用户输入当作表达式本体,攻击者可执行任意 Java 代码,泄露数据、远程命令。
🔐 二、变量注入防护核心原理
- 表达式模板 与 变量值 分离:
- 模板(
String condition = "#user.level >= 5")固定、可控; - 变量(
context.setVariable("user", userInput))仅为“值”,不再被解析为表达式。
- 模板(
- 安全性:即使
userInput = "T(java.lang.Runtime)...“,也会被当作普通字符串,不会执行。 - 适用场景:权限控制、规则引擎、动态过滤、注解式安全等。
🛠️ 三、四大场景汇总
| 场景 | 表达式来源 | 说明 |
|---|---|---|
| 1. 硬编码 | 直接写在 Java 代码里 | 代码中可以明显看的硬编码的 表达式模板内容。 |
| 2. YAML 配置 | application.yml | 通过 @ConfigurationProperties+Controller 实时传参。 |
| 3. 数据库 | 表中字段 expression | Controller 从 DB 读出规则,再 注入网络请求出入的变量参数。 |
| 4. 注解 | @PreAuthorize | 注解式声明表达式模板。 |
3.1 硬编码示例
@RestController
public class HardcodedSpELController {
@PostMapping("/eval-hardcoded")
public Boolean eval(@RequestBody User user) {
// 1. 解析器与上下文
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext ctx = new StandardEvaluationContext();
// 2. 获取网络请求的变量User
ctx.setVariable("user", user);
// 3. 硬编码的表达式模板
String condition = "#user.level >= 5";
// 4. 执行并返回结果
return parser.parseExpression(condition)
.getValue(ctx, Boolean.class);
}
public static class User {
private String role;
private int level;
public User() {}
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
public int getLevel() { return level; }
public void setLevel(int level) { this.level = level; }
}
}
3.2 YAML 配置示例
# application.yml
access:
rule: "#user.role == 'admin'"
@Component
@ConfigurationProperties("access")
class AccessRuleConfig {
private String rule;
public String getRule() { return rule; }
public void setRule(String rule) { this.rule = rule; }
}
@RestController
public class YamlSpELController {
@Autowired
private AccessRuleConfig config;
@PostMapping("/eval-yaml")
public Boolean eval(@RequestBody User user) {
// 1. 注入变量
StandardEvaluationContext ctx = new StandardEvaluationContext();
ctx.setVariable("user", user);
// 2. 解析 YAML 中的表达式模板
String condition = config.getRule();
return new SpelExpressionParser()
.parseExpression(condition)
.getValue(ctx, Boolean.class);
}
public static class User {
private String role;
private int level;
public User() {}
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
public int getLevel() { return level; }
public void setLevel(int level) { this.level = level; }
}
}
3.3 数据库读取示例
INSERT INTO access_rule(resource,expression) VALUES('/vip','#user.level>=10');
@RestController
public class DbSpELController {
@Autowired
private AccessRuleDao dao;
@PostMapping("/eval-db")
public Boolean eval(@RequestParam String resource,
@RequestBody User user) {
// 1. 从数据库读取表达式模板
String expr = dao.getRuleByResource(resource).getExpression();
// 2. 注入网络请求的 user
StandardEvaluationContext ctx = new StandardEvaluationContext();
ctx.setVariable("user", user);
// 3. 执行并返回
return new SpelExpressionParser()
.parseExpression(expr)
.getValue(ctx, Boolean.class);
}
}
3.4 注解式示例
@EnableGlobalMethodSecurity(prePostEnabled=true)
@Configuration
class SecurityConfig { /* ... */ }
@RestController
public class AnnotatedSpELController {
@GetMapping("/admin-area")
@PreAuthorize("#user.role == 'admin'")
public String admin(@RequestBody User user) {
// 如果通过,才会执行此方法体
return "欢迎管理员:" + user.getRole();
}
public static class User {
private String role;
private int level;
public User() {}
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
public int getLevel() { return level; }
public void setLevel(int level) { this.level = level; }
}
}
Spring Security 会在调用前,把网络请求中的
user对象注入到表达式上下文。
🤔 四、核心疑问全解析
- 表达式从哪来?
- 就是字符串模板,源自硬编码、配置文件、数据库、注解等。
- 变量值有啥用?
- 为“模板”提供运行时数据;不参与模板构造,彻底切断注入通路。
- 为何能防注入?
- 用户输入 只做值,不允许出现在表达式的语法层面。
- 何时不用 SpEL?
- 逻辑固定、无需动态配置时,可直接用 Java 代码。
⚙️ 五、SimpleEvaluationContext 防护
5.1 什么是 SimpleEvaluationContext
- 简化版 EvaluationContext,只支持读写属性,禁用方法调用、类型引用等高级功能。
- 适合“只读数据绑定”场景,进一步缩小攻击面。
5.2 示例代码
@RestController
public class SpELController {
@PostMapping("/evaluate")
public Boolean evaluateAccess(@RequestBody User user) {
// 1. 构建只读上下文,禁止方法调用 & 类型访问
SimpleEvaluationContext ctx = SimpleEvaluationContext
.forReadOnlyDataBinding()
.build();
// 2. 真实网络请求反序列化得到的 user
ctx.setVariable("user", user);
// 3. 表达式模板
String condition = "#user.level >= 5";
// 4. 解析并执行
return new SpelExpressionParser()
.parseExpression(condition)
.getValue(ctx, Boolean.class);
}
public static class User {
private String role;
private int level;
public User() {}
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
public int getLevel() { return level; }
public void setLevel(int level) { this.level = level; }
}
}
5.3 防护效果对比
| 功能 | StandardEvaluationContext | SimpleEvaluationContext |
|---|---|---|
| 属性读取 | ✅ | ✅ |
| 方法调用 | ✅ | ❌ |
| 类型引用 | ✅ | ❌ |
| 自定义函数 | ✅ | ❌ |
| 注入面 | 较小(依赖变量) | 极小(连本地方法都禁了) |
结论:
- 变量防护 是第一道屏障;
- SimpleEvaluationContext 能当第二道盾牌,彻底锁死方法调用与类型访问,适合最严格的只绑定场景。
🎉 六、全文小结
- SpEL 强大又危险,只要表达式与变量分离,即可杜绝注入;
- 四大场景(硬编码/YAML/数据库/注解)覆盖所有常见用法;
- 核心套路:模板固定、变量带值、安全解析;
- 防护升级:
SimpleEvaluationContext禁止方法 & 类型,更严苛。
✨ 提示:
- 项目常见表达式来源(配置、数据库);
- 核心关注:用户输入是否都作为变量传入,不参与表达式拼接;
-
parseExpression(condition) //内容是否可控
-
- 若为 Web 场景或需极致安全,推荐使用
SimpleEvaluationContext。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END











暂无评论内容