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的異常。