CSRF的防御解決過程


CSRF是什么,就不多說,網絡上的帖子多的去了,關於其定義。

 

這里主要介紹我們項目中,是如何解決這個問題的。方案比較簡單,重點是介紹和記錄一下遇到的問題和一些小的心得。

 

1. 解決方案

A. 用戶登錄的時候,將創建一個token,此token存放於session當中。(是否在登錄后創建token,依據各自系統需求變化)

B. 基於Filter,對所有的Http請求進行攔截,捕獲請求路徑,確認路徑URL是否在配置的CSRF安全攔截路徑列表CsrfList中。

C. 若在CsrfList中,則檢查session中是否含有sToken字段以及Http請求頭中是否含有rToken字段。

D. 若sToken和rToken相等,則認為安全合法的請求,否則將請求攔截,拒絕此次請求。

這里:

1》. 主要是基於過濾器Filter來實現,另外,一個比較核心的思想,是將安全路徑(需要校驗的,比如系統參數相關的增刪改相關的數據提交請求)通過配置的方式,以配置文件或者數據庫表的形式配置在系統中(本案例,采用的是靜態配置文件)。說白了,和Shiro或者Spring security的權限管理很像。

2》. 另外一點,將后台生成的token數據傳遞前端,並在前端有數據提交的時候將這個token值帶回到后台。笨一點的辦法,就是在每次ajax數據提交的時候,都給調用beforeSend方法給XMLHttpRequest里面添加自定義的Header屬性(當然,也可以通過其他方式實現token的回傳到后台,我這里采用的是Http的自定義Header屬性的模式)。最好是有一個全局的配置,至少是文件級別的配置,減少ajax提交數據的時候寫入重復的beforeSend調用。

 

2. 核心代碼

核心代碼,分Filter后端的部分,以及前端的beforeSend調用部分。

1》. Filter對應的后端部分

package com.tk.logc.core.csrf;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.apache.log4j.Logger;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;

import com.google.gson.Gson;
import com.tk.logc.core.Constants;
import com.tk.logc.core.ResultData;


/**
 * @author shihuc
 * @date  2018年8月21日 上午9:23:02
 */
public class CsrfFilter implements Filter{
    
    static Logger logger = Logger.getLogger(CsrfFilter.class);
    
    static Set<String> csrfUrls = new HashSet<String>();
    
    static {
        InputStream in = CsrfFilter.class.getResourceAsStream("/conf/csrf.properties"); Properties properties = new Properties(); try { properties.load(in); } catch (IOException e) { e.printStackTrace(); } Iterator<Entry<Object, Object>> it = properties.entrySet().iterator(); while (it.hasNext()) { Entry<Object, Object> entry = it.next(); Object key = entry.getKey(); String keys = key.toString().trim(); String urlPref[] = keys.split("_"); String urlPrefix = "/"; for(String pu: urlPref){ urlPrefix += pu + "/"; } logger.info("Prefix: " + urlPrefix); Object value = entry.getValue(); String urls = value.toString(); String urlSuffix[] = urls.split(","); String realUrl = ""; for(String suffix: urlSuffix){ suffix = suffix.trim(); realUrl = urlPrefix + suffix; csrfUrls.add(realUrl); logger.info("URL: " + realUrl); } }
    }

    /* (non-Javadoc)
     * @see javax.servlet.Filter#destroy()
     */
    @Override
    public void destroy() {
        // TODO Auto-generated method stub
        
    }

    /* (non-Javadoc)
     * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
     */
    @Override
    public void doFilter(ServletRequest req, ServletResponse rsp, FilterChain chain) throws IOException, ServletException {    
        HttpServletRequest hReq = (HttpServletRequest) req; Subject subject = SecurityUtils.getSubject(); boolean isAuthed = subject.isAuthenticated(); Session session = subject.getSession(); if(isAuthed) { Object csrfToken = session.getAttribute(Constants.CURRENT_USER_JOB_KEY); Object httpToken = hReq.getHeader(Constants.CURRENT_USER_JOB_KEY); String uri = hReq.getRequestURI().toString(); String ctx = hReq.getContextPath().toString(); String tarUri = uri.substring(ctx.length(), uri.length()); logger.info("REQ URL:" + tarUri + ", sToken: " + csrfToken + ", rToken: " + httpToken); if(csrfUrls.contains(tarUri)){ if (csrfToken != null && !csrfToken.equals(httpToken)){ Gson gson = new Gson(); ResultData<HashMap<String, String>> rd = new ResultData<HashMap<String, String>>(); rd.setSuccess(false); rd.setMsg(Constants.CURRENT_CSRF_ERRINFO); rsp.setCharacterEncoding("UTF-8"); rsp.setContentType("text/html;charset=UTF-8"); rsp.getWriter().write(gson.toJson(rd)); }else{ chain.doFilter(req, rsp); } }else{ chain.doFilter(req, rsp); } }else{ chain.doFilter(req, rsp); }  
    }

