关于 Token,你应该知道的十件事
轉(zhuǎn)自:http://ju.outofmemory.cn/entry/134189
原文是一篇很好的講述 Token 在 Web 應(yīng)用中使用的文章,而這是我和Special合作翻譯的譯文。
1. Token 應(yīng)該被保存起來(lái)(放到 local / session stograge 或者 cookies)
在單頁(yè)應(yīng)用程序中,有些用戶刷新瀏覽器后會(huì)帶來(lái)一些跟 token 相關(guān)的問(wèn)題。而解決方法很簡(jiǎn)單:你應(yīng)該把 token 保存到起來(lái):放到 session storage, local storage 或者是客戶端的 cookie 里。而瀏覽器不支持 session storage 時(shí)都應(yīng)該轉(zhuǎn)存到 cookies 里。
如果你想“我把 token 保存到 cookie ,不就跟以前沒(méi)有任何分別?”。可是在這種情況下你只是把 cookie 當(dāng)作一個(gè)儲(chǔ)存機(jī)制,而不是一種驗(yàn)證機(jī)制。(比如說(shuō),這個(gè) cookie 不會(huì)被 Web 框架用于用戶驗(yàn)證,所以沒(méi)有 XSRF 攻擊的危險(xiǎn))。
2. Tokens 除了像 cookie 一樣有有效期,而且你可以有更多的操作方法
Tokens 應(yīng)該有一個(gè)有效期(在JSON Web Tokens中是作為exp屬性),否則其他人只要登錄過(guò)一次就可以永遠(yuǎn)地通過(guò) API 的驗(yàn)證。Cookies 基于同樣的理由也有一個(gè)有效期。
在 Cookies 的使用中,有不同的選項(xiàng)可以控制 cookie 的生命周期:
. cookies 可以在瀏覽器關(guān)閉后刪除(session cookies);
. 另外你可以實(shí)現(xiàn)服務(wù)器端的檢查(通常由你使用的 Web 框架完成),還有也可以實(shí)現(xiàn)絕對(duì)有效期或彈性有效期(sliding window expiration);
. Cookies 可以帶有有效期地保存起來(lái)(瀏覽器關(guān)閉后也不刪除)。
而在 tokens 的使用中,一旦 token 過(guò)期,只需要重新獲取一個(gè)。你可以使用一個(gè)接口去刷新 token:
. 讓舊的 token 失效;
. 檢查這個(gè)用戶是不是還存在,權(quán)限是否被取消或者任何對(duì)你的程序來(lái)說(shuō)是有必要的;
. 得到一個(gè)更新了有效期的 token。
你甚至可以把 token 原來(lái)的發(fā)布時(shí)間也保存起來(lái),并且強(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 });
});
如果你需要撤回 tokens(當(dāng) token 的生存期比較長(zhǎng)的時(shí)候這很有必要)那么你需要一個(gè) token 的生成管理器去作檢查。
3. Local / session storage 不會(huì)跨域工作,請(qǐng)使用一個(gè)標(biāo)記 cookie
如果你設(shè)置一個(gè) cookie 的域名為.yourdomain.com它將可以被youdomain.com和app.yourdomain.com獲取,這樣用戶登錄并且轉(zhuǎn)到app.yourdomain.com后也能很容易地從主域名找回這個(gè) cookie(假如你的是電商網(wǎng)站)。
而另一方面,保存在 local / session storage 的 tokens,就不能從不同的域名中讀取(甚至是子域名也不行)。那你能怎么做?
一個(gè)可能的選擇是,當(dāng)用戶通過(guò)app.yourdomain.com上面的驗(yàn)證時(shí)你生成一個(gè) token 并且作為一個(gè) cookie 保存到.yourdomain.com
$.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中你可以檢查這個(gè) cookie 是不是已經(jīng)存在了,并且如果存在的話就轉(zhuǎn)到app.youromdain.com去。從這以后,這個(gè) token 將會(huì)對(duì)程序的子域名以及之后通常的流程都有效(直到這個(gè) token 超過(guò)有效期)。
不過(guò)這將會(huì)導(dǎo)致 cookie 存在但 token 被刪除了或其他意外情況的發(fā)生。在這種情況下,用戶將不得不重新登錄。但重要的是,像我們之前說(shuō)的,我們不會(huì)這個(gè)用 cookie 作為驗(yàn)證方法,只是作為一個(gè)存儲(chǔ)機(jī)制去支持存儲(chǔ)信息在不同的域名中。
4. 每個(gè) CORS(跨域資源共享)請(qǐng)求都會(huì)帶上預(yù)請(qǐng)求(Preflight request)
有些人指出 Authorization header 不是一個(gè)simple header,因此對(duì)于一個(gè)特定的 URLs 的所有請(qǐng)求都會(huì)帶上一個(gè)預(yù)請(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 ....
但這只會(huì)發(fā)生在你發(fā)送Content-Type:application/json時(shí)。不過(guò)這說(shuō)明已經(jīng)出現(xiàn)在絕大多數(shù)的程序中了。
一個(gè)小小的警告,theOPTIONS請(qǐng)求不會(huì)帶有 Authorization header 自身,所以你的網(wǎng)絡(luò)框架應(yīng)該支持區(qū)別對(duì)待OPTISON和后來(lái)的請(qǐng)求。(微軟的 IIS 因?yàn)槟承┰蚝孟駮?huì)有問(wèn)題)。
5. 當(dāng)你需要流傳送某些東西,請(qǐng)用 token 去獲取一個(gè)已簽名的請(qǐng)求。
當(dāng)使用 cookies 時(shí),你可以很容易開始一個(gè)文件的下載或流傳送內(nèi)容。然而,在 tokens 的使用中,請(qǐng)求是通過(guò) XHR 完成的,你不能依賴于它。而解決方法應(yīng)該是像 AWS 那樣通過(guò)生成一個(gè)簽名了的請(qǐng)求,例如,Hawk Bewits 是一個(gè)很好的框架去啟用它:
Request:
POST /download-file/123
Authorization: Bearer...
Response:
ticket=lahdoiasdhoiwdowijaksjdoaisdjoasidja
這個(gè) ticket 是無(wú)狀態(tài)并且是基于 URL 的:host + path + query + headers + timestamp + HMAC,并且有一個(gè)有效期。所以它可以用于像只能在5分鐘內(nèi)去下載一個(gè)文件。
你然后可以轉(zhuǎn)到/download-file/123?ticket=lahdoiasdhoiwdowijaksjdoaisdjoasidja中去。服務(wù)器就會(huì)檢查這個(gè) ticket 是不是有效然后像正常一樣開始下一步的服務(wù)。
6.XSS比XSRF要更容易防范
XSS 攻擊的原理是,攻擊者插入一段可執(zhí)行的 JavaScripts 腳本,該腳本會(huì)讀出用戶瀏覽器的 cookies 并將它傳輸給攻擊者,攻擊者得到用戶的 Cookies 后,即可冒充用戶。但是要防范 XSS 也很簡(jiǎn)單,在寫入 cookies 時(shí),將HttpOnly設(shè)置為true,客戶端 JavaScripts 就無(wú)法讀取該 cookies 的值,就可以有效防范 XSS 攻擊。因?yàn)?Tokens 也是儲(chǔ)存在本地的 session storage 或者是客戶端的 cookies 中,也是會(huì)受到 XSS 攻擊。所以在使用 tokens 的時(shí)候,必須要考慮過(guò)期機(jī)制,不然攻擊者就可以永久持有受害用戶帳號(hào)。
相比 XSS,XSRF 的危害性更大,因?yàn)榇蠖鄶?shù) Web 框架都已經(jīng)內(nèi)置了 XSS 防范機(jī)制(例如在 Ruby on Rails 中,用戶的輸入在輸出的時(shí)候都會(huì)做轉(zhuǎn)義操作,攻擊者插入的腳本就無(wú)法執(zhí)行),對(duì)于大部分開發(fā)者而言,甚至連 XSRF 都不知道是什么玩意,更別提防范了。XSRF 目前并不是每個(gè) Web 框架都有防范機(jī)制,因此開發(fā)者更應(yīng)該留意 XSRF 。
7. 注意 token 的大小
Token 機(jī)制在每次請(qǐng)求 API 的時(shí)候,都需要帶上一個(gè)Authorization的 Http Header 。
# Token
GET /foo
Authorization: Bearer ...2kb token...
# Cookie
GET /foo
connect.sid: ...20 bytes cookie...
Token 的大小其實(shí)由你儲(chǔ)存在 token 中的信息量所決定,例如可能有nickname,openid等開發(fā)者另外加上的信息。
但是 session cookies 機(jī)制只需要一個(gè)字串作為用戶標(biāo)識(shí)即可(例如 PHP 的 PHPSESSIONID),其中關(guān)于用戶的信息都會(huì)直接儲(chǔ)存到服務(wù)端的數(shù)據(jù)庫(kù)中,當(dāng)用戶請(qǐng)求時(shí)才從數(shù)據(jù)庫(kù)中撈出來(lái)用。
當(dāng)然 Token 機(jī)制也可以仿照 session cookies 機(jī)制這么做了,也是個(gè)有效控制 token 大小的方法。
Token 中只保留關(guān)鍵的幾條身份標(biāo)識(shí)信息,其余都放到數(shù)據(jù)庫(kù)里面了,權(quán)限控制的時(shí)候再撈出。這樣做的好處是,開發(fā)者可以完全掌控 token,因?yàn)殛P(guān)鍵信息都已經(jīng)是你代碼和數(shù)據(jù)庫(kù)中的一部分了,想怎么弄都可以了。
舉個(gè)例子:
GET /foo
Authorization: Bearer ……500 bytes token….
Then on the server:
app.use('/api',
// 首先檢查 token;
expressJwt({secret: secret}),
// 然后再?gòu)臄?shù)據(jù)庫(kù)中撈出用戶信息。
function(req, res, next) {
req.user.extra_data = get_from_db();
next();
});
另外值得一提的是,你也可以把東西都丟 Cookies 里面(而不是只丟個(gè)身份標(biāo)識(shí)字串)。只要確保資料經(jīng)過(guò)了嚴(yán)格的加密,攻擊者無(wú)法利用,現(xiàn)在有些 Web 框架已經(jīng)有類似機(jī)制,例如 Nodejs 的這個(gè)插件mozilla/node-client-sessions。
8. 有需要的話,要加密并且簽名 token
雖然 TLS/SSL 機(jī)制可以隔絕大多數(shù)中間人攻擊,但是如果 token 中帶有了用戶的敏感信息,開發(fā)者也應(yīng)該要加密這些信息。
使用 JWT(文中第 9 點(diǎn)) 可以加密 token,但是由于目前大多數(shù) Web 框架還未支持 JWT,所以可以使用 AES-CBC 算法加密 token。
app.post('/authenticate', function (req, res) {
// 校驗(yàn)用戶;
// 加密 token;
var encrypted = { token: encryptAesSha256('shhhh', JSON.stringify(profile)) };
// 給加密后的 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;
}
// 上面就是 encrypt-then-MAC (加密后簽名)做法。
當(dāng)然你也可以用文中的第 7 點(diǎn),直接將敏感信息丟數(shù)據(jù)庫(kù)中。
9. 將 JSON Web Tokens 應(yīng)用到 OAuth 2
OAuth 2 是一個(gè)解決身份驗(yàn)證的授權(quán)協(xié)議,并且廣泛地使用了 token 。
用戶通過(guò) OAuth 2 協(xié)議授權(quán)第三方應(yīng)用權(quán)限,然后服務(wù)器返回一個(gè)access_token給第三方應(yīng)用,通常也帶有scope參數(shù),第三方應(yīng)用通過(guò)帶上access_token請(qǐng)求服務(wù)器,可以在授權(quán)范圍(scope)內(nèi)調(diào)用 API。
一般來(lái)說(shuō),類似這種 token 是不透明的,就是核心數(shù)據(jù)都儲(chǔ)存以 hash-table 結(jié)果儲(chǔ)存在服務(wù)器中,客戶端只持有一個(gè)令牌(access_token),任何人都可以用這個(gè)令牌在授權(quán)范圍(scope)內(nèi)調(diào)用服務(wù)器端的 API。
Signed tokens(例如JWT))和這種形式的 token 最主要的區(qū)別是,JWT 是無(wú)狀態(tài)的,它不儲(chǔ)存在服務(wù)端 hash-table 中,服務(wù)端中不保留 JWT 請(qǐng)求的相關(guān)信息,JWT 會(huì)把授權(quán)信息和 API 調(diào)用返回都丟一起返回給客戶端。
JWT 通常以 Base64 + AES 方式編碼傳輸。OAuth 2 協(xié)議也支持 JWT,因?yàn)?OAuth 2 并未限制 access_token 數(shù)據(jù)格式,你可以將 JWT 應(yīng)用在 OAuth 2 上。
10. Tokens 不是萬(wàn)能的解決方法,得根據(jù)你的需求自行采用
這些年來(lái),我們幫助過(guò)不少大公司實(shí)現(xiàn)了他們的以 Token 為基礎(chǔ)的驗(yàn)證授權(quán)架構(gòu)。曾經(jīng)有一家 10k + 員工,有著大量數(shù)據(jù)的公司,他們想實(shí)現(xiàn)一個(gè)中央權(quán)限管理系統(tǒng),其中有一個(gè)需要是某個(gè)員工只能讀取某個(gè)國(guó)家某個(gè)醫(yī)院某個(gè)床位的id和name字段數(shù)據(jù),想想這樣的細(xì)粒度的權(quán)限管理是多么難實(shí)現(xiàn),無(wú)論是技術(shù)上還是行政上。
當(dāng)然采用 tokens 與否,得看大家的具體需求,但是,要忠告大家的是,不要什么內(nèi)容都寫到 tokens 了,加之前想想有沒(méi)有這個(gè)必要。
總結(jié)
以上是生活随笔為你收集整理的关于 Token,你应该知道的十件事的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: ios内联函数 inline
- 下一篇: 利用路由器端口映射远程连接