概述
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>
通过提供 id、name、gender 和 percentage(可选,如表单验证所示),我们可以在这个视图中添加学生。在执行此表单之前,我们需要提供 user 和 password 来在 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 项目 中找到。