4-SpringSecurity:CSRF防护

本文详细介绍了SpringSecurity中CSRF(跨站请求伪造)防护的原理与实践,包括登录、POST接口、退出时的防护措施,并探讨了前后端分离项目的CSRF解决方案。实验展示了如何在模板和JavaScript中处理CSRF令牌,以及如何配置Cookie存储类型。强调了涉及浏览器操作的安全请求应启用CSRF防护。

背景

本系列教程,是作为团队内部的培训资料准备的。主要以实验的方式来体验SpringSecurity的各项Feature。

依赖不变,核心依赖为WebSpringSecurityThymeleaf

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
</dependencies> 

从官网中可以知道,CSRF防护的关键在于我们发请求时附带一个随机数(CSRF token),而这个随机数不会被浏览器自动携带(eg: Cookie就会被浏览器自动带上)。

实验0:登录时的CSRF防护

显然,我们这里的登录请求是个POST方法(SpringSecurity默认忽略"GET", “HEAD”, “TRACE”, "OPTIONS"等幂等请求的CSRF拦截)。登录时必须携带_csrf参数,与认证信息一并提交,否则报403。

  • 后端安全配置(默认开启CSRF
@Override
protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/user/add").hasAuthority("p1").antMatchers("/user/query").hasAuthority("p2").antMatchers("/user/**").authenticated().anyRequest().permitAll() // Let other request pass.and()// .csrf().disable() // turn off csrf, or will be 403 forbidden.formLogin() // Support form and HTTPBasic.loginPage("/login").failureHandler(new AuthenticationFailureHandler(){@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {exception.printStackTrace();request.getRequestDispatcher(request.getRequestURL().toString()).forward(request, response);}});} 
  • 前端模板(新增了_csrf参数):
<form action="login" method="post"><span>用户名</span><input type="text" name="username" /> <br><span>密码</span><input type="password" name="password" /> <br><span>csrf token</span><input type="text" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> <br><input type="submit" value="登录">
</form> 

Note:

1.当然,实际中可以将新增的_csrf参数作为一个隐藏域进行提交:<input type="text" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" hidden/>
2.其实,如果我们使用默认的登录页面,可以在页面元素中看到同样有个隐藏域:

实验1:POST接口CSRF防护

通过form表单是一种发送POST请求的方式,但我们其他的请求不可能都通过form表单来提交。下面通过原生的JavaScript发起Ajax的POST请求。

  • 后端接口
@Controller
public class HelloController {@RequestMapping("/")public String hello(){return "index";}@PostMapping(value = "/ok")@ResponseBodypublic String ok() {return "ok post";}} 
  • 前端模板(新增index.html)
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><meta name="csrf" th:content="${_csrf.token}"><meta name="_csrf_header" th:content="${_csrf.headerName}" /><title>SpringSecurity</title>
</head>

<body><a href="/user/add">添加用户</a><a href="/user/query">查询用户</a><a href="/logout">退出</a><script language="JavaScript"> // let token = document.getElementsByTagName('meta')['csrf'].content;let token = document.querySelector('meta[name="csrf"]').getAttribute('content');let header = document.getElementsByTagName('meta')['_csrf_header'].content;console.log("token: ", token);console.log("header: ", header);function click() {let xhr = new XMLHttpRequest();xhr.open("POST", "http://localhost:8080/ok", true);xhr.setRequestHeader(header, token);xhr.onload = function (e) {console.log("response: ", e.target.responseText);}xhr.onerror = function (e) {console.log("error: ", e)}xhr.send(null);}click(); </script>
</body> 

Note: 前面这两个实验中用到了一些参数:_csrf.parameterName_csrf.token_csrf_header等,这些可以从源码中获悉:

public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository {
	private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";

	private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN";

	private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class
			.getName().concat(".CSRF_TOKEN");

	private String parameterName = DEFAULT_CSRF_PARAMETER_NAME;

	private String headerName = DEFAULT_CSRF_HEADER_NAME;private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME;
} 

实验2:退出时的CSRF防护

退出url在开启CSRF之后,直接以a标签形式请求/logout(即GET方式)会报404;此时logout必须以POST方式才可以正常退出。

public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>> extends
		AbstractHttpConfigurer<LogoutConfigurer<H>, H> {
	private List<LogoutHandler> logoutHandlers = new ArrayList<>();
	private SecurityContextLogoutHandler contextLogoutHandler = new SecurityContextLogoutHandler();
	private String logoutSuccessUrl = "/login?logout";
	private LogoutSuccessHandler logoutSuccessHandler;
	private String logoutUrl = "/logout";
	private RequestMatcher logoutRequestMatcher;
	private boolean permitAll;private boolean customLogoutSuccess;...

	/**
	 * The URL that triggers log out to occur (default is "/logout"). If CSRF protection
	 * is enabled (default), then the request must also be a POST. This means that by
	 * default POST "/logout" is required to trigger a log out. If CSRF protection is
	 * disabled, then any HTTP method is allowed.
	 *
	 * <p>
	 * It is considered best practice to use an HTTP POST on any action that changes state
	 * (i.e. log out) to protect against <a
	 * href="https://en.wikipedia.org/wiki/Cross-site_request_forgery">CSRF attacks</a>. If
	 * you really want to use an HTTP GET, you can use
	 * <code>logoutRequestMatcher(new AntPathRequestMatcher(logoutUrl, "GET"));</code>
	 * </p>
	 *
	 * @see #logoutRequestMatcher(RequestMatcher)
	 * @see HttpSecurity#csrf()
	 *
	 * @param logoutUrl the URL that will invoke logout.
	 * @return the {@link LogoutConfigurer} for further customization
	 */
	public LogoutConfigurer<H> logoutUrl(String logoutUrl) {
		this.logoutRequestMatcher = null;
		this.logoutUrl = logoutUrl;
		return this;
	}} 

