[Web 安全] 如何經過JWT防護CSRF

名字都是用來唬人的。javascript

先解釋兩個名詞,CSRF 和 JWT。html

CSRF (Cross Site Request Forgery),它講的是你在一個瀏覽器中打開了兩個標籤頁,其中一個頁面經過竊取另外一個頁面的 cookie 來發送僞造的請求,由於 cookie 是隨着請求自動發送到服務端的。前端

JWT (JSON Web Token),經過某種算法將兩個 JSON 對象加密成一個字符串,該字符串能表明惟一用戶。java

CSRF 的產生

首先經過一個圖來理解 CSRF 是什麼現象。nginx

CSRF

想要攻擊成功,這三步缺一不可。web

  • 第一,登陸受害者網站。若是受害者網站是基於 cookie 的用戶驗證機制,那麼當用戶登陸成功後,瀏覽器就會保存一份服務端的 SESSIONID。算法

  • 第二,這時候在同一個瀏覽器打開攻擊者網站,雖說它沒法獲取 SESSIONID 是什麼(由於設置了 http only 的 cookie 是沒法被 JavaScript 獲取的),可是從瀏覽器向受害者網站發出的任何請求中,都會攜帶它的 cookie,不管是從哪一個網站發出。數據庫

  • 第三,利用這個原理,在攻擊者網站發出一個請求,命令受害者網站進行一些敏感操做。因爲此時發出的請求是處於 session 中的,因此只要該用戶有權限,那麼任何請求都會被執行。express

好比,打開優酷,並登陸。再打開攻擊者網站,它裏面有個 <img> 標籤是這樣的:npm

<img src="http://api.youku.com/follow/123" />

這個 api 只是個例子,具體的 url 和參數均可以經過瀏覽器的開發者工具(Network 功能)事先肯定。假如它的做用是讓該登陸的用戶關注由 123 肯定的一個節目或者用戶,那麼經過 CSRF 攻擊,這個節目的關注量就會不斷上升。

解釋兩點。第一,爲何舉這個例子,而不是銀行這種和金錢有關的操做?很簡單,由於它容易猜。對於攻擊者來講,沒有什麼是必定能成功的,好比 SQL 注入,攻擊者他不知道某網站的數據庫是怎麼設計的,可是他通常會經過我的經驗去嘗試,好比不少網站把用戶的主鍵設置爲 user_id,或 sys_id 等。

銀行的操做每每通過多重確認,好比圖形驗證碼、手機驗證碼等,光靠 CSRF 完成一次攻擊基本上是天方夜譚。但其餘類型的網站每每不會刻意去防範這些問題。雖然金錢上的利益很可貴到,但 CSRF 能辦到的事情仍是不少,好比利用別人發虛假微博、加好友等,這些都能對攻擊者產生利益。

第二,如何確保用戶打開優酷以後,又打開攻擊者網站?作不到。不然任何人打開優酷以後,都會莫名其妙地去關注某個節目了。可是你要知道,這個攻擊成本僅僅是一條 API 調用而已,它在哪裏都能出現,你從任何地方下載一張圖片,讓你請求這個地址,看也不看就點肯定,請求不就發出去了嗎?

CSRF 的防護

對於如何防範 CSRF,通常有三種手段。

判斷請求頭中的 Referer

這個字段記錄的是請求的來源。好比 http://www.example.com 上調用了百度的接口 http://api.map.baidu.com/service 那麼在百度的服務端,就能夠經過 Referer 判斷這個請求是來自哪裏。

在實際應用中,這些跟業務邏輯無關的操做每每會放在攔截器中(或者說過濾器,不一樣技術使用的名詞可能不一樣)。意思是說,在進入到業務邏輯以前,就應該要根據 Referer 的值來決定這個請求能不能處理。

在 Java Servlet 中能夠用 Filter(古老的技術);用 Spring 的話能夠建攔截器;在 Express 中是叫中間件,經過 request.get('referer') 來取得這個值。每種技術它走的流程其實都同樣。

但要注意的是,Referer 是瀏覽器設置的,在瀏覽器兼容性大不相同的時代中,若是存在某種瀏覽器容許用戶修改這個值,那麼 CSRF 漏洞依然存在。

在請求參數中加入 csrf token

討論 GET 和 POST 兩種請求,對於 GET,其實也沒什麼須要防範的。爲何?由於 GET 在「約定」當中,被認爲是查詢操做,查詢的意思就是,你查一次,查兩次,無數次,結果都不會改變(用戶獲得的數據可能會變),這不會對數據庫形成任何影響,因此不須要加其餘額外的參數。

因此這裏要提醒各位的是,儘可能聽從這些約定,不要在 GET 請求中出現 /delete, /update, /edit 這種單詞。把「寫」操做放到 POST 中。

對於 POST,服務端在建立表單的時候能夠加一個隱藏字段,也是經過某種加密算法獲得的。在處理請求時,驗證這個字段是否合法,若是合法就繼續處理,不然就認爲是惡意操做。

