学完这篇,你能在 Spring Boot 项目里把 SQL 注入、XSS、CSRF 三类最常见的 Web 漏洞堵住,知道每类漏洞该在哪一层拦、用哪个 API 拦、以及哪些写法看似安全其实无效。
第一步:先认清漏洞长什么样
三个典型错误写法,先对照自己的代码看有没有:
// SQL 注入:字符串拼 SQL
String sql = "SELECT * FROM users WHERE name='" + name + "'";
// XSS:原样输出用户内容
model.addAttribute("content", comment.getText()); // 模板里用 th:utext 渲染
// CSRF:只有 Cookie 鉴权,没有任何令牌校验
注意:很多项目"测试过没被黑"就以为安全,实际是没人打。注入和 XSS 一旦被自动化扫描器扫到,基本当天就会被利用。
第二步:SQL 注入——只用预编译,禁用字符串拼接
JDBC 一律走 PreparedStatement,参数用占位符:
String sql = "SELECT id, name FROM users WHERE name = ?";
try (Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, name);
try (ResultSet rs = ps.executeQuery()) { /* ... */ }
}
MyBatis 里 #{} 会预编译,${} 是直接拼接。默认全部用 #{};模糊查询写 CONCAT('%', #{kw}, '%'),不要写 '%${kw}%'。排序字段、表名这类不能传参的位置,用白名单判断:
private static final Set<String> SORTABLE = Set.of("id", "created_at", "score");
if (!SORTABLE.contains(sort)) sort = "id";
JPA/Hibernate 用命名参数 :name,动态条件用 Criteria API,不要拼 JPQL 字符串。
注意:${} 在 XML 里不只出现在 WHERE,ORDER BY ${col}、LIMIT ${n}、LIKE '%${kw}%' 都是常见漏点,全局搜一遍 ${ 逐个确认。
第三步:XSS——在输出环节编码,不在输入环节过滤
XSS 的本质是"数据被当成代码执行",所以要在输出到具体上下文时编码:
- Thymeleaf:
th:text 默认转义,安全;th:utext 不转义,危险。
- JSP:用
<c:out value="${x}"/> 或 ${fn:escapeXml(x)}。
- 手写拼接:引入 OWASP Java Encoder。
<dependency>
<groupId>org.owasp.encoder</groupId>
<artifactId>encoder</artifactId>
<version>1.2.3</version>
</dependency>
Encode.forHtml(s); // HTML 正文
Encode.forHtmlAttribute(s); // 属性值
Encode.forJavaScript(s); // 塞进 JS 变量
Encode.forUriComponent(s); // URL 参数
富文本不能简单转义,用 Jsoup 白名单清洗:
Safelist sl = Safelist.basicWithImages();
sl.addTags("p", "h2", "pre", "code");
sl.addProtocols("a", "href", "http", "https");
String clean = Jsoup.clean(raw, sl);
再补一条 CSP 响应头兜底:Content-Security-Policy: default-src 'self'; script-src 'self',能让漏网的 inline script 也执行不了。
注意:Safelist.relaxed() 允许的标签和属性偏多,能用 basicWithImages() 就别用 relaxed;另外别在"入库前"做 HTML 转义,会导致内容重复转义、导出乱码。
第四步:CSRF——令牌 + SameSite 双保险
Spring Security 默认已开启 CSRF,关键是别随手关掉。Thymeleaf 表单用 th:action 时会自动注入隐藏域 _csrf,你什么都不用写。前后端分离的 AJAX 请求,把 token 放到请求头:
http.csrf(c -> c.csrfTokenRepository(
CookieCsrfTokenRepository.withHttpOnlyFalse()));
前端从 Cookie 读 XSRF-TOKEN,请求时带上 X-XSRF-TOKEN 头。同时给会话 Cookie 加 SameSite=Lax(Spring Boot 配置 server.servlet.session.cookie.same-site=lax),对跨站 POST 形成第二道拦截。
注意:Spring Security 6 用 CookieCsrfTokenRepository 配 SPA 时,需要设置 SpaCsrfTokenRequestHandler,否则首次请求和 token 延迟加载会对不上,表现为 403 且难以排查。
第五步:验证是否真的生效
改完代码别只看逻辑,用固定 payload 回归:
- SQL:
' OR '1'='1、1' AND SLEEP(5)--
- XSS:
<script>alert(1)</script>、"><img src=x onerror=alert(1)>
- CSRF:用 curl 模拟跨站 POST,不带 token,看是否返回 403
把这三组放进接口测试里,每次改动跑一遍。
注意:验证时看的是"响应里有没有原样出现 payload",而不是页面弹没弹窗——存储型 XSS 可能在列表页、后台管理页才触发,要逐个展示位置检查。
小结
- SQL 注入只靠预编译解决:JDBC 用
?,MyBatis 用 #{},动态列名走白名单。
- XSS 在输出上下文编码:Thymeleaf 用
th:text,手拼用 OWASP Encoder,富文本用 Jsoup 白名单,再加 CSP 兜底。
- CSRF 靠 Spring Security 的 token 机制 + SameSite Cookie,不要图省事
csrf().disable()。
- 三者的共同点是"收口在框架层",靠人肉 review 逐个接口堵必然漏。
- 最后一定要用固定 payload 做回归测试,把安全当成可验证的功能而不是感觉。