    /* (non-Javadoc)
     * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
     */
    @Override
    public void init(FilterConfig arg0) throws ServletException {
        // TODO Auto-generated method stub
        
    }

}

這里,配置文件在靜態塊里面加載,這里采用了一點點小技巧,方便配置簡單化,因為一個后台系統配置功能頁面,往往會有多個操作,例如:create,update,delete等,配置的時候,可以將key和value部分優化,然后后台加載時,進行URL路徑組裝重配。例如,我這里的配置文件:

#
#所有需要做CSRF攔截校驗的URL,沒有弄明白操作邏輯前,請勿修改
#Key部分是url組成的一部分,依據下划線分隔,和Value部分逗號分開的部分組合成最終的URL
#Value部分,反映的是一類業務中多個子類型的操作,每個都用逗號分隔
#例如:a_b=u1,u2 對應的URL信息解析后是: /a/b/u1和/a/b/u2
#
system_role=create,update,initUpdate,delete,saveRolePermission,addRolePermission
system_user=deleteUser,createUser,updateUser,userRole,saveUserRole

 

2》. 前端JS的核心代碼

(function($){
  var _ajax = $.ajax;
  $.ajax = function(options){
    var fn = {
      beforeSend: function (XMLHttpRequest) { XMLHttpRequest.setRequestHeader("X-Job-Key", $("#csrfToken").val()); }
    };
    if(options.beforeSend){
      fn.beforeSend = options.beforeSend;
    }
    var _options = $.extend(options,{
      beforeSend: fn.beforeSend
    })
    _ajax(_options);
  }
})(jQuery);

這段JS代碼,是前端的核心,擴展了jQuery的ajax的行為,主要是將beforeSend函數擴展了,在每次只需ajax的時候,都要執行beforeSend,完成給Http請求頭部添加一個自定義的屬性值,供后台收到請求的時候,解析校驗。

 

3. 注意事項

這里,主要涉及到幾點,都是一些細節,容易落入坑里:

1》.前端用http頭部自定義的屬性,比較用Cookie安全,為了高效,通過擴展ajax的請求,就像我上面的核心代碼JS部分的例子一樣,每一個JS文件里面,類似上面加入這段代碼。注意:代碼最后有一個分號,這個分號一定得加上,否則,在一個JS文件里面,若有多個(function($){})(jQuery)這樣的代碼段,就會出現下面的錯誤。

Uncaught TypeError: (intermediate value)(intermediate value)(...) is not a function
    at VM71 xxxx.js:22

 

為了效率,beforeSend的使用,若只有少量的地方使用,可以采用下面的模式,在需要的ajax調用里面使用。

$.ajax({
    url: url, 
    data: {"id":roleId}, 
    dataType:"json",
    //stype:"GET",
 beforeSend: function (XMLHttpRequest) { XMLHttpRequest.setRequestHeader("X-Job-Key", $("#randomx").val()); },
    success: function(data){                            
        if(data.isSuccess){
            //初始化數據
            initRoleData(data.object);
        }else{
            bootbox.alert(data.msg);
        }
  }
});

 

2》.Http頭部定義的屬性變量,不建議使用帶有下划線的變量

這里,之所以這么說,主要是因為現在的web應用系統,很多會采用Nginx作為反向代理,Nginx會對Http請求頭部的帶有下划線的屬性進行過濾處理,丟棄掉了。這樣一來,帶有下划線的屬性,ajax或者其他模式發起的HTTP請求,就會被Nginx默認給丟棄了,后台應用服務器上,就獲取不到該變量。

下面是Nginx官方文檔對變量定義中下划線的描述:

underscores_in_headers

Context: http, server
Allows or disallows underscores
in custom HTTP header names. If this directive is set to on, the following example header is considered valid by Nginx: test_ header: value.
Syntax: on or off Default value: off

這個問題,被坑的現象是,在本地調試一點問題沒有,上測試環境,就總是失敗,獲取rToken值總是null,才想起有這么一個坑。。。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM