CSRF spring mvc 跨站請求偽造防禦
CSRF
CSRF(Cross-site request forgery跨站請求偽造,也被稱為“One Click Attack”或者Session Riding,通常縮寫為CSRF或者XSRF,是一種對網站的惡意利用。儘管聽起來像跨站指令碼(XSS),但它與XSS非常不同,並且攻擊方式幾乎相左。XSS利用站點內的信任使用者,而CSRF則通過偽裝來自受信任使用者的請求來利用受信任的網站。與XSS攻擊相比,CSRF攻擊往往不大流行(因此對其進行防範的資源也相當稀少)和難以防範,所以被認為比XSS更具危險性。
攻擊示例
如:一個網站使用者Bob可能正在瀏覽聊天論壇,而同時另一個使用者Alice也在此論壇中,並且後者剛剛釋出了一個具有Bob銀行連結的圖片訊息。設想一下,Alice編寫了一個在Bob的銀行站點上進行取款的form提交的連結,並將此連結作為圖片src。如果Bob的銀行在cookie中儲存他的授權資訊,並且此cookie沒有過期,那麼當Bob的瀏覽器嘗試裝載圖片時將提交這個取款form和他的cookie,這樣在沒經Bob同意的情況下便授權了這次事務。
CSRF是一種依賴web瀏覽器的、被混淆過的代理人攻擊(deputy attack)。在上面銀行示例中的代理人是Bob的web瀏覽器,它被混淆後誤將Bob的授權直接交給了Alice使用。
下面是CSRF的常見特性:
依靠使用者標識危害網站
利用網站對使用者標識的信任
欺騙使用者的瀏覽器傳送HTTP請求給目標站點
另外可以通過IMG標籤會觸發一個GET請求,可以利用它來實現CSRF攻擊。
spring mvc 框架下防禦策略
思路概要:
1.初始化頁面時在token隱藏域。
2.表單提交後帶入token到後臺,驗證token,如成功繼續操否為受到攻擊。
3.操作完之後重新生成token到頁面隱藏域。
程式碼示例:
建立攔截器:
/**
* <一句話功能簡述>
* <功能詳細描述>
* 防止跨站請求偽造攔截器
* 為每個返回的頁面新增CSRFToken引數
* @author Tangguilin
* @version [版本號, 2016年3月26日]
*/
public class AvoidCSRFInterceptor extends HandlerInterceptorAdapter
{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception
{
String url = request.getRequestURI();
if (!url.endsWith(".do"))
{
return true;
}
HandlerMethod handlerMethod = (HandlerMethod)handler;
Method method = handlerMethod.getMethod();
VerifyCSRFToken annotation = method.getAnnotation(VerifyCSRFToken.class);
if (annotation != null)
{
String xrq = request.getHeader("X-Requested-With");//是否為Ajax標誌
//非法的跨站請求校驗
if (annotation.verifyCSRFToken() && !verifyCSRFToken(request))
{
if (StringUtil.isEmpty(xrq))
{
//form表單提交,url get方式,重新整理csrftoken並跳轉提示頁面
request.getSession(false).setAttribute("CSRFToken",
TokenProcessor.getInstance().generateToken(request));
response.setContentType("application/json;charset=UTF-8");
PrintWriter out = response.getWriter();
out.print("非法請求");
response.flushBuffer();
return false;
}
else
{
//重新整理CSRFToken,返回錯誤碼,用於ajax處理,可自定義
BaseDataResp baseResp = new BaseDataResp();
String csrftoken = TokenProcessor.getInstance().generateToken(request);
request.getSession(false).setAttribute("CSRFToken", csrftoken);
baseResp.setCode(IDiMengResultCode.SystemManager.CSRF);
response.setContentType("application/json;charset=UTF-8");
PrintWriter out = response.getWriter();
out.print(baseResp.toString());
response.flushBuffer();
return false;
}
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView)
throws Exception
{
String url = request.getRequestURI();
if (!url.endsWith(".do"))
{
return;
}
//第一次生成token
if (modelAndView != null)
{
if (request.getSession(false) == null
|| StringUtil.isEmpty((String)request.getSession(false).getAttribute("CSRFToken")))
{
request.getSession(false).setAttribute("CSRFToken",
TokenProcessor.getInstance().generateToken(request));
return;
}
}
//重新整理token
HandlerMethod handlerMethod = (HandlerMethod)handler;
Method method = handlerMethod.getMethod();
RefreshCSRFToken refreshAnnotation = method.getAnnotation(RefreshCSRFToken.class);
String xrq = request.getHeader("X-Requested-With");//是否為Ajax標誌
if (refreshAnnotation != null && refreshAnnotation.refreshCSRFToken() && StringUtil.isEmpty(xrq))
{
request.getSession(false).setAttribute("CSRFToken", TokenProcessor.getInstance().generateToken(request));
return;
}
VerifyCSRFToken verifyAnnotation = method.getAnnotation(VerifyCSRFToken.class);
if (verifyAnnotation != null)
{
//成功後重新整理token
if (verifyAnnotation.verifyCSRFToken())
{
if (StringUtil.isEmpty(xrq))
{
request.getSession(false).setAttribute("CSRFToken",
TokenProcessor.getInstance().generateToken(request));
}
else
{
Map<String, String> map = new HashMap<String, String>();
map.put("CSRFToken", TokenProcessor.getInstance().generateToken(request));
response.setContentType("application/json;charset=UTF-8");
OutputStream out = response.getOutputStream();
out.write((",'csrf':" + JSONObject.toJSONString(map) + "}").getBytes("UTF-8"));
}
}
}
}
/** <一句話功能簡述>
* 處理跨站請求偽造
* 針對需要登入後才能處理的請求,驗證CSRFToken校驗
* @author tangguilin
* @param request
*/
protected boolean verifyCSRFToken(HttpServletRequest request)
{
String requstCSRFToken = request.getHeader("CSRFToken");//請求中的CSRFToken
if (StringUtil.isEmpty(requstCSRFToken))
{
return false;
}
String sessionCSRFToken = (String)request.getSession().getAttribute("CSRFToken");
if (StringUtil.isEmpty(sessionCSRFToken))
{
return false;
}
return requstCSRFToken.equals(sessionCSRFToken);
}
}
註解類:
/**
* 跨站請求仿照註解
* 重新整理CSRFToken
* @author Tangguilin
* @version [版本號, 2016年3月28日]
*/
@Target({java.lang.annotation.ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RefreshCSRFToken
{
/**
* 重新整理CSRFToken
* @author tangguilin
* @return
*/
public abstract boolean refreshCSRFToken();
}
/**
* 跨站請求仿照註解
*
* @author Tangguilin
* @version [版本號, 2016年3月28日]
*/
@Target({java.lang.annotation.ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface VerifyCSRFToken
{
/**
* 需要驗證防跨站請求
* @author tangguilin
* @return
*/
public abstract boolean verifyCSRFToken();
}
配置攔截:
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<!-- 定義在mvc:interceptor下面的表示是對特定的請求才進行攔截的 -->
<bean class="com.front.interceptor.AvoidCSRFInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
在jsp頁面中新增隱藏域
<input type="hidden" name="CSRFToken" value="${CSRFToken }"></input>
修改公用Ajax
ajax: function(options) {
var datas = null;
if (options["isAjaxForm"]) {
$("#" + options["formId"]).submit();
} else {
if (options["serialize"]) {
if (options["formId"]) {
datas = $("#" + options["formId"]).serialize();
} else {
datas = $(document.forms[0]).serialize();
}
}
var defaultOptions = {
type: "post",
async: false,
data: datas
};
options = $.extend(defaultOptions, options);
var path="";
if(options["url"].indexOf(basePath)==-1){
path=basePath;
}
var headers = {};
headers['CSRFToken'] = $("input[name='CSRFToken']").val();
$.ajax({
type: options["type"],
headers: headers,
async: options["async"],
dataType: options["dataType"],
url: path + options["url"],
data: options["data"],
success: function(data) {
//登入超時重新整理頁面後跳轉首頁
if("2000062"==data.code){
window.location.reload();
}
//處理跨站請求偽造
if("666666" == data.code){
if(Dialog){
Dialog.show("操作失敗,請重新整理頁面重試","error");
}else{
alert("操作失敗,請重新整理頁面重試");
}
return false;
}
if (typeof(options["success"]) == "function") {
options["success"](data);
}
},
error: function(data) {
//CSRFToken處理
if(data && data.readyState && data.readyState == '4'){
var responseBody = data.responseText;
if(responseBody){
responseBody = "{'retData':"+responseBody;
var resJson = eval('(' + responseBody + ')');
$("input[name='CSRFToken']").val(resJson.csrf.CSRFToken);
if (typeof(options["success"]) == "function") {
options["success"](resJson.retData);
}
}
return ;
}
//登入超時跳轉登入頁
/*if(data.responseText){
window.location.reload();//重新整理當前頁面.
return;
}*/
if (typeof(options["error"]) == "function") {
options["error"](data);
}else{
alert("請求地址:"+options["url"]+" 出現異常!請聯絡管理員!");
}
}
});
}
}
使用場景
為需要防禦的Controller加上@VerifyCSRFToken(verifyCSRFToken = true)註解
@RefreshCSRFToken(refreshCSRFToken=true):如新開啟的頁面,彈出框體需要CSRFToken時使用