可采用form表单或者Ajax的形式发送POST请求,携带_csrf参数,这里以form表单为例,点击POST logout按钮,可成功退出:

<form action="logout" method="post"><input type="text" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" hidden/> <br><input type="submit" value="POST logout">
</form> 

实验3:前后端分离时的CSRF防护

前面是通过在模板引擎中接收后端传回的_csrf,这里演示下前后端分离项目如何实现CSRF防护下的安全请求。

A CsrfTokenRepository that persists the CSRF token in a cookie named “XSRF-TOKEN” and reads from the header “X-XSRF-TOKEN” following the conventions of AngularJS. When using with AngularJS be sure to use withHttpOnlyFalse().

  • 后端安全配置(修改CSRF存储类型:CookieCsrfTokenRepository)
@Override
protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/user/add").hasAuthority("p1").antMatchers("/user/query").hasAuthority("p2").antMatchers("/user/**").authenticated().anyRequest().permitAll() // Let other request pass.and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()// .csrf().disable() // turn off csrf, or will be 403 forbidden.formLogin() // Support form and HTTPBasic.loginPage("/login").failureHandler(new AuthenticationFailureHandler(){@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {exception.printStackTrace();request.getRequestDispatcher(request.getRequestURL().toString()).forward(request, response);}});} 
  • 前端脚本
</body><script> function getCookie(name) {let arr = document.cookie.split("; ");for (let i = 0; i < arr.length; i++) {let arr2 = arr[i].split("=");if (arr2[0] == name) {return arr2[1];}}return "";}console.log("XSRF-TOKEN: ", getCookie("XSRF-TOKEN"));// 之后就可以拿着前面获取到的"XSRF-TOKEN"去请求后端POST等接口了 </script>
</body> 

Note: 这里大部分同学有个问题:Cookie都被自动带到请求中了,那攻击者不就又可以拿到了吗?

由于Cookie中的信息对于攻击者来说是不可见的,无法伪造的,虽然Cookie被浏览器自动携带了,但攻击者能做的仅仅是用一下Cookie,而Cookie里面到底放了什么内容,攻击者是不知道的,所以将CSRF-TOKEN写在Cookie中是可以防御CSRF的,相比默认的存放在Session中,CSRF-TOKEN写在Cookie中仅仅是换了一个存储位置。

什么时候需要开启CSRF?

官方文档建议,但凡涉及到浏览器用户操作,均应启用CSRF防护。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值