概述

Thymeleaf 是一个用于处理和创建 HTML、XML、JavaScript、CSS 和纯文本的 Java 模板引擎。对于 Thymeleaf 和 Spring 的详细介绍,请参阅 这篇教程

本文将讨论如何在使用 Thymeleaf 应用的 Spring MVC 中防止跨站请求伪造(Cross-Site Request Forgery,简称 CSRF)攻击。我们将特别测试针对 HTTP POST 方法的 CSRF 攻击。

CSRF 是一种迫使已登录的用户执行未授权操作的网络攻击。

Maven 依赖

首先,让我们看看集成 Thymeleaf 和 Spring 所需的配置。需要在项目依赖中添加 thymeleaf-spring 库:

注意,在 Spring 4 项目中,应使用 thymeleaf-spring4 而非 thymeleaf-spring5。最新的依赖库可以在 这里 查找。

此外,为了使用 Spring Security,我们需要添加以下依赖:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>5.7.3</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>5.7.3</version>
</dependency>

Spring Security 相关的最新版本可在 这里这里 获取。

Java 配置

除了 此处 中介绍的 Thymeleaf 配置外,还需要为 Spring Security 添加配置。为此,我们需要创建以下类:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebMVCSecurity {

    @Bean
    public InMemoryUserDetailsManager userDetailsService() {
        UserDetails user = User.withUsername("user1")
            .password("{noop}user1Pass")
            .authorities("ROLE_USER")
            .build();

        return new InMemoryUserDetailsManager(user);
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring()
            .antMatchers("/resources/**");
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .anyRequest()
            .authenticated()
            .and()
            .httpBasic();
        return http.build();
    }
}

有关更详细的 Security 配置信息,请参考 Spring Security 教程

默认情况下,Java 配置已经启用了 CSRF 保护。 若要禁用这个有用的功能,我们需要在 configure(…) 方法中添加如下内容:

.csrf().disable()

在 XML 配置中,我们需要手动指定 CSRF 保护,否则它将不起作用:

<security:http 
  auto-config="true"
  disable-url-rewriting="true" 
  use-expressions="true">
    <security:csrf />
     
    <!-- Remaining configuration ... -->
</security:http>

另外,请注意,如果使用带有 PATCH、POST、PUT 或 DELETE HTTP 动词的登录或登出页面,需要在代码中手动将 CSRF 令牌作为隐藏参数添加到表单中。

示例:登录表单中使用 CSRF 令牌

<form name="login" th:action="@{/login}" method="post"> 
...
<input type="hidden" 
th:name="${_csrf.parameterName}" 
th:value="${_csrf.token}" />
</form>

对于其他表单,如果使用了 th:action,CSRF 令牌会自动添加到包含隐藏输入的表单中:

<input 
  type="hidden" 
  name="_csrf"
  value="32e9ae18-76b9-4330-a8b6-08721283d048" /> 
<!-- Example token -->

视图配置

现在我们来处理包含动作和测试流程的 HTML 文件。在第一个视图中,我们将尝试向列表中添加新学生:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org">
<head>
<title>Add Student</title>
</head>
<body>
    <h1>Add Student</h1>
        <form action="#" th:action="@{/saveStudent}" th:object="${student}"
          method="post">
            <ul>
                <li th:errors="*{id}" />
                <li th:errors="*{name}" />
                <li th:errors="*{gender}" />
                <li th:errors="*{percentage}" />
            </ul>
    <!-- Remaining part of HTML -->
    </form>
</body>
</html>

通过提供 idnamegenderpercentage(可选,如表单验证所示),我们可以在这个视图中添加学生。在执行此表单之前,我们需要提供 userpassword 来在 Web 应用程序中进行身份验证。

4.1. 浏览器 CSRF 攻击测试

接下来是第二个 HTML 视图,其目的是尝试 CSRF 攻击:

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<form action="http://localhost:8080/spring-thymeleaf/saveStudent" method="post">
    <input type="hidden" name="payload" value="CSRF attack!"/>
    <input type="submit" />
</form>
</body>
</html>

我们知道动作 URL 是 http://localhost:8080/spring-thymeleaf/saveStudent。黑客试图访问此页面进行攻击。

进行测试时,打开另一个未登录应用的浏览器,尝试提交表单。我们将收到如下页面:

屏幕截图

由于没有 CSRF 令牌,我们的请求被拒绝。

请注意,HTTP 会话用于存储 CSRF 令牌。当发送请求时,Spring 会比较生成的令牌与会话中存储的令牌,以确认用户未被黑客入侵。

4.2. JUnit CSRF 攻击测试

如果你不想通过浏览器测试 CSRF,还可以通过快速集成测试实现。首先,为测试设置 Spring 配置:

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = { 
  WebApp.class, WebMVCConfig.class, WebMVCSecurity.class, InitSecurity.class })
public class CsrfEnabledIntegrationTest {

    // configuration

}

然后进行实际测试:

@Test
public void addStudentWithoutCSRF() throws Exception {
    mockMvc.perform(post("/saveStudent").contentType(MediaType.APPLICATION_JSON)
      .param("id", "1234567").param("name", "Joe").param("gender", "M")
      .with(testUser())).andExpect(status().isForbidden());
}

@Test
public void addStudentWithCSRF() throws Exception {
    mockMvc.perform(post("/saveStudent").contentType(MediaType.APPLICATION_JSON)
      .param("id", "1234567").param("name", "Joe").param("gender", "M")
      .with(testUser()).with(csrf())).andExpect(status().isOk());
}

第一个测试由于缺少 CSRF 令牌,将返回禁止状态,而第二个测试将正常执行。

总结

本文讨论了如何使用 Spring Security 和 Thymeleaf 框架防止 CSRF 攻击。

完整实现此教程的示例项目可在 GitHub 项目 中找到。