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