人人都会AI编程

23.1 常见 Web 攻击防护:SQL 注入、XSS、CSRF

更新时间:2026-07-10

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 实体编码,将 < 转为 &lt;> 转为 &gt; 等。
  • 在 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 综合防护实践

真实项目中,单点防御往往不够,需要多层防护组合:

  1. 参数化查询 + 白名单 杜绝 SQL 注入。
  2. 模板引擎自动转义 + CSP + HttpOnly 构成 XSS 防御纵深。
  3. CSRF Token + SameSite Cookie + 正确的 HTTP 方法 封堵跨站请求伪造。
  4. 对所有输入进行严格校验(长度、格式、类型),输出时编码。
  5. 定期使用 OWASP ZAP 或 Burp Suite 扫描应用,发现潜在遗漏。
  6. 关注 Spring Security 的安全公告,及时升级版本。

记住:安全没有银弹,只有贯穿需求、编码、测试、运维全流程的安全意识,才能构建真正可信的 Web 应用。