原文:10 Things You Should Know about Tokens
序言
幾周之前,我們發(fā)表了一篇針對AngularJs應(yīng)用的文章:單頁應(yīng)用中cookies和tokens對比。這樣的主題在社區(qū)反應(yīng)良好,所以我們發(fā)表了第二篇文章:類似socket.io的實時框架中的基于Token的認證。這篇文章同樣大受歡迎,所以我們決定繼續(xù)寫一篇文章,以此探索基于Token的認證中的一些最常見問題的更多細節(jié)。所以,就有了這篇文章。
1 Tokens 應(yīng)該被存儲在local/session storage或者cookies中
在應(yīng)用tokens的單頁應(yīng)用中,有人就遇到過這樣的問題:刷新瀏覽器后,怎么處理tokens?答案很簡單:你必須將tokens存儲在某個位置:session storage、local storage或者客戶端的 cookies。當瀏覽器不支持session storage 的時候,絕大部分的session storage的實現(xiàn)會依賴于cookies。
你可以會產(chǎn)生這樣的疑問:如果我在cookies中存儲了Token,那我豈不是又回到了原點?實際上并非如此,這種情形下,你只是使用cookies來實現(xiàn)存儲機制,而非認證機制(例如此時cookie并沒有被web框架用來認證一個用戶,因此不會帶來XSRF攻擊問題)。
2 Tokens也會像cookies一樣過期,但你有更多的控制權(quán)
Tokens有過期時間(在JWT中用exp屬性表示),否則就可以一次登錄永久認證了。出于同樣的原因,cookies也有過期時間。
在cookies的范疇內(nèi),有以下幾種選擇來控制cookie的生命周期:
- (1)當瀏覽器關(guān)閉時,session cookies會被銷毀;
- (2)另外你可以實現(xiàn)服務(wù)端的檢查(通常你使用的web框架會為你完成),并且可以設(shè)置過期時間。
- (3)通過設(shè)置過期時間,cookies可以持久化(關(guān)閉瀏覽器后仍不銷毀)
在tokens的范疇內(nèi),當token過期后,你只需要得到一個新的token即可。你可以在某個節(jié)點刷新token:
- (1)校驗舊的token
- (2)檢查用戶是否依然存在、授權(quán)有沒有被收回等一切對你的應(yīng)用有意義的事情
- (3)生成一個帶有新的過期時間的新的token
你甚至可以在token中存儲原始的生成時間,并在兩周后強制用戶重新登錄。
app.post('/refresh_token', function (req, res) {
// verify the existing token
var profile = jwt.verify(req.body.token, secret);
// if more than 14 days old, force login
if (profile.original_iat - new Date() > 14) { // iat == issued at
return res.send(401); // re-logging
}
// check if the user still exists or if authorization hasn't been revoked
if (!valid) return res.send(401); // re-logging
// issue a new token
var refreshed_token = jwt.sign(profile, secret, { expiresInMinutes: 60*5 });
res.json({ token: refreshed_token });
});
如果你想使一個過期時間很長的token失效,你需要類似注冊表的東西來再次驗證已發(fā)布的tokens。
3 Local/session storage無法跨域工作,請使用標記cookie
如果你設(shè)置一個cookie的作用域為.yourdomain.com
,那么這個cookie將在yourdomain.com
和app.yourdomain.com
中都有效。這樣的話,假如用戶已經(jīng)在yourdomain.com
登錄,你可以很容易通過redirect的方式使該用戶進入app.yourdomain.com
。
然而,存儲在local/session storage的tokens并不支持跨域訪問,即使是該域名下的子域名也不可以。這時你該怎么辦呢?
一種可能的方案是,當用戶在app.yourdomain.com
認證時,你生成一個token,并在yourdomain.com
設(shè)置一個cookie。
$.post('/authenticate', function() {
// store token on local/session storage or cookie
....
// create a cookie signaling that user is logged in
$.cookie('loggedin', profile.name, '.yourdomain.com');
});
然后,在youromdain.com
中你可以檢查該cookie是否存在,如果該cookie存在就通過redirect的方式讓用戶進入app.yourdomain.com
。這個token在應(yīng)用的子域名中是可用的,通過子域認證后,就可以正常使用token了(如果token此時仍然有效,就使用該token,如果失效了,就生成新的,直到最后一次登錄超過了你設(shè)置的閾)。
當然,這樣有可能發(fā)生一種情況:cookie存在,但token被刪除了或者其他事情發(fā)生了。在這種情形下,用戶就必須重新登錄了。我想說的重點是,正如之前所說,我們使用cookie并不是進行認證,而僅僅因為cookie的存儲機制支持跨域訪問時存儲信息,我們用cookie進行存儲tokens而已。
4 每一個跨域請求都將進行preflight請求
有人指出,Authorization header并不是一個簡單的header,因此對于特定的urls必須要進行一次 pre-flight請求。
OPTIONS https://api.foo.com/bar
GET https://api.foo.com/bar
Authorization: Bearer ....
OPTIONS https://api.foo.com/bar2
GET https://api.foo.com/bar2
Authorization: Bearer ....
GET https://api.foo.com/bar
Authorization: Bearer ....
只有在設(shè)置了類似Content-Type: application/json
的請求頭時才會這么干。
但對于絕不部分應(yīng)用來說,這種情況已經(jīng)非常普遍。
需要注意的是,OPTIONS請求本身并沒有Authorization header,所以你的web框架應(yīng)該區(qū)別對待OPTIONS請求和后續(xù)的請求(提示:Microsoft IIS由于某些原因會出現(xiàn)一些問題)。
5 當你需要stream something時,請使用token來得到一個簽名的請求
使用cookie的時候,你可以很容易地觸發(fā)文件下載并傳送一些內(nèi)容。但是,在tokens的世界中,由于請求是通過XHR來完成,所以你不能指望它。解決的辦法就是,像AWS那樣生成一個簽名請求。 Hawk Bewits是一個實現(xiàn)該功能的非常好的框架。
REQUEST:
POST /download-file/123
Authorization: Bearer...
RESPONSE:
ticket=lahdoiasdhoiwdowijaksjdoaisdjoasidja
這里的ticket是無狀態(tài)的,它是基于URL( host + path + query + headers + timestamp + HMAC)生成的,并且有過期時間。所以它可以用來在接下來的,比如5分鐘,來下載文件。
然后你redirect到/download-file/123?ticket=lahdoiasdhoiwdowijaksjdoaisdjoasidja
。服務(wù)器將檢查該ticket的有效性并繼續(xù)照常工作。
6 處理XSS比處理XSRF更容易
Cookies有這樣的一個特性:在服務(wù)器端為cookie設(shè)置HttpOnly標識,這樣Cookies就只能通過服務(wù)器
訪問,而不能通過JavaScript訪問(譯者注:HttpOnly屬性可以阻止客戶端腳本訪問Cookie)。這一點非常有用:可以阻止通過XSS攻擊來獲取cookie中的內(nèi)容。
由于tokens存儲在local/session storage或者 cookie中,因此token可以通過XSS攻擊來獲取。這是一個值得警惕的地方。鑒于此,你應(yīng)該把token的過期時間設(shè)置地盡量短。
但是如果你考慮到cookies的攻擊面,其中一個主要的就是XSRF。現(xiàn)實是,XSRF是被誤解甚多的攻擊,大部分開發(fā)者可能并沒有理解這種風(fēng)險,所以很多應(yīng)用缺少防XSRF攻擊的策略。但是,每個人都知道注入是什么。簡單來講,如果你允許在你的網(wǎng)站輸入但沒有對輸入內(nèi)容進行轉(zhuǎn)義,你將面臨XSS攻擊。根據(jù)我們的經(jīng)驗,防范XSS攻擊比防范XSRF攻擊要容易一些。另外,并不是每個web框架都建立了防范XSRF的機制。而XSS攻擊可以很容易地通過大部分的模板引擎的默認的轉(zhuǎn)義語法進行防范。
7 每次請求都攜帶token,請留意它的大小
每次你寫一個API的請求,都會在Authorization header中發(fā)送token:
GET /foo
Authorization: Bearer ...2kb token...
vs
GET /foo
connect.sid: ...20 bytes cookie...
取決于你在token中存放多少信息,token可能會很大。而cookies通常只會包含一個身份信息(connect.sid,PHPSESSID等),其他內(nèi)容則存放在服務(wù)器中(如果只有一個服務(wù)器則是在內(nèi)存中,如果是服務(wù)器群則是在數(shù)據(jù)庫中)。
現(xiàn)在你可以隨意實現(xiàn)token機制。token中包含最基本的信息,在服務(wù)器端你可以在每個API請求中用更多數(shù)據(jù)擴充它。這正是cookie做的事情,不同之處在于,你對token可以進行有完全的控制權(quán),可以有意識地決定存放哪些數(shù)據(jù),它已經(jīng)是你代碼的一部分了。
GET /foo
Authorization: Bearer ……500 bytes token….
然后在服務(wù)端:
app.use('/api',
// validate token first
expressJwt({secret: secret}),
// enrich req.user with more data from db
function(req, res, next) {
req.user.extra_data = get_from_db();
next();
});
```
值得一提的是,你也可以將session完全存儲在cookie中(而不是僅僅只存儲一個身份)。有些web平臺支持這樣做,有些則不支持。例如,在node.js中你可以使用[mozilla/node-client-sessions](https://github.com/mozilla/node-client-sessions)。
### 8 如果你在token中存放私密信息,請對token加密
token簽名可以阻止篡改它的內(nèi)容。TLS/SSL可以阻止中間人攻擊。但是如果payload包含用戶敏感信息(像SSN等),你也可以對敏感信息加密。對于JWT而言意味著實現(xiàn)JWE規(guī)范,但大部分的依賴庫都還沒有實現(xiàn)JWE標準。所以,最簡單的就是,像下面這樣使用AES-CBC加密:
```
app.post('/authenticate', function (req, res) {
// validate user
// encrypt profile
var encrypted = { token: encryptAesSha256('shhhh', JSON.stringify(profile)) };
// sing the token
var token = jwt.sign(encrypted, secret, { expiresInMinutes: 60*5 });
res.json({ token: token });
}
function encryptAesSha256 (password, textToEncrypt) {
var cipher = crypto.createCipher('aes-256-cbc', password);
var crypted = cipher.update(textToEncrypt, 'utf8', 'hex');
crypted += cipher.final('hex');
return crypted;
}
```
當然你也可以像第7條指出的那樣,將私密信息放在數(shù)據(jù)庫中。
更新:[Pedro Felix](https://twitter.com/pmhsfelix) 正確地指出:MAC-then-encrypt對于 [Vaudenay-style attacks](http://www.thoughtcrime.org/blog/the-cryptographic-doom-principle/)是非常脆弱的。我已經(jīng)更新了encrypt-then-MAC代碼。
### 9 JWT可以用在OAuth中:Bearer Token
Tokens常常和OAuth聯(lián)系在一起。OAuth2是一種用來解決身份認證問題的授權(quán)協(xié)議。在OAuth2中,用戶首先會被提示同意讀取他/她的數(shù)據(jù),然后授權(quán)服務(wù)器會返回一個access_token,它可以代表該用戶的身份去調(diào)用APIs。
通常來講,這些tokens都是不透明的,被稱為bearer tokens。bearer tokens是一種隨機字符串,這些字符串會與過期時間、請求的范圍(如:好友列表)以及當前授權(quán)用戶信息一起,以哈希表的方式存儲在服務(wù)器(數(shù)據(jù)庫、緩存等)中。之后,當API被調(diào)用的時候,token一同被發(fā)送,服務(wù)器就會在哈希表中查找,以此判斷授權(quán)信息(token是否過期?當前token是否對想要調(diào)用的API有足夠的作用范圍?)。
這些tokens與我們之前討論的普通tokens的主要區(qū)別在于,簽名的tokens(像JWT)是無狀態(tài)的,它們無需存儲在哈希表中,因此是一種更輕量級的方案。OAuth2并沒有規(guī)定access_token的格式,所以你可以從授權(quán)服務(wù)器返回一個包含作用域/權(quán)限、過期時間的JWT。
### 10 Tokens并非靈丹妙藥,請慎重考慮的授權(quán)用例。
幾年前,我們幫助某大型公司實現(xiàn)了一套基于token的架構(gòu)。這是一家擁有10萬+員工、大量信息需要被保護的公司。他們想對“認證和授權(quán)”建立一個集中的、全機構(gòu)的倉庫。想象一個這樣的用例:用戶X可以獲取位于國家W的醫(yī)院Z的臨床試驗Y的id和name。你可以想象,這種細粒度的授權(quán)機制,無論從技術(shù)上還是行政上,都會很快變得無法管理。
* Tokens可以變得很大
* 你的apps/APIs變得更復(fù)雜
* 無論是誰來負責(zé)授予權(quán)限這個工作,他管理起來都會很痛苦
最終我們致力于信息架構(gòu)上,以此確保生成合理的范圍和權(quán)限。結(jié)論:抵抗住將所有東西都放在tokens中的誘惑,在決定從頭到尾用token方案之前做一些分析和調(diào)整。
**免責(zé)聲明:**在面對安全問題的時候,請確保你盡力做好每件事。這里的代碼和建議僅僅作為參考。
### 原文概念補充
**Polyfill**:用于實現(xiàn)瀏覽器并不支持的原生API的代碼。
**XSS** : `XSS`攻擊通常指的是通過利用網(wǎng)頁開發(fā)時留下的漏洞,通過巧妙的方法注入惡意指令代碼到網(wǎng)頁,使用戶加載并執(zhí)行攻擊者惡意制造的網(wǎng)頁程序。這些惡意網(wǎng)頁程序通常是[JavaScript](https://zh.wikipedia.org/wiki/JavaScript),但實際上也可以包括[Java](https://zh.wikipedia.org/wiki/Java),[VBScript](https://zh.wikipedia.org/wiki/VBScript),[ActiveX](https://zh.wikipedia.org/wiki/ActiveX),[Flash](https://zh.wikipedia.org/wiki/Flash)或者甚至是普通的[HTML](https://zh.wikipedia.org/wiki/HTML)。攻擊成功后,攻擊者可能得到更高的權(quán)限(如執(zhí)行一些操作)、私密網(wǎng)頁內(nèi)容、[會話](https://zh.wikipedia.org/wiki/%E4%BC%9A%E8%AF%9D)和[cookie](https://zh.wikipedia.org/wiki/Cookie)等各種內(nèi)容。
**XSRF**:跨站請求偽造(`Cross-site request forgery`),也被稱為`one-click attack `或者`session riding`,通常縮寫為`CSRF` 或者`XSRF`, 是一種挾制用戶在當前已登錄的Web應(yīng)用程序上執(zhí)行非本意的操作的攻擊方法。跟[跨網(wǎng)站腳本](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%B6%B2%E7%AB%99%E6%8C%87%E4%BB%A4%E7%A2%BC)(XSS)相比,**XSS** 利用的是用戶對指定網(wǎng)站的信任,CSRF 利用的是網(wǎng)站對用戶網(wǎng)頁瀏覽器的信任。簡單地說,是攻擊者通過一些技術(shù)手段欺騙用戶的瀏覽器去訪問一個自己曾經(jīng)認證過的網(wǎng)站并執(zhí)行一些操作(如發(fā)郵件,發(fā)消息,甚至財產(chǎn)操作如轉(zhuǎn)賬和購買商品)。由于瀏覽器曾經(jīng)認證過,所以被訪問的網(wǎng)站會認為是真正的用戶操作而去執(zhí)行。這利用了web中用戶身份驗證的一個漏洞:簡單的身份驗證只能保證請求發(fā)自某個用戶的瀏覽器,卻不能保證請求本身是用戶自愿發(fā)出的。
**man in the middle attacks**:在[密碼學(xué)](https://zh.wikipedia.org/wiki/%E5%AF%86%E7%A0%81%E5%AD%A6)和[計算機安全](https://zh.wikipedia.org/wiki/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%AE%89%E5%85%A8)領(lǐng)域中,中間人攻擊(`Man-in-the-middle attack`,縮寫:`MITM`)是指攻擊者與通訊的兩端分別創(chuàng)建獨立的聯(lián)系,并交換其所收到的數(shù)據(jù),使通訊的兩端認為他們正在通過一個私密的連接與對方直接對話,但事實上整個會話都被攻擊者完全控制。在中間人攻擊中,攻擊者可以攔截通訊雙方的通話并插入新的內(nèi)容。在許多情況下這是很簡單的(例如,在一個未加密的[Wi-Fi](https://zh.wikipedia.org/wiki/Wi-Fi) [無線接入點](https://zh.wikipedia.org/wiki/%E6%97%A0%E7%BA%BF%E6%8E%A5%E5%85%A5%E7%82%B9)的接受范圍內(nèi)的中間人攻擊者,可以將自己作為一個中間人插入這個網(wǎng)絡(luò))。
一個中間人攻擊能成功的前提條件是攻擊者能將自己偽裝成每一個參與會話的終端,并且不被其他終端識破。中間人攻擊是一個(缺乏)相互[認證](https://zh.wikipedia.org/wiki/%E8%AE%A4%E8%AF%81)的攻擊。大多數(shù)的加密協(xié)議都專門加入了一些特殊的認證方法以阻止中間人攻擊。例如,[SSL](https://zh.wikipedia.org/wiki/SSL)協(xié)議可以驗證參與通訊的一方或雙方使用的證書是否是由權(quán)威的受信任的[數(shù)字證書認證機構(gòu)](https://zh.wikipedia.org/wiki/%E6%95%B0%E5%AD%97%E8%AF%81%E4%B9%A6%E8%AE%A4%E8%AF%81%E6%9C%BA%E6%9E%84)頒發(fā),并且能執(zhí)行雙向身份認證。
菜鳥一枚,不當之處,歡迎指正。