<form method="post" action="/delete">
  <!-- 其餘字段 -->
  <input type="hidden" name="csrftoken" value="由服務端生成"/>
</form>

這個 html 片斷由服務端生成,好比 JSP,PHP 等,對於 Node.js 的話能夠是 Jade 。

這的確是一個很好的防範措施,再增長一些處理的話,還能防止表單重複提交。

但是對於一些新興網站,不少都採用了「單頁」的設計,或者退一步,不管是否是單頁,它的 HTML 多是由 JavaScript 拼接而成,而且表單也都是異步提交。因此這個辦法有它的應用場景,也有侷限性。

新增 HTTP Header

思想是,將 token 放在請求頭中,服務端能夠像獲取 Referer 同樣獲取這個請求頭,不一樣的是,這個 token 是由服務端生成的,因此攻擊者他沒辦法猜。

這篇文章的另外一個重點——JWT——就是基於這個方式。拋開 JWT 不談,它的工做原理是這樣的:

JWT

解釋一下這四個請求,類型都是 POST 。

  1. 經過 /login 接口,用戶登陸,服務端傳回一個 access_token,前端把它保存起來,能夠是內存當中,若是你但願用來模擬 session 的話。也能夠保存到 localStorage 中,這樣能夠實現自動登陸。

  2. 調用 /delete 接口,參數是某樣商品的 id。仔細看,在這個請求中,多了一個名爲 Authoriaztion 的 header,它的值是以前從服務端傳回來的 access_token,在前面加了一個「Bearer」(這是和服務端的約定,約定就是說,說好了加就一塊兒加,不加就都不加……)

  3. 調用 /logout 接口,一樣把 access_token 加在 header 中傳過去。成功以後,服務端和前端都會把這個 token 置爲失效,或直接刪除。

  4. 再調用 /delete 接口,因爲此時已經沒有 access_token 了,因此服務端判斷該請求沒權限,返回 401 。

各位有沒有發現,從頭到尾,整個過程沒有涉及 cookie,因此 CSRF 是不可能發生的!

關於 JWT 的約定

若是不關心 JWT,那文章徹底能夠結束了,由於看到這裏,除了章節標題提到的內容以外,各位還能夠引伸出幾點:第一,在設計 API 時多斟酌一下;第二,利用 token 作單點登陸;第三,cookie 和 token 這兩種用戶驗證機制的不一樣。

而 JWT,其實就是對新增的 HTTP Header 的約定。就好比 GET 請求中的參數,約定了用 & 分隔,可是用別的能夠嗎?固然能夠,你用 逗號 或者 分號 也行啊,服務端再規定一個轉義的規則就好了。只不過,約定是爲了讓全部人更規範地作事情,若是按照約定行事的話,那從一個工具換到另外一個工具,本身須要改的代碼就不多。這裏就不深刻談了。

三個組成部分

這個網站 對 JWT 的術語和內容有最官方的說明。

JWT 的每一個部分都是字符串,由 點 分隔,因此它的格式是這樣的:

XXX1.XXX2.XXX3

整個字符串是 URL-safe 的,因此能夠直接用在 GET 請求的參數中。

第一部分 JWT Header

它是一個 JSON 對象,表示這個整個字符串的類型和加密算法,好比

{
  "typ":"JWT",
  "alg":"HS256"
}

通過 base64url 加密以後變成

eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9

第二部分 JWT Claims Set

它也是一個 JSON 對象,能惟一表示一個用戶,好比

{
  "iss": "123",
  "exp": 1441593850
}

通過 base64url 加密以後變成

eyJpc3MiOiIxMjMiLCJleHAiOjE0NDE1OTM4NTB9

在官網有詳細的屬性說明,儘可能使用裏面提到的 Registered Claim Names,這樣能夠提升閱讀性。這裏的 iss 表示 issuer,就是發起請求的人,它的值是跟業務相關的,因此由你的應用去決定。exp 表示 expiration time,即何時過時,注意,這個值是秒數,而不是毫秒數,因此是在整型範圍內的。

第三部分 JWS Signature

這個簽名的計算跟第一部分中的 alg 屬性有關,假如是 HS256,那麼服務端須要保存一個私鑰,好比 secret 。而後,把第一部分和第二部分生成的兩個字符串用 點 鏈接以後,再結合私鑰,用 HS256 加密能夠獲得以下字符串

AOtbon6CebgO4WO9iJ4r6ASUl1pACYUetSIww-GQ72w

如今就集齊三個部分了,用 . 鏈接,獲得完整的 token 。

例子 1/2:以 Express 做爲服務端

對於服務端來講,已經存在各類庫去支持 JWT 了,推薦幾個以下:

平臺
Java maven com.auth0 / java-jwt / 0.4
PHP composer require lcobucci/jwt
Ruby gem install jwt
.NET Install-Package System.IdentityModel.Tokens.Jwt
Node.js npm install jsonwebtoken

