Web 应用始终面对来自网络的各种攻击,对于 Java 开发者而言,SQL 注入、跨站脚本(XSS)和跨站请求伪造(CSRF)是最常见也最危险的三类漏洞。Spring Security 提供了系统性的防护能力,但前提是开发者必须理解这些攻击的原理,并正确使用防护工具。
23.1.1 SQL 注入防护
SQL 注入是指攻击者通过输入恶意构造的 SQL 片段,干扰应用原本的查询逻辑,从而窃取数据、篡改信息甚至控制数据库服务器。
1. 典型攻击场景
假设登录验证的代码这样写:
String sql = "SELECT * FROM users WHERE username = '"
+ username + "' AND password = '" + password + "'";
用户在登录表单中输入 admin' OR '1'='1 作为用户名,密码任意,最终执行的 SQL 变成:
SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = 'xxx'
'1'='1' 恒为真,攻击者无需知道密码即可绕过认证。
2. 防护方案:参数化查询
最根本的防护是 绝不允许用户输入与 SQL 语句进行字符串拼接,而要使用参数化查询(PreparedStatement)。在 Spring 生态中,不同的持久层框架都提供了对应的安全 API:
使用 JdbcTemplate
jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE username = ? AND password = ?",
new Object[]{username, password},
new UserRowMapper()
);
JdbcTemplate 内部使用 PreparedStatement,? 占位符传值,自动进行转义。
使用 JPA / Hibernate
@Query("SELECT u FROM User u WHERE u.username = :username")
User findByUsername(@Param("username") String username);
命名参数同样安全。切勿使用 JPQL 或 HQL 拼接字符串。
使用 MyBatis
<select id="findByUsername" resultType="User">
SELECT * FROM users WHERE username = #{username}
</select>
#{} 使用预编译,安全;${} 直接拼接,除非绝对必要(如动态表名),否则禁用。
3. 动态排序、表名等特殊场景
当确实需要动态传入排序字段或表名时,参数化查询无法直接支持,此时必须进行 白名单校验:
private static final Set<String> ALLOWED_COLUMNS = Set.of("id", "username", "email", "create_time");
private static final Set<String> ALLOWED_DIRECTIONS = Set.of("ASC", "DESC");
public List<User> getUsersByOrder(String orderBy, String sortDirection) {
if (orderBy == null || !ALLOWED_COLUMNS.contains(orderBy)) {
throw new IllegalArgumentException("非法字段: " + orderBy);
}
if (sortDirection == null || !ALLOWED_DIRECTIONS.contains(sortDirection.toUpperCase())) {
throw new IllegalArgumentException("非法排序: " + sortDirection);
}
// 此时可安全拼接,因为值只能是白名单内的固定值
return mapper.getUsersByOrder(orderBy, sortDirection.toUpperCase());
}
4. 存储过程与 ORM 框架的误区
不要以为使用了 ORM 框架就能 100% 免疫 SQL 注入。凡是存在拼接原生 SQL 或 JPQL 的地方,都等同于裸露在注入风险下。例如 JPA 中这样写:
String username = request.getParameter("username");
Query query = entityManager.createQuery(
"SELECT u FROM User u WHERE u.username = '" + username + "'"
);
这依然是字符串拼接,同样会被注入。坚持使用参数标记或 Criteria API 才能确保安全。
5. 额外防御层
- 最小权限原则:应用连接数据库的账号只授予必要的 SELECT、INSERT、UPDATE 权限,收回 DROP、ALTER 等高风险操作权限。
- 数据库防火墙:生产环境部署 SQL 防火墙(如阿里云 DMS),可拦截异常查询模式。
- 输入长度与格式校验:在业务层对参数进行正则校验,进一步缩小攻击面。
23.1.2 跨站脚本(XSS)防护
XSS 攻击通过在页面中注入恶意脚本,窃取用户 cookie、重定向到钓鱼网站、篡改页面内容。分为反射型、存储型和 DOM 型三类,共同点都是 未对用户输入进行适当转义。
1. 核心防护:输出编码
XSS 的根源是浏览器将用户输入当作 HTML 或 JavaScript 解析。防御的关键在于对所有输出到页面的数据进行上下文敏感的编码:
- 在 HTML 标签体或属性中输出时,进行 HTML 实体编码,将
<转为<,>转为>等。 - 在 JavaScript 代码块中输出变量时,进行 JavaScript 编码。
- 在 URL 参数中输出时,进行 URL 编码。
2. Spring 生态中的自动防护
Thymeleaf 模板引擎
默认对 ${} 表达式输出进行 HTML 转义,除非使用 th:utext 强制输出原始 HTML。因此,只需要:
- 始终使用
th:text替换th:utext,除非内容确定安全。 - 避免在脚本块中内联后端变量,若必须,使用 Thymeleaf 的 JavaScript 内联语法
[[${value}]],它也会进行适当的 JavaScript 转义。
Spring MVC 的 @ResponseBody + Jackson
返回 JSON 数据时,Jackson 默认不进行 HTML 编码,因为 JSON 是数据格式,由前端负责安全渲染。后端需确保 HTTP 响应设置 Content-Type: application/json,防止浏览器将 JSON 错误解析为 HTML 引发 MIME 类型混淆攻击。同时配合 X-Content-Type-Options: nosniff 响应头。
3. 额外的防护手段
- 内容安全策略(CSP):通过 HTTP 响应头
Content-Security-Policy限制脚本来源、禁止内联脚本,是防御 XSS 的最强防线。Spring Security 可配置 CSP 头:
http.headers()
.contentSecurityPolicy("script-src 'self'");
- HttpOnly Cookie:将用于身份认证的 cookie 标记为
HttpOnly,阻止 JavaScript 通过document.cookie读取,即使发生 XSS 也能保护会话令牌。Spring Security 默认即启用。 - 输入验证:对用户提交的内容进行校验和清理。如使用
@Pattern正则约束昵称格式;对于富文本,推荐使用 OWASP AntiSamy 或 HTML Sanitizer 来净化白名单标签。 X-XSS-Protection头:较旧的安全机制,设为0关闭浏览器自带的有缺陷的 XSS 过滤器,完全依赖 CSP。
4. 真实案例:富文本的净化处理
对于文章、评论等需要保留部分 HTML 标签的场景,不能粗暴转义。需要使用净化库:
// 引入依赖 org.owasp.html:html-sanitizer
PolicyFactory policy = Sanitizers.FORMATTING
.and(Sanitizers.LINKS)
.and(Sanitizers.IMAGES);
String safeHtml = policy.sanitize(userInput);
只保留允许的标签和属性,移除所有事件处理器(如 onclick)和 javascript: 伪协议。
23.1.3 跨站请求伪造(CSRF)防护
CSRF 攻击诱骗已登录用户执行非本意的操作。例如,攻击者在自己的网站中放置一个隐藏的表单:
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="toAccount" value="attacker" />
<input type="hidden" name="amount" value="10000" />
</form>
<script>document.forms[0].submit();</script>
如果用户刚登出银行系统但会话仍然有效,则此请求会带着 cookie 自动认证,转账执行。
1. 防御原理:同步令牌模式
核心思路是在表单中嵌入一个服务端生成的、不可预测的随机令牌(CSRF Token),提交时带回去,服务端校验该 token 是否与用户会话中存储的匹配。攻击者无法获取该 token,因此无法构造有效请求。
2. Spring Security 的 CSRF 防护
Spring Security 从 4.x 起默认开启 CSRF 保护,应用于所有改变状态的 HTTP 方法(POST、PUT、DELETE、PATCH)。配置项:
http.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
默认情况下,CSRF Token 存储在服务器端 Session 中,并暴露为一个请求属性 _csrf。对于 Thymeleaf 模板,所有表单自动包含隐藏域:
<form method="post" th:action="@{/transfer}">
<!-- Spring 自动插入:<input type="hidden" name="_csrf" value="..."> -->
<input name="toAccount" />
<button type="submit">转账</button>
</form>
对于 AJAX 请求,需要在请求头中携带 token:
var token = document.querySelector('meta[name="_csrf"]').content;
var header = document.querySelector('meta[name="_csrf_header"]').content;
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
[header]: token
},
body: JSON.stringify({...})
});
3. RESTful API 的 CSRF 策略
如果应用是纯粹的无状态 API,不依赖 cookie 进行会话认证(如使用 JWT Bearer Token),CSRF 攻击的重点——自动携带 cookie——就不存在,此时可以关闭 CSRF 保护:
http.csrf().disable();
但若 API 中仍使用 cookie 或 Session 认证,则必须保留 CSRF 防护。另一种方案是使用 SameSite=Strict 的 cookie,现代浏览器支持良好,可从源头阻止跨站点请求携带 cookie。
4. 常见漏网之鱼
- GET 请求改变状态:CSRF 防护只覆盖非 GET 方法。如果你的应用用
GET /deleteUser?id=123执行删除,则完全暴露。务必遵守 HTTP 方法语义:GET 只读,状态变更使用 POST/PUT/DELETE。 - 非表单提交:手动编写的 AJAX 忘记添加 CSRF header,导致请求被拦截。测试时要开启 403 错误监控。
- 跨域配置不当:CORS 宽松设置(
Access-Control-Allow-Origin: *)叠加 cookie 认证时,可能让第三方站点成功发起跨域请求并带上凭证。应严格限定Access-Control-Allow-Origin,且设置Access-Control-Allow-Credentials: true时才返回具体域名,不能用星号。
23.1.4 综合防护实践
真实项目中,单点防御往往不够,需要多层防护组合:
- 参数化查询 + 白名单 杜绝 SQL 注入。
- 模板引擎自动转义 + CSP + HttpOnly 构成 XSS 防御纵深。
- CSRF Token + SameSite Cookie + 正确的 HTTP 方法 封堵跨站请求伪造。
- 对所有输入进行严格校验(长度、格式、类型),输出时编码。
- 定期使用 OWASP ZAP 或 Burp Suite 扫描应用,发现潜在遗漏。
- 关注 Spring Security 的安全公告,及时升级版本。
记住:安全没有银弹,只有贯穿需求、编码、测试、运维全流程的安全意识,才能构建真正可信的 Web 应用。