背景
1、什么是CSRF攻擊?
這里不再介紹CSRF,已經了解CSRF原理的同學可以直接跳到:“3、前后端分離下有何不同?”。
不太了解的同學可以看這兩篇對CSRF介紹比較詳細的參考文章:
CSRF 攻擊的應對之道
淺談CSRF攻擊方式
如果來不及了解CSRF的原理,可以這么理解:有一個人發給你一個搞(mei)笑(nv)圖片鏈接,你打開這個鏈接之后,便立刻收到了短信:你的銀行里的錢已經轉移到這個人的帳戶了。
2、有哪些防御方案?
上面這個例子當然有點危言聳聽,當然可以確定的是確實會有這樣的漏洞:你打開了一個未知域名的鏈接,然后你就自動發了條廣告帖子、你的Gmail的郵件內容就泄露了、你的百度登錄狀態就沒了……
防御方案在上面的兩篇文章里也有提到,總結下,無外乎三種:
用戶操作限制,比如驗證碼;
請求來源限制,比如限制HTTP Referer才能完成操作;
token驗證機制,比如請求數據字段中添加一個token,響應請求時校驗其有效性;
第一種方案明顯嚴重影響了用戶體驗,而且還有額外的開發成本;第二種方案成本最低,但是并不能保證100%安全,而且很有可能會埋坑;第三種方案,可取!
token驗證的CSRF防御機制是公認最合適的方案,也是本文討論的重點。
3、前后端分離下有何不同?
《CSRF 攻擊的應對之道》這篇文章里有提到:
要把所有請求都改為 XMLHttpRequest 請求,這樣幾乎是要重寫整個網站,這代價無疑是不能接受的
我們前端架構早已經告別了服務端語言(PHP/JAVA等)綁定路由、攜帶數據渲染模板引擎的方式(畢竟是2011年的文章了,我們笑而不語)。
當然, 前端不要高興的太早:前后端分離之后,Nodejs不具備完善的服務端SESSION、數據庫等功能。
總結一下,在“更先進”的前端架構下,與以往的架構會有一些區別:
Nodejs層不處理SESSION,無法直接實現會話狀態數據保存;
所有的數據通過Ajax異步獲取,可以靈活實現token方案;
實現思路
如上文提到,這里僅僅討論在“更先進”的前端后端架構背景下的token防御方案的實現。
1、可行性方案
token防御的整體思路是:
第一步:后端隨機產生一個token,把這個token保存在SESSION狀態中;同時,后端把這個token交給前端頁面;
第二步:下次前端需要發起請求(比如發帖)的時候把這個token加入到請求數據或者頭信息中,一起傳給后端;
第三步:后端校驗前端請求帶過來的token和SESSION里的token是否一致;
上文提到過,前后端分離狀態下,Nodejs是不具備SESSION功能的。那這種token防御機制是不是就無法實現了呢?
肯定不是。我們可以借助cookie把這個流程升級下:
第一步:后端隨機產生一個token,基于這個token通過SHA-56等散列算法生成一個密文;
第二步:后端將這個token和生成的密文都設置為cookie,返回給前端;
第三步:前端需要發起請求的時候,從cookie中獲取token,把這個token加入到請求數據或者頭信息中,一起傳給后端;
第四步:后端校驗cookie中的密文,以及前端請求帶過來的token,進行正向散列驗證;
當然這樣實現也有需要注意的:
散列算法都是需要計算的,這里會有性能風險;
token參數必須由前端處理之后交給后端,而不能直接通過cookie;
cookie更臃腫,會不可避免地讓頭信息更重;
現在方案確定了,具體該如何實現呢?
2、具體實現
我們的技術棧是 koa(服務端) + Vue.js(前端) 。有興趣可以看這些資料:
趣店前端團隊基于koajs的前后端分離實踐
koa-grace——基于koa的標準前后端分離框架
grace-vue-webpack-boilerplate
在服務端,實現了一個token生成的中間件,koa-grace-csrf:
// 注意:代碼有做精簡
const tokens = require('./lib/tokens');
return function* csrf(next) {
let curSecret = this.cookies.get('密文的cookie');
// 其他如果要獲取參數,則為配置參數值
let curToken = '請求http頭信息中的token';
// token不存在
if (!curToken || !curSecret) {
return this.throw('CSRF Token Not Found!',403)
}
// token校驗失敗
if (!tokens.verify(curSecret, curToken)) {
return this.throw('CSRF token Invalid!',403)
}
yield next;
// 無論何種情況都種兩個cookie
// cookie_key: 當前token的cookie_key,httpOnly
let secret = tokens.secretSync();
this.cookies.set(options.cookie_key, secret);
// cookie_token: 當前token的的content,不需要httpOnly
let newToken = tokens.create(secret);
this.cookies.set(options.cookie_token, newToken)
}
在前端代碼中,對發送ajax請求的封裝稍作優化:
this.$http.post(url, data, {
headers: {
'http請求頭信息字段名': 'cookie中的token'
}
}).then((res) => {})
總結一下:
Nodejs生成一個隨機數,通過隨機數生成散列密文;并將隨機數和密文存到cookie;
客戶端JS獲取cookie中的隨機數,通過http頭信息交給Nodejs;
Nodejs響應請求,校驗cookie中的密文和頭信息中的隨機數是否匹配;
這里依舊有個細節值得提一下:Nodejs的上層一般是nginx,而nginx默認會過濾頭信息中不合法的字段(比如頭信息字段名包含“_”的),這里在寫頭信息的時候需要注意。
上文也提到,通過cookie及http頭信息傳遞加密token會有很多弊端;有沒有更優雅的實現方案呢?
3、更優雅的架構
首先,我們明確前后端分離的一些基本原則:
后端(Java / PHP )職責:
服務層顆粒化接口,以便前端Nodejs層異步并發調用;
用戶狀態保存,實現用戶權限等各種功能;
前端(Nodejs + Javascript)職責:
Nodejs層完成路由托管及模板引擎渲染功能
Nodejs層不負責實現任何SESSION和數據庫功能
我們提到,前端Nodejs層不負責實現任何SESSION和數據庫功能,但有沒有可能把后端緩存系統做成公共服務提供給Nodejs層使用呢?想想感覺前端整條路都亮了有木有?!這里先挖一個坑,后續慢慢填。
4、延伸
這里再順便提一下,新架構下的XSS防御。
猶記得,在狼廠使用PHP的年代,經常被安全部門曝出各類XSS漏洞,然后就在smaty里添加各種escape濾鏡,但是添加之后發現竟然把原始數據也給轉義了。
當然,現在更多要歸功于各種MVVM單頁面應用:使得前端完全不需要通過讀取URL中的參數來控制VIEW。
不過,還有一點值得一提:前后端分離框架下,路由由Nodejs控制;我自己要獲取的后端參數和需要用在業務邏輯的參數,在主觀上前端同學更好把握一些。
所以, 在koa(服務端) + Vue.js(前端)架構下基本不用顧慮XSS問題(至少不會被全安組追著問XSS漏洞啥時候修復)。
總結
要不學PHP、看Java、玩Python做全棧好了?