SpringMVC/boot-CSRF安全方案
1. CSRF原理與防禦方案概述
一. 原理
-
增刪改的介面引數值都有規律可循,可以被人惡意構造增刪改介面
-
將惡意構造的增刪改介面發給對應特定使用者,讓特定使用者點選
-
特定使用者使用自己的認證資訊對該介面發起了請求,可能被新增危險資訊(比如管理員賬號),修改敏感資訊(比如退款金額),刪除關鍵資訊(比如刪除差評)
二. 防禦方案概述
-
引數不可猜解,發起請求時在引數中增加隨機token引數
-
token引數在後臺與儲存在cookie,session,tair中的token引數進行比對,若不匹配或者沒有該引數,則校驗不通過
-
黑客無法獲取到特定使用者的隨機token值,所以杜絕CSRF的危害
2 ali修復方案
一 . 確認應用型別
若為正常業務,提供資料/檔案等增刪改服務,則需要配置CSRF,請繼續看下去
若應用不是Web應用,或者只是HSF服務或者給其他應用伺服器呼叫的API介面服務(純內網的純Server to Server,不是通過Login獲取登陸態的介面,而是通過AKSK加簽名驗證簽名) ,則不需要配置CSRF,請提供相應加簽驗籤程式碼給對應答疑或者安全工程師進行確認並關閉漏洞。
二 . 安全包引入
Step1. 配置擴充套件包POM依賴
注意:SpringMVC擴充套件安全包引入後會預設開啟一系列開關,包括XSS開關,CSRF開關等,從而導致業務短暫出現異常(前端頁面亂碼,介面訪問返回403狀態碼等),只需要繼續按照文件操作下去,業務最終會恢復正常。
1. SpringMVC擴充套件包
請參考網上SpringMVC安全擴充套件引入文件
2 SpringBoot擴充套件包 (starter)
請參考網上SpringBoot安全擴充套件引入文件
Step2. 檢視是否依賴成功
POM配置完成後,在IDEA的External Libraries中查詢是否以下包存在
//SpringMVC僅檢查這個包
com.alibaba.security:security-spring-webmvc
//SpringBoot僅檢查這個包
com.alibaba.security:security-spring-boot-starter
三 . CSRF開關配置
Step1. 顯式配置CSRF開關
雖然CSRF在安全包引入之後,會自動開啟CSRF攔截,但是為了確保配置可讀性以及後續問題排查方便,請在resources下的"application.properties" (若沒有,請建立)檔案中協商如下配置:
spring.security.csrf.enabled = true
注:開啟了該開關之後,所有訪問請求都會因為沒有token帶入被攔截導致訪問不成功,需要繼續配置下去,讓業務恢復正常
Step2. 將token帶給前端
配置好開關之後,安全包會生成一個隨機字串,我們稱為CSRF_Token,該token會被預設存入cookie中。若使用VM,則可以通過VM呼叫相關介面獲得。
這個Token需要在前端的每個增刪改介面請求中作為引數帶入給伺服器用於校驗安全性
不同前端技術方案有不同帶入方式:
1 VM後端模板
a. VM後端模板有三種token帶入請求的配置方式:
- 在application.properties中統一配置
- CSRF Token 自動生成的URL對映列表,多值使用逗號分隔(預設值為空)
- 當前URL風格為ant風格,風格值由配置項 spring.security.csrf.url.style 決定
spring.security.csrf.token.urls = /csrf_token/**
b. 在Controller類級別使用註解@CsrfTokenModel配置
@Controller
@RequestMapping("/csrf")
@CsrfTokenModel
public class CsrfController {
@RequestMapping("/form")
public String form() {
return "csrf_form";
}
}
c.在Controller類級別使用註解配置
此種情況可以使得該controller下所有模板渲染。都可以通過巨集獲取token的引數名稱和值
在方法級別使用註解@CsrfTokenModel配置
@Controller
@RequestMapping("/csrf")
public class CsrfController {
@RequestMapping("/form")
@CsrfTokenModel
public String form() {
return "csrf_form";
}
}
如果需要CSRF Token校驗的Controller或者方法過多時,當前框架還提供一種便利的方式, 即URL對映級別的自動生成方式,只需在application.properties檔案中增加如下配置:
-
CSRF Token 自動生成的URL對映列表,多值使用逗號分隔(預設值為空)
-
當前URL風格為正則表示式,風格值由配置項 spring.security.csrf.url.style 決定
spring.security.csrf.token.urls = /csrf_token/**
- CSRF Token 模型屬性名稱
spring.security.csrf.token.model.attribute = csrfToken
後端配置好之後,在VM模板中,針對所有請求form表單,增加對應欄位,確保每次請求都能帶上
- Velocity Template Code
<form method="post" action="/form/submit">
<input type="hidden" name="${csrfToken.parameterName}" value="${csrfToken.token}">
<input type="text" name="name"/>
<br>
<input type="submit" value="Submit"/>
</form>
- 渲染後的HTML
<form method="post" action="/form/submit">
<input type="hidden" name="_csrf" value="bfe23341-b28c-41a3-bed8-dfbd65385fc8">
<input type="text" name="name"/>
<br>
<input type="submit" value="Submit"/>
</form>
正常情況下如上圖所示,渲染後,欄位name為p_csrf, value為隨機生成的值,同時會在cookie中放入對應欄位。請注意,token的欄位名一定是要從csrftoken這個obj中取出來的,不能在前端自定義,若要在後端更換欄位名,請參考下面的『CSRF定製化功能』
2. ajax前後端分離
ajax發起請求的情況下,token無法直接渲染到頁面上,通過下方途徑解決該問題。
- 在cookie中讀取token,將其帶入到ajax請求的引數中。然後傳到後端(Cookie中token的key預設為XSRF-TOKEN)
- 若cookie中的XSRF-TOKEN值無法被js讀取,請檢查該值httponly屬性未true,若為true,請在"application.properties"中新增一個配置項,如下:
- 如果是在引數中攜帶,預設Token名稱是_csrf,如果是在header中攜帶,預設Token名稱是X-XSRF-TOKEN
spring.security.csrf.cookieHttpOnly = false
- 設定完成之後,請清除瀏覽器快取之後重新嘗試獲取XSRF-TOKEN值
3. 跨域下的token傳輸
在一般業務場景下,安全包會將token種到服務端對應的域名cookie下,可以被前端js呼叫和植入到header或者引數中。但是在跨域場景下,前端頁面與後端服務端不是同一個域名,導致無法取到服務端域名下的cookie。
假設aaaa.com要跨域訪問bbbb.com的介面,bbbb.com的介面做了csrf校驗。此時按照如下步驟進行token互動:
開啟CORS跨域頭的業務解決方案如下:
1. bbbb.com新增一個介面,返回自身的csrf token
該介面實現示例如下:
@RequestMapping(value = "/ajax", produces = MediaType.APPLICATION_JSON_VALUE)
@CrossOrigin(origins = "http://aaaaaa.com:7001", maxAge = 3600)
@ResponseBody
public CsrfToken getCsrfToken(HttpServletRequest request, HttpServletResponse response) {
CsrfToken csrfToken = csrfTokenRepository.loadToken(request);
if (csrfToken == null) {
csrfToken = csrfTokenRepository.generateToken(request);
csrfTokenRepository.saveToken(csrfToken, request, response);
}
return csrfToken;
}
2. aaaa.com攜帶with-credentials的頭部來獲取該token
function callOtherDomain(){
var xhr = new XMLHttpRequest();
if(xhr) {
xhr.open('GET', 'http://bbbbbb.com:7001/csrf/ajax', true);
xhr.withCredentials = true;
xhr.onload = function () {
result.innerHTML = xhr.responseText;
var json = JSON.parse(xhr.responseText);
token_key = json.paramterName;
token = json.token;
};
xhr.send(null);
}
}
3. 將獲取到的token存放在客戶端上(比如localstorage,或者頁面隱藏欄位中)
4. aaaa.com 訪問bbbb.com其他介面的時候,獲取token作為介面引數/頭部引數傳遞給bbbb.com,同時訪問該介面時應該設定withcredentials = true:
function buttonClick(token_key, token){
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://bbbbbb.com:7001/csrf/cors/check');
xhr.withCredentials = true;
xhr.onload = function () {
result.innerHTML = xhr.responseText;
};
xhr.onerror = function () {
result.innerHTML = "Error!";
}
xhr.send(token_key + '=' + token + '&' + otherparams)
}
Step3. 後端進行Token校驗
後端進行token校驗,目前只提供全域性url檢查方式,在沒有顯式配置情況下預設對所有POST請求進行token檢查,為了更好的對業務進行支援,建議在classpath下的application.properties進行顯式配置,如下:
//根據業務需求進行配置是否攔截GET請求,安全要求POST請求必須攔截
spring.security.csrf.supportedMethods = POST,GET
//使用ant風格配置需要進行token檢查的url(安全要求對所有增刪改進行token校驗)
spring.security.csrf.url.included = /**
//使用ant風格配置無需需要進行token檢查的url,只能對查詢介面進行excluded
spring.security.csrf.url.excluded = /csrf/nocheck
校驗之後,若成功,則會順利執行對應後臺功能,若失敗,則會返回403的狀態碼或者301跳轉taobao.error的情況,如下:
status : 403
message : Invalid CSRF Token '' was found on the request parameter 'p_csrf' or header 'h_csrf'.
===
status: 301
location: err2.taobao.com