原文地址:http://moon-walker.iteye.com/blog/2397907,https://www.oschina.net/code/snippet_1029551_27153#45401
此文僅作爲當時解決此問題記錄
CSRF攻擊
CSRF攻擊全稱爲:Cross-site request forgery,直接翻譯爲:跨站請求僞造。直接看名稱還是有點難以理解,容易跟XSS攻擊搞混。在講解如何防禦之前,首先看看如何攻擊,舉個簡單的攻擊例子:
1、假設你知道身邊的一個同事每天都會登陸他的xxx網上銀行(假設這個銀行沒有做CSRF防禦),由於習慣他一般會採用默認的瀏覽器登陸;
2、在他登陸網上銀行之後,你往他的郵箱發一封郵件(他是你同事你當然知道他的郵箱),郵件的內容爲一張圖片(圖片內容要足夠吸引人去點擊,比如“京東商城”滿199-100什麼的),圖片的鏈接地址爲xxx網上銀行的轉賬操作鏈接比如:https://xxx.bank.com/ trans_money? money=10000&target_accuout=“你的銀行賬戶”,該鏈接執行的操作是向你的賬戶轉10000塊。
3、你的同事收到郵件後,點擊這個圖片,會使用默認瀏覽器打開上述轉賬鏈接。由於他已經使用默認瀏覽器登陸了自己的網上銀行,這時就會在你同事毫無知覺的情況下,借他的手向你的賬戶轉10000塊大洋(別轉太多了,太多了需要短信驗證什麼的)。
上述攻擊示例僅僅是爲了說明CSRF的攻擊方式,請勿嘗試 不要拿自己的同事做獵物。當然現在的網上銀行不會像我說的這麼弱智,即便嘗試也沒有效果。
這裏舉例是用郵件向攻擊目標推送“攻擊鏈接”,也可以使用任意的其他方式,比起在其他網站上掛一個“攻擊鏈接”,如果他在登陸自己“網上銀行”的瀏覽器裏同時打開了這個“其他網站”,你又成功的引誘他點擊了這個“攻擊鏈接”,就可以接攻擊目標自己的手執行你想要的任何操作。這就是所謂的CSRF攻擊,可見其危害之大。
攻擊者一般會在通過掃描工具掃描系統是否存在CSRF漏洞的操作鏈接,然後分析這些鏈接是否有價值,比如刪除或修改重要數據、發送郵件等。然後構造這些鏈接請求,在目標用戶登陸該系統的情況下,通過各種手段誘導你去執行這些鏈接,從而達到自己的攻擊目的。
CSRF防禦
CSRF防禦比較常見的手段是對每個請求進行token驗證,對驗證不通過的請求進行攔截。這種方式理論可以對每個請求都進行token驗證,但這樣系統就缺乏一些靈活性,根據具體情況,一般不會對所有的請求進行token校驗,只對有數據更改的部分(post請求)或者敏感數據查詢進行token校驗。token驗證流程如下:
1、客戶端發起請求瀏覽一個頁面,服務端收到請求 通過UUID生成一個隨機數作爲token,存放在服務器端,現在的系統一般都是多實例分佈式部署,所有一般採用共享緩存進行存儲,比如redis(爲什麼不使用session、request或者本地緩存?因爲下一次請求有可能落到另外一臺機器上)。
2、服務端渲染頁面返回時,把這個新生成的token放到一個hidden的隱藏變量中。
3、客戶端在請求或修改敏感數據時,在請求header中附帶上這個token。服務端收到請求後,獲取header中的token,與共享緩存中的token進行對比:
A、假設兩個token相同,則通過驗證,爲了防止表單重複提交,這時可以在“共享緩存”中刪除這個token。然後繼續進行正常業務處理,在請求返回之前生成新token存放到“共享緩存”,連同該新token一起返回給客戶端,以便後續請求繼續使用。
B、假設兩個token不相同,說明有可能是token已過期(“共享緩存”中的token不能無限期的存放,一般半個小時左右即可),或者是遭到了CSRF攻擊。這時攔截該請求,返回請求失敗。如果是token已過期,可以刷新頁面獲取新的token 即可繼續操作,這也就是爲什麼有的網站在過一段時間直接需要刷新一下才能繼續操作的原因。
可以看到這個token是實時變化的,CSRF攻擊者無法進行僞造,從而達到防禦的目的。
Spring MVC中的CSRF防禦
通過上述流程,可以看到CSRF防禦的關鍵就是Token的生成、刪除和驗證,這些操作都是在正常的業務操作之前或者之後進行的(驗證和刪除是之前,生成是之後)。在Spring MVC中很容易就能想到通過攔截器HandlerInterceptor進行處理(如果對spring攔截器不清楚的,可以點擊這裏)。具體的處理方式,根據鏈接是否有規律又分爲兩種:
攔截器統一處理方式:對鏈接有規律的處理比較簡單(比如RESTful風格的鏈接),只需要對固定的鏈接進行鏈接,在preHandle中進行token的“驗證和刪除”,在postHandle中進行token的“生成”即可,這也是使用RESTful風格編程的福利:
攔截器xml配置:
<mvc:interceptor>
<mvc:mapping path="/xx/delete"/>
<mvc:mapping path="/xx/update"/>
<bean class="com.xxx.intercepter.CSRFInterceptor" />
</mvc:interceptor>
CSRFInterceptor攔截器實現:
public class CSRFInterceptor implements HandlerInterceptor {
@Resource
private CSRFTokenUtil csrfTokenUtil;
//驗證和刪除token
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requstCSRFToken = request.getHeader("csrf-token");
if(csrfTokenUtil.verifyToken(requstCSRFToken)){
csrfTokenUtil.deleteToken(requstCSRFToken);//驗證通過後,立即刪除token,可以表單防止重複提交。
return true;
}
return false;
}
//生成token
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception {
String new_token = csrfTokenUtil.generate();
request.setAttribute("csrf-token", new_token); //頁面上後續異步操作,需要新的token
}
//省略afterCompletion方法
}
另外還需要在,訪問指定頁面時,生成token
訪問頁面的請求生成token:
@RequestMapping("/form")
@ VerifyCSRFToken
@ResponseBody
public String form (Map map,Integer id) {
//省略業務代碼
String new_token = csrfTokenUtil.generate();
map.put("csrf-token",new_token);//生成token
return “/form”
}
ok,大功告成,可見如果採用spring mvc的RESTful風格編程,對防禦CSRF攻擊是so eazy。
指定註解方式:但不幸的是我們有許多老系統,不是RESTful風格的,鏈接的規則也是雜亂無章,腫麼辦。這時可以採用攔截器加註解的方式,進行處理,處理起來稍微麻煩些,分三步說明:
1、創建攔截器,攔截所有的請求:
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.xxx.intercepter.CSRFInterceptor" />
</mvc:interceptor>
2、新建一個自定義註解VerifyCSRFToken,加到需要進行token驗證的Controller方法中,並在這個方法返回之前,生成新token。
VerifyCSRFToken註解定義:
@Target({ java.lang.annotation.ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface VerifyCSRFToken {
//需要驗證防跨站請求
public abstract boolean verify() default true;
}
訪問頁面的請求生成token:
@RequestMapping("/form")
@ VerifyCSRFToken
@ResponseBody
public String form (Map map,Integer id) {
//省略業務代碼
String new_token = csrfTokenUtil.generate();
map.put("csrf-token",new_token);//生成token
return “/form”
}
需要進行防禦的方法:
@RequestMapping("/update")
@ VerifyCSRFToken
@ResponseBody
public void update (Integer id) {
//省略業務代碼
String new_token = csrfTokenUtil.generate();
result. put("csrf-token",new_token); //重新生成token
sendResultJson(result);
}
3、最後看下攔截器的處理,由於token的生成已經分散到各個Cotrlloer方法中,攔截器的postHandle無需處理。由於攔截器CSRFInterceptor攔截了所有的請求,在preHandle需要首先取出含有@ VerifyCSRFToken的方法,才能進行token校驗。具體實現如下:
public class CSRFInterceptor implements HandlerInterceptor {
@Resource
private CSRFTokenUtil csrfTokenUtil;
//驗證和刪除token
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
VerifyCSRFToken verifyCSRFToken = method.getAnnotation(VerifyCSRFToken.class);
// 只對使用@VerifyCSRFToken註解的方法,進行csrf token校驗
if (verifyCSRFToken != null) {
String requstCSRFToken = request.getHeader("csrf-token");
if (csrfTokenUtil.verifyToken(requstCSRFToken)) {
csrfTokenUtil.deleteToken(requstCSRFToken);//驗證通過後,立即刪除token,可以表單防止重複提交。
return true;
}
return false;
}
return true;
}
//生成token
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception {
}
//省略afterCompletion方法
}
對比兩種方式:“攔截器統一處理方式”看起來更優雅,對業務沒有侵入性,但鏈接必須是規則的RESTful風格。“指定註解方式”使用更靈活,可以對指定的方法進行防禦,但對業務代碼有一定的侵入性。兩種方式可以根據自己的系統具體情況進行選擇。
token工具類
前面代碼實例中用的了CSRF的工具類CSRFTokenUtil,這個工具類封裝了token的生成、校驗、刪除。前面也提到過,在分佈式的部署系統中,只能使用“共享緩存”進行token在服務端的存在,本工具類使用的是redis。
另外,通過前面的代碼我們會發現,每次訪問一個新頁面時都需要生成一個新的token放到redis,如果有惡意用戶 一直刷新頁面(多機併發刷),理論上會到導致redis緩存被刷爆。所有我們必須對每個用的token數量進行限制,但又不能太少,否則用戶不能同時打開多個頁面。這裏我們限制每個用戶,最多隻能生產100個token,如果超過100不再生產新的token,而是隨機選擇這個100箇中的一個token返回,採用的是redis的set(集合)數據類型進行存儲(提示:下列代碼中的redis的操作 進行過封裝,請使用自己的項目中redis的使用方式替換),這樣可以防止redis 惡意被刷,實現代碼如下:
/**
* csrf攻擊防禦工具類
* Created by gantianxing on 2017/10/13.
*/
public class CSRFTokenUtil {
public static final String CSRF_TOKEN="csrf-token";
public static final int THIRTY_MINUTES = 30*60;//token緩存時間30分鐘
@Resource
private RedisUtil redis;
/**
* 生成新token 放入redis set中(集合)
* 每個user最多允許100個token
* @return
*/
public String generate() {
int userId = getUserId();
if(userId > 0){
String key = "user_token"+userId;
int snum = redis.scard(key);
//如果該用戶的token數大於100,則隨機返回一個已有token,不在生成新token
if(snum > 100){
token = redis.srandmenber(key);
}else {//否則生成新token
String uuid = UUID.randomUUID().toString();
redis.sadd(key,THIRTY_MINUTES,uuid);
}
}
return token;
}
/**
* 驗證token(在set中查找)
* @param page_token
* @return
*/
public boolean verifyToken(String page_token){
if(redis.isNotBlank(page_token)){
int userId = getUserId();
String key = "user_token"+userId;
//判斷redis集合中是否存在
if(userId>0 && redis.sismember(key,page_token)){
return true;
}
}
return false;
}
/**
* 刪除token(從set中刪除)
* @param page_token
*/
public void deleteToken(String page_token){
int userId = getUserId();
if (userId>0){
String key = "user_token"+userId;
redis.srem(key, page_token);//從redis集合中刪除
}
}
//獲取當前用戶id
private int getUserId(){
//獲取用戶id邏輯省略,一般會把用戶信息放到TreadLocal中,從TreadLocal中獲取
return 123;
}
}
關於token的工具類的編寫,主要核心有兩點:1、使用支持分佈式的“共享緩存” 2、token要遵守誰創建誰使用的原則(就是跟用戶綁定),同時必須限制每個用戶創建token數量。
對於CSRF攻擊方式,以及如何防禦就總結到這裏。