若是以前有 Node.js 和 Express 的學習經歷的話,那對下面的代碼應該很容易理解。

var express = require('express'),
    jwt     = require('jsonwebtoken');

var router      = express.Router(),
    PRIVATE_KEY = 'secret';

router.post('/login', function(req, res, next) {

    // 生成 JWT
    var token = jwt.sign({
        iss: '123'
    }, PRIVATE_KEY, {
        expiresInMinutes: 60
    });

    // 將 JWT 返回給前端
    res.send({
      access_token: token
    });
});

router.post('/delete', function(req, res, next) {
    var auth    = req.get('Authorization'),
        token   = null;

    // 判斷請求頭中是否有 Authoriaztion 字段,爲了縮短代碼就減小了別的驗證
    if (auth) {
        token = /Bearer (.+)/.exec(auth)[1];
        res.send(jwt.decode(token));
    } else {
        res.sendStatus(401);
    }
});

關於 jsonwebtoken 的使用能夠看它的手冊

例子中定義了兩個 API。

  • /login,會返回一個 JWT 字符串。其中包含了一個用戶 id,和存活時間,這個時間會被轉換成 exp 和 iat (issue at, 發起請求的時間),二者之差就是存活時間。

  • /delete,驗證請求頭中是否有 Authorization 字段,而且是否合法,若是是的話就處理請求,不然返回 401 。

注意一下,服務端期待的 Authoriaztion 請求頭是這樣的格式:

Authorization: Bearer XXX1.XXX2.XXX3

這個跟 JWT 無關,是 OAuth 2.0 的一種格式。由於 Authorization 這個字段也是約定的,它由 token 的類型和值組成,類型除了上文提到的 Bearer,還有 Basic、MAC 等。

例子 2/2:以 Backbone 做爲前端

前端的工做分兩方面,一是存儲 jwt,二是在全部的請求頭中增長 Authoriaztion 。

若是是重構已有的代碼,第二個工做可能有點難度,除非舊代碼中的表單都是異步提交,而且請求的方法是本身包裝過的,由於只有這樣纔有機會去修改請求頭。

在若干星期以前的這篇文章中,寫了怎麼在 Angular 中攔截請求。如今就以 Backbone 爲例。

// 先保存原始的 sync 方法
var sync = Backbone.sync;

Backbone.sync = function (method, model, options) {

  var token = window.localStorage.getItem('jwt');

  // 若是存在 token,就把它加到請求頭中
  if (token) {
    options.headers.Authorization = 'Bearer ' + token;
  }

  // 調用原始的 sync 方法
  sync(method, model, options);
};

對跨域的額外處理

在跨域的應用場景中,須要服務端作一些額外的設置,這些設置是加在響應頭上的。

Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Authorization

第一個表示容許來自任何域名的請求。第二個表示容許一些自定義的請求頭,由於 Authoriaztion 是自定義的,因此必須加上這個配置,若是各位使用了其餘的請求頭,請一樣加上。

若是服務端用了 nginx,那麼這些配置能夠寫在 nginx.conf 文件中。若是是在代碼中配置,那麼不管是 Java,仍是 Node.js,都有 response.setHeader 方法。

小結

我對 Web 安全方面的瞭解還不太深,因此沒有太多經驗可談。安全性是一個在日常不太受重視的領域,由於完成一個項目的優先級歷來都是:功能 > 顏值 > 性能, 安全 。至少得保證用戶在使用過程當中不會出錯,而後再作得酷炫或清新一點,性能和安全只有在知足了前兩項,或者迫在眉睫的時候纔去考慮。當服務器承受不了那麼高的負載了,纔會去增長更多的服務器,但業務功能從一開始就不能少。

但是這樣作有錯嗎?並無吧。在特定的場景,作特定的處理,或許是性價比最高的決策了。

這篇文章中反覆提到的一個詞是「約定」,它貌似和「具體狀況具體分析」這個觀點矛盾了,額……。

約定是人與人之間的共識,好比說 GET 請求,那麼對方的第一反應就是查詢,當有人破壞約定,用 GET 請求去作刪除操做時,就會讓別人很難理解(當有一大堆人這麼作的時候,就不難理解了吧……)。或者當咱們提到 JWT 的時候,那它就應該是由三個部分組成,若是有人僅僅是按照本身的算法來生成一個 token,一樣能夠惟一標識用戶,那他必須得像共事的人解釋,這個算法的安全性、使用方法等。

另外一方面,若是真心以爲按照「約定」辦事不必,太麻煩,而且能夠接受「耍小聰明」的後果的話,那就按本身的想法去作吧(真的再也不考慮一下了嗎)。

爲何 HTML5 新增了那麼多語義化的標籤,是由於一切都在朝着更規範的方向走。

相關文章
相關標籤/搜索