CSRF:跨站请求伪造(Cross-Site Request Forgery)
Spring Security 中的 .csrf() 是用来开启或配置这种保护机制,防止恶意网站“冒充用户”向你的网站发起请求。
一、CSRF 攻击原理简要
CSRF 的典型攻击场景如下:
- 用户登录了网站 A(如你的后台系统),并保持了登录状态(cookie 存在)。
- 然后用户去访问了恶意网站 B。
- 恶意网站 B 背后偷偷发起一个请求,比如 POST /user/delete?id=123 到网站 A。
- 因为用户登录状态存在(带有 cookie),服务器误以为是用户自己的操作,执行了该请求。
结果:用户被“代操作”了。
二、Spring Security 的 CSRF 防护机制
Spring Security 通过 CSRF Token(令牌) 机制 来避免这种攻击:
1️⃣ 当用户访问页面时,Spring 会生成一个隐藏的 CSRF Token(在表单中)。
2️⃣ 用户提交表单时,CSRF Token 会随请求一起发送(通常在请求体或请求头)。
3️⃣ 服务器验证这个 Token 是否正确匹配。
✅ 匹配成功才处理请求,否则拒绝(返回 403 Forbidden)。
三、什么时候需要 / 不需要开启 CSRF
场景 | 是否推荐开启 CSRF |
---|---|
表单提交、页面操作(如 Thymeleaf 页面) | ✅ 推荐开启 |
前后端分离、REST API | ❌ 通常关闭,改用 JWT、OAuth 等鉴权机制 |
小程序 / App 接口 | ❌ 一般关闭,因为不使用 cookie |
四、Spring Security 配置 CSRF
部分请求忽略CSRF校验(如 /api/ 下的所有请求):
.csrf(csrf ->
csrf.ignoringRequestMatchers("/api/**") // 忽略API的CSRF保护
)
如果所有请求都需要进行 CSRF 校验,那么 .csrf(…) 的写法可以更简单,不需要 ignoringRequestMatchers,直接用默认配置就行了:
.csrf(Customizer.withDefaults())
// 或者
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
完整配置代码:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private CustomAuthFailureHandler failureHandler;
@Autowired
private CustomAuthSuccessHandler successHandler;
// RateLimitFilter 请求限流,自定义
private final RateLimitFilter rateLimitFilter;
public SecurityConfig(RateLimitFilter rateLimitFilter) {
this.rateLimitFilter = rateLimitFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").permitAll() // API接口,无需认证
.requestMatchers("/login", "/doLogin").permitAll() // 登录页面、登录接口允许匿名访问
.anyRequest().authenticated() // 其他页面必须登录
)
// 在身份验证之前进行限流
.addFilterBefore(rateLimitFilter, UsernamePasswordAuthenticationFilter.class)
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/**") // 忽略API的CSRF保护
)
//所有请求都必须用 HTTPS(而不是 HTTP)访问。
.requiresChannel(channel -> channel.anyRequest().requiresSecure())
//登录成功后,创建一个新的 Session,然后把原来 Session 里的数据(比如购物车、表单数据)复制过来。
//这样可以避免有人在登录之前偷偷设置了一个 Session ID,防止Session劫持。
.sessionManagement(session -> session
.sessionFixation(SessionManagementConfigurer.SessionFixationConfigurer::migrateSession)
)
// .formLogin(AbstractAuthenticationFilterConfigurer::permitAll) //使用系统自带的默认登录页面
.formLogin(form -> form //指定自定义登录页面,允许表单登录
.loginPage("/login")
.loginProcessingUrl("/doLogin") // 登录表单提交地址(告诉Spring Security这个接口是你处理登录的)
.defaultSuccessUrl("/main", true) // 登录成功后跳转的页面
.failureUrl("/login?error")
.successHandler(successHandler) // 自定义登录成功处理
.failureHandler(failureHandler) // 自定义登录失败处理
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout") // 设置登出请求URL
.logoutSuccessUrl("/login") // 登出成功后跳转到的页面
.invalidateHttpSession(true) // 使当前Session无效
.deleteCookies("JSESSIONID") // 删除JSESSIONID Cookie
.permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, UserDetailsService userDetailsService) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder
.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
return authenticationManagerBuilder.build();
}
}
五、在表单中加入 CSRF 隐藏字段
1.使用 Thymeleaf 渲染HTML页面
Spring Boot + Thymeleaf 默认集成 Spring Security 的 CSRF 支持,只需要在表单中加上 th:action 和 th:method,Thymeleaf 会自动插入 CSRF Token:
<form th:action="@{/doLogin}" method="post">
<!-- 自动注入 CSRF token -->
<input type="text" name="username" />
<input type="password" name="password" />
<button type="submit">登录</button>
</form>
在最终渲染的 HTML 中,会自动变成这样:
<form action="/doLogin" method="post">
<!-- 自动加入这一行 CSRF token -->
<input type="hidden" name="_csrf" value="xxxxxx" />
<input type="text" name="username" />
<input type="password" name="password" />
<button type="submit">登录</button>
</form>
2.使用原生 HTML,而不是 Thymeleaf
你需要手动添加一个隐藏字段<input type=“hidden” … /> 到form中,放入 CSRF Token:
<form action="/doLogin" method="post">
<input type="hidden" name="_csrf" value="${_csrf.token}" />
...
</form>
你还需要在 controller 中把 token 传给页面:
@GetMapping("/login")
public String loginPage(Model model, HttpServletRequest request) {
CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
model.addAttribute("_csrf", token);
return "login"; // 指向 login.html
}
3. 如果是用 JavaScript/AJAX 提交请求
// 允许前端 JavaScript 访问 Cookie 中的 CSRF token(默认是 HttpOnly,前端无法访问)。
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
从 Cookie 中读取 Token,并加入到请求头。以原生 JavaScript 为例:
function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
if (match) return match[2];
return null;
}
const token = getCookie("XSRF-TOKEN");
fetch('/your/api', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': token // 关键点:加到请求头中。名字必须是 X-XSRF-TOKEN(Spring Security 会识别这个名字)。
},
body: JSON.stringify({ key: 'value' })
});