CSRF是什么?
跨站点请求伪造(Cross-Site Request Forgery,CSRF)是一种常见的网络攻击手段。由于http协议本身无状态,客户端与服务端在基于http协议进行数据交互时,常利用cookie进行服务端和客户端的之间交互的状态和数据的记录,因此cookie里面可能会放置服务端生成的session id(会话ID)和用来识别客户端访问服务端过 中的客户端的身份标记,那么恶意站点就有可能通过正常站点中存在的cookie信息来伪造用户的请求,会给服务器代理很大的危险。CSRF的示意图如下:
为克服CSRF的危险,Spring Security提供了Token的机制来方式CSRF攻击。在Spring Boot项目中引入Spring Security的maven依赖即可以默认开启Spring Security的配置,此时会默认开启CSRF验证。此时具有如下的一些特性:
- CSRF验证不针对GET类型请求,仅针对POST请求。
- Spring Security开启后会自动默认生成CSRF参数(包括TOKEN值),当POST请求时需要请求携带对应的TOKEN值用于验证。如果请求中不存在验证参数或者验证参数和服务端保存的不一致,则认为是异常的请求则会出现403异常返回值。
CSRF的参数不建议存放在cookie中(因为会被恶意请求得到),当前后端不分离是可以存放到DOM中进行保存,Spring在返回页面时自动填充CSRF参数。但对于前后端分离的情况(跨域环境),下面的解决方案中还是将CSRF参数放到cookie中进行保存,并通过开启withCredentials设置允许cookie的跨站点发送。
AJAX中解决CSRF问题
系统和环境描述:
- Spring Boot开启Security(会自动开启CSRF机制)
- JSP页面(Spring Boot项目中的JSP,无跨域情况)
- AJAX POST请求
- 请求出现403异常
简单的禁用CSRF
在继承了WebSecurityConfigurerAdapter的Spring管理类的configure方法中加入http.csrf().disable()代码即可以禁用Spring Security开启的CSRF机制。可以解决上述的POST请求403问题。
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
// 根据角色设置访问权限
setRoleAuthorize(http);
// 禁用CSRF
disableCsrf(http);
}
private void disableCsrf(HttpSecurity http) throws Exception {
http.csrf().disable();
}
设置AJAX请求
如果需要使用安全认证,则可以在不禁用CSRF的情况下,在JSP页面进行如下的代码设置。首先在JSP页面的<head>标签中加入如下的<meta>标签,加入后在Spring相应该页面后会自动填充其中的csrf字段的值:
<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
然后在AJAX请求前,加入如下的代码设置。这种代码的设置也是Spring Boot文档中推荐使用的:
$(document).ready(function () {
// 设置请求头
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function (e, xhr, options) {
xhr.setRequestHeader(header, token);
});
// 自己的POST请求方法
$("#submit").click(function () {
var userId = $("#userId").val();
var productId = $("#productId").val();
var quantity = $("#quantity").val();
var params = {
userId: userId,
productId: productId,
quantity: quantity
};
$.post("./start", params, function(result) {
alert(result.msgInfo);
});
});
})
</script>
Form表单提交时设置隐藏字段
如果是在Form表单POST提交时出现CSRF的问题,则可以在表单中加入下面的隐藏字段一并提交,即可以避免POST请求出现CSRF的问题:
<form action="/signOut" method="POST">
# 表单其他内容
# ...
# 表单隐藏字段
<input type="hidden" id="${_csrf.parameterName}" name="${_csrf.parameterName}" value="${_csrf.token}"/>
</form>
此外还有如下两种方式,暂时未进行测试。
1. 直接设置POST请求header:
var headers = {};
headers['X-CSRF-TOKEN'] = "[[${_csrf.token}]]";
$.ajax({
url: url,
type: "POST",
headers: headers,
dataType: "json",
success: function(result) {
}
});
2. 直接作为POST请求参数进行设置:
$.ajax({
url: url,
data: {
"[[${_csrf.parameterName}]]": "[[${_csrf.token}]]"
},
type: "POST",
dataType: "json",
success: function(result) {
}
});
跨域情况下使用Axios时解决CSRF问题
该情况下使用前后端分离的程序设计思想,跨域环境如下:
- 服务端:Spring Boot项目(开启Spring Security)
- 前端:Ice(封装React)
- 请求工具:Axios GET/POST(GET请求不存在跨域问题)
- 发送Post请求时同样出现403异常
解决跨域情况下CSRF问题
对于前后端分离设置的跨域情况下的CSRF问题解决,由于前端页面之间的转换不在经过Spring服务端,因此Spring生成的CSRF参数无法直接放置到页面内作为请求传入。
因此下面使用cookie存放Spring生成的CSRF参数,并实现跨域环境下的cookie请求的方式解决该问题。具体如下:
1. Axios POST请求中设置withCredentials: true(该值默认为false),开启允许跨域发送cookie验证。
axios({
method: 'post',
url: 'http://localhost:8080/react-user/create',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
data: {
userName: username,
sex: sex,
note: note
},
withCredentials: true, // 开启跨域cookie验证(同时需要Spring Boot服务端也开启对应的设置)
// xsrfCookieName: 'XSRF-TOKEN', // 这里实际上是默认的设置,所以可以不设置
}).then(function (response) {
console.log(response);
}).catch(function (error) {
console.log(error);
});
同时需要注意,在axios中已经默认设置了如下的字段用于XSRF。因此对于请求头中可以不设置XSRF-TOKEN来发送请求。
// `xsrfCookieName` is the name of the cookie to use as a value for xsrf token
xsrfCookieName: 'XSRF-TOKEN', // default
// `xsrfHeaderName` is the name of the http header that carries the xsrf token value
xsrfHeaderName: 'X-XSRF-TOKEN', // default
2. Spring Boot服务端设置Access-Control-Allow-Credentials: true响应头,与上述前端设置配合来开启跨域环境下cookie的正确发送。在IoC中加入如下的Bean:
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
// spring boot跨域设置
registry.addMapping("/**")
//设置允许跨域请求的域名
.allowedOrigins("*")
//是否允许证书 不再默认开启(在跨域情况下使用cookie时开启,需要axios开启该对应的选项)
.allowCredentials(true)
//设置允许的方法
.allowedMethods("*")
//跨域允许时间
.maxAge(3600);
}
};
}
此时如果只在前端中设置了withCredentials: true,而服务端没有开启allowCredentials(true),则请求时会出现如下的异常:
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
注:并且如果服务端没有上述设置,则跨域环境下前端无法获取到cookie中的值,输出为空。加上以后才能够获取到cookie中的值。
3. 配置Spring Security使用cookie来存储XSRF_TOKEN的值。完成上述配置后,服务端和客户端就可以在跨域环境下来发送cookie完成CSRF验证了,但还需要在Spring Security中将需要验证的XSRF_TOKEN的值放入到cookie中,才能供客户端获取并发送。因此需要继承了WebSecurityConfigurerAdapter的Spring管理类的configure方法中加入如下代码:
// ...
http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
// ...
注意:同时完成以上3点配置,才可以在跨域环境下正常的使用POST请求,并开启CSRF验证。否则还是会出现请求403的异常。