从一次 Spring Boot + MySQL 报错,看 Web 攻击面与防御模型,用安全视角复盘攻击面、风险点与防御机制的记录(带SQL漏洞的进阶版)
众嗦粥汁,一个spring boot项目里DemoAplication是启动器,各个Controller是主要的攻击面,config是副攻击面,现在我通过AI写了一个spring boot项目并分析它的攻击点,以下是Usercontroller的代码和整个项目的结构// ✅ 查询用户:/user/get?id=1// ✅ 新增用户:/user/add?name=tom)";// 取最后插入的自增
零、项目代码和攻击面介绍
众嗦粥汁,一个spring boot项目里DemoAplication是启动器,各个Controller是主要的攻击面,config是副攻击面,现在我通过AI写了一个spring boot项目并分析它的攻击点,以下是Usercontroller的代码和整个项目的结构

package com.liangning.demo.controller;
import com.liangning.demo.common.Result;
import com.liangning.demo.entity.User;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/user")
public class UserController {
private final JdbcTemplate jdbcTemplate;
public UserController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// ✅ 查询用户:/user/get?id=1
@GetMapping("/get")
public Result<User> getUser(@RequestParam Long id) {
String sql = "SELECT id, name FROM users WHERE id = ?";
Map<String, Object> row = jdbcTemplate.queryForMap(sql, id);
User u = new User();
u.setId(((Number) row.get("id")).longValue());
u.setName((String) row.get("name"));
return Result.ok(u);
}
// ✅ 新增用户:/user/add?name=tom
@GetMapping("/add")
public Result<Long> addUser(@RequestParam String name) {
String sql = "INSERT INTO users(name) VALUES (?)";
jdbcTemplate.update(sql, name);
// 取最后插入的自增 id(对 MySQL 有效)
Long id = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
return Result.ok(id);
}
@GetMapping("/getUnsafe")
public Object getUserUnsafe(@RequestParam String id) {
String sql = "SELECT id, name FROM users WHERE id = " + id;
System.out.println("SQL=" + sql);
return jdbcTemplate.queryForMap(sql);
}
@GetMapping("/getSafe")
public Object getUserSafe(@RequestParam int id) {
String sql = "SELECT id, name FROM users WHERE id = ?";
System.out.println("SQL(template)=" + sql + ", param=" + id);
return jdbcTemplate.queryForMap(sql, id);
}
}
一、引言(短、聚焦)
SQL 注入是什么?
简要解释攻击原理:
SQL 注入是攻击者通过构造恶意输入,使得用户输入被错误地当成 SQL 语句的一部分执行,从而泄露或非法操作数据库。
例如:id=1 OR 1=1会改变原本预期的条件语义。
说明它仍然是 Web 安全的基础性问题。
二、为什么 Spring Boot 也可能存在 SQL 注入风险
即使框架自动配置方便,仍可能存在漏洞,原因包括:
-
直接字符串拼接 SQL(危险)
-
JDBC
Statement或拼接拼出来的查询 -
错误处理机制泄露信息(堆栈/SQL)
-
动态 SQL / 手写查询未做好参数化
可以引用“Spring Boot 默认方便但不无敌”的观点。
三、真实案例:可复现的 SQL 注入示例
3.1 不安全版代码(实战)
@GetMapping("/getUnsafe")
public Object getUserUnsafe(@RequestParam String id) {
String sql = "SELECT id, name FROM users WHERE id = " + id;
System.out.println("SQL=" + sql);
return jdbcTemplate.queryForMap(sql);
}
攻击输入示例:
GET /user/getUnsafe?id=1 OR 1=1
结果

这说明攻击成立 & 信息泄露严重。
1) status=500
✓ 服务器内部错误
不是“找不到页面”(404),也不是“你没权限”(403),而是后端代码执行过程中抛异常了。
2) Incorrect result size: expected 1, actual 3
这句最关键:
✓ 期望只查到 1 行,但实际查到了 3 行。
它在告诉你:你后端写的查询方式是“我只要一条记录”,但数据库返回了多条。
四、安全修复策略(多层面)
4.1 参数化查询(核心)
正确写法:
@GetMapping("/getSafe")
public Object getUserSafe(@RequestParam int id) {
String sql = "SELECT id, name FROM users WHERE id = ?";
return jdbcTemplate.queryForMap(sql, id);
}
对比行为:
-
输入
1 OR 1=1→ 400 Bad Request / 拒绝执行 -
SQL 不被注入,参数边界立起来
解释参数化机制为什么防注入(参数与 SQL 分离)。
结果为

但是仍然有错误信息泄露的问题
4.2 全局统一异常处理(错误不泄露)
解决方案:

内容为
package com.liangning.demo.exception;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<Map<String, Object>> handleBadRequest(MethodArgumentTypeMismatchException e) {
System.err.println("[400] Bad request: " + e.getMessage());
Map<String, Object> body = new HashMap<>();
body.put("code", 400);
body.put("msg", "Bad Request");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleServerError(Exception e) {
e.printStackTrace();
Map<String, Object> body = new HashMap<>();
body.put("code", 500);
body.put("msg", "Internal Server Error");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
}
}
目的:
-
前端不再看到具体堆栈、类名、SQL
-
后端日志保留详细信息
-
所有异常返回统一结构
这是工程级防护,不只防注入,还把错误信息泄露降为最小。
4.3 输入验证/转换边界(加强安全)
参数化之外,还应该:
-
对输入类型/格式做验证(如
@Valid, Bean Validation) -
对字符串或复杂条件输入设定提前拒绝
这是 “提前在边界碰掉危险输入” 的策略。
五、进一步的防护建议(进阶)
原文可能只提到了参数化,这里增加几点实战建议:
5.1 使用 ORM/框架自动处理
例如 Spring Data JPA 或 MyBatis :
-
MyBatis: 使用
#{param}避免注入
不要用${param}直接拼接。
5.2 做最小权限原则(数据库)
即使注入成功,也限制账户权限范围,降低破坏面。
5.3 输入过滤/编码层
对用户输入进行正则或规范范围验证(长度/字符/类型验证)。
可与 Bean Validation 或 HTTP 过滤器结合。
六、结果对照 + 验收方法(工程级)
| 场景 | 预期 | 实际 |
|---|---|---|
| 不安全 SQL | 直接注入成功 | SQL 改写 |
| 安全 SQL(参数化) | 注入失败 | 类型转换/查询无结果 |
| 异常展示 | 堆栈 + SQL | JSON 错误,不泄露细节 |
-
未修复前白页堆栈
-
修复后统一 JSON / 400/500
-
SQL 模板 vs 攻击改写显式对比


七、总结(工程视角)
-
SQL 注入仍是常见漏洞
-
参数化查询与 ORM 是最核心防御
-
错误信息泄露也是重要攻击面
-
输入验证/最小权限是强化手段
-
真实工程需要全流程闭环验证
更多推荐


所有评论(0)