不久前请契,我在在前后端分離實(shí)踐中提到了基于 Token 的認(rèn)證锅风,現(xiàn)在我們稍稍深入一些萝喘。
通常情況下淮逻,我們在討論某個技術(shù)的時候,都是從問題開始阁簸。那么第一個問題:
為什么要用 Token爬早?
而要回答這個問題很簡單——因?yàn)樗芙鉀Q問題!
可以解決哪些問題呢启妹?
- Token 完全由應(yīng)用管理筛严,所以它可以避開同源策略
- Token 可以避免 CSRF 攻擊
- Token 可以是無狀態(tài)的,可以在多個服務(wù)間共享
Token 是在服務(wù)端產(chǎn)生的饶米。如果前端使用用戶名/密碼向服務(wù)端請求認(rèn)證桨啃,服務(wù)端認(rèn)證成功,那么在服務(wù)端會返回 Token 給前端檬输。前端可以在每次請求的時候帶上 Token 證明自己的合法地位照瘾。如果這個 Token 在服務(wù)端持久化(比如存入數(shù)據(jù)庫),那它就是一個永久的身份令牌丧慈。
于是析命,又一個問題產(chǎn)生了:需要為 Token 設(shè)置有效期嗎?
需要設(shè)置有效期嗎逃默?
對于這個問題鹃愤,我們不妨先看兩個例子。一個例子是登錄密碼完域,一般要求定期改變密碼软吐,以防止泄漏,所以密碼是有有效期的吟税;另一個例子是安全證書凹耙。SSL 安全證書都有有效期鸟蟹,目的是為了解決吊銷的問題,對于這個問題的詳細(xì)情況使兔,來看看知乎的回答建钥。所以無論是從安全的角度考慮,還是從吊銷的角度考慮虐沥,Token 都需要設(shè)有效期熊经。
那么有效期多長合適呢?
只能說欲险,根據(jù)系統(tǒng)的安全需要镐依,盡可能的短,但也不能短得離譜——想像一下手機(jī)的自動熄屏?xí)r間天试,如果設(shè)置為 10 秒鐘無操作自動熄屏槐壳,再次點(diǎn)亮需要輸入密碼,會不會瘋喜每?如果你覺得不會务唐,那就親自試一試,設(shè)置成可以設(shè)置的最短時間带兜,堅(jiān)持一周就好(不排除有人適應(yīng)這個時間枫笛,畢竟手機(jī)廠商也是有用戶體驗(yàn)研究的)。
然后新問題產(chǎn)生了刚照,如果用戶在正常操作的過程中刑巧,Token 過期失效了,要求用戶重新登錄……用戶體驗(yàn)豈不是很糟糕无畔?
為了解決在操作過程不能讓用戶感到 Token 失效這個問題啊楚,有一種方案是在服務(wù)器端保存 Token 狀態(tài),用戶每次操作都會自動刷新(推遲) Token 的過期時間——Session 就是采用這種策略來保持用戶登錄狀態(tài)的浑彰。然而仍然存在這樣一個問題恭理,在前后端分離、單頁 App 這些情況下闸昨,每秒種可能發(fā)起很多次請求蚯斯,每次都去刷新過期時間會產(chǎn)生非常大的代價。如果 Token 的過期時間被持久化到數(shù)據(jù)庫或文件饵较,代價就更大了。所以通常為了提升效率遭赂,減少消耗循诉,會把 Token 的過期時保存在緩存或者內(nèi)存中。
還有另一種方案撇他,使用 Refresh Token茄猫,它可以避免頻繁的讀寫操作狈蚤。這種方案中,服務(wù)端不需要刷新 Token 的過期時間划纽,一旦 Token 過期脆侮,就反饋給前端,前端使用 Refresh Token 申請一個全新 Token 繼續(xù)使用勇劣。這種方案中靖避,服務(wù)端只需要在客戶端請求更新 Token 的時候?qū)?Refresh Token 的有效性進(jìn)行一次檢查,大大減少了更新有效期的操作比默,也就避免了頻繁讀寫幻捏。當(dāng)然 Refresh Token 也是有有效期的,但是這個有效期就可以長一點(diǎn)了命咐,比如篡九,以天為單位的時間。
時序圖表示
使用 Token 和 Refresh Token 的時序圖如下:
1)登錄
2)業(yè)務(wù)請求
3)Token 過期醋奠,刷新 Token
上面的時序圖中并未提到 Refresh Token 過期怎么辦榛臼。不過很顯然,Refresh Token 既然已經(jīng)過期窜司,就該要求用戶重新登錄了讽坏。
當(dāng)然還可以把這個機(jī)制設(shè)計(jì)得更復(fù)雜一些,比如例证,Refresh Token 每次使用的時候路呜,都更新它的過期時間,直到與它的創(chuàng)建時間相比织咧,已經(jīng)超過了非常長的一段時間(比如三個月)胀葱,這等于是在相當(dāng)長一段時間內(nèi)允許 Refresh Token 自動續(xù)期。
到目前為止笙蒙,Token 都是有狀態(tài)的抵屿,即在服務(wù)端需要保存并記錄相關(guān)屬性。那說好的無狀態(tài)呢捅位,怎么實(shí)現(xiàn)轧葛?
無狀態(tài) Token
如果我們把所有狀態(tài)信息都附加在 Token 上,服務(wù)器就可以不保存艇搀。但是服務(wù)端仍然需要認(rèn)證 Token 有效尿扯。不過只要服務(wù)端能確認(rèn)是自己簽發(fā)的 Token,而且其信息未被改動過焰雕,那就可以認(rèn)為 Token 有效——“簽名”可以作此保證衷笋。平時常說的簽名都存在一方簽發(fā),另一方驗(yàn)證的情況矩屁,所以要使用非對稱加密算法辟宗。但是在這里爵赵,簽發(fā)和驗(yàn)證都是同一方,所以對稱加密算法就能達(dá)到要求泊脐,而對稱算法比非對稱算法要快得多(可達(dá)數(shù)十倍差距)空幻。更進(jìn)一步思考,對稱加密算法除了加密容客,還帶有還原加密內(nèi)容的功能秕铛,而這一功能在對 Token 簽名時并無必要——既然不需要解密,摘要(散列)算法就會更快耘柱∪缤保可以指定密碼的散列算法,自然是 HMAC调煎。
上面說了這么多镜遣,還需要自己去實(shí)現(xiàn)嗎?不用士袄!JWT 已經(jīng)定義了詳細(xì)的規(guī)范悲关,而且有各種語言的若干實(shí)現(xiàn)。
不過在使用無狀態(tài) Token 的時候在服務(wù)端會有一些變化娄柳,服務(wù)端雖然不保存有效的 Token 了寓辱,卻需要保存未到期卻已注銷的 Token。如果一個 Token 未到期就被用戶主動注銷赤拒,那么服務(wù)器需要保存這個被注銷的 Token秫筏,以便下次收到使用這個仍在有效期內(nèi)的 Token 時判其無效。有沒有感到一點(diǎn)沮喪挎挖?
在前端可控的情況下(比如前端和服務(wù)端在同一個項(xiàng)目組內(nèi))这敬,可以協(xié)商:前端一但注銷成功,就丟掉本地保存(比如保存在內(nèi)存蕉朵、LocalStorage 等)的 Token 和 Refresh Token崔涂。基于這樣的約定始衅,服務(wù)器就可以假設(shè)收到的 Token 一定是沒注銷的(因?yàn)樽N之后前端就不會再使用了)冷蚂。
如果前端不可控的情況,仍然可以進(jìn)行上面的假設(shè)汛闸,但是這種情況下蝙茶,需要盡量縮短 Token 的有效期,而且必須在用戶主動注銷的情況下讓 Refresh Token 無效蛉拙。這個操作存在一定的安全漏洞尸闸,因?yàn)橛脩魰J(rèn)為已經(jīng)注銷了,實(shí)際上在較短的一段時間內(nèi)并沒有注銷孕锄。如果應(yīng)用設(shè)計(jì)中吮廉,這點(diǎn)漏洞并不會造成什么損失,那采用這種策略就是可行的畸肆。
在使用無狀態(tài) Token 的時候宦芦,有兩點(diǎn)需要注意:
- Refresh Token 有效時間較長,所以它應(yīng)該在服務(wù)器端有狀態(tài)轴脐,以增強(qiáng)安全性调卑,確保用戶注銷時可控
- 應(yīng)該考慮使用二次認(rèn)證來增強(qiáng)敏感操作的安全性
到此,關(guān)于 Token 的話題似乎差不多了——然而并沒有大咱,上面說的只是認(rèn)證服務(wù)和業(yè)務(wù)服務(wù)集成在一起的情況恬涧,如果是分離的情況呢?
分離認(rèn)證服務(wù)
當(dāng) Token 無狀態(tài)之后碴巾,單點(diǎn)登錄就變得容易了溯捆。前端拿到一個有效的 Token,它就可以在任何同一體系的服務(wù)上認(rèn)證通過——只要它們使用同樣的密鑰和算法來認(rèn)證 Token 的有效性厦瓢。就樣這樣:
當(dāng)然提揍,如果 Token 過期了,前端仍然需要去認(rèn)證服務(wù)更新 Token:
可見煮仇,雖然認(rèn)證和業(yè)務(wù)分離了劳跃,實(shí)際即并沒產(chǎn)生多大的差異。當(dāng)然浙垫,這是建立在認(rèn)證服務(wù)器信任業(yè)務(wù)服務(wù)器的前提下刨仑,因?yàn)檎J(rèn)證服務(wù)器產(chǎn)生 Token 的密鑰和業(yè)務(wù)服務(wù)器認(rèn)證 Token 的密鑰和算法相同。換句話說夹姥,業(yè)務(wù)服務(wù)器同樣可以創(chuàng)建有效的 Token杉武。
如果業(yè)務(wù)服務(wù)器不能被信任,該怎么辦佃声?
不受信的業(yè)務(wù)服務(wù)器
遇到不受信的業(yè)務(wù)服務(wù)器時艺智,很容易想到的辦法是使用不同的密鑰。認(rèn)證服務(wù)器使用密鑰1簽發(fā)圾亏,業(yè)務(wù)服務(wù)器使用密鑰2驗(yàn)證——這是典型非對稱加密簽名的應(yīng)用場景十拣。認(rèn)證服務(wù)器自己使用私鑰對 Token 簽名,公開公鑰志鹃。信任這個認(rèn)證服務(wù)器的業(yè)務(wù)服務(wù)器保存公鑰夭问,用于驗(yàn)證簽名。幸好曹铃,JWT 不僅可以使用 HMAC 簽名缰趋,也可以使用 RSA(一種非對稱加密算法)簽名。
不過,當(dāng)業(yè)務(wù)服務(wù)器已經(jīng)不受信任的時候秘血,多個業(yè)務(wù)服務(wù)器之間使用相同的 Token 對用戶來說是不安全的味抖。因?yàn)槿魏我粋€服務(wù)器拿到 Token 都可以仿冒用戶去另一個服務(wù)器處理業(yè)務(wù)……悲劇隨時可能發(fā)生。
為了防止這種情況發(fā)生灰粮,就需要在認(rèn)證服務(wù)器產(chǎn)生 Token 的時候仔涩,把使用該 Token 的業(yè)務(wù)服務(wù)器的信息記錄在 Token 中,這樣當(dāng)另一個業(yè)務(wù)服務(wù)器拿到這個 Token 的時候粘舟,發(fā)現(xiàn)它并不是自己應(yīng)該驗(yàn)證的 Token熔脂,就可以直接拒絕。
現(xiàn)在柑肴,認(rèn)證服務(wù)器不信任業(yè)務(wù)服務(wù)器霞揉,業(yè)務(wù)服務(wù)器相互也不信任,但前端是信任這些服務(wù)器的——如果前端不信任晰骑,就不會拿 Token 去請求驗(yàn)證适秩。那么為什么會信任?可能是因?yàn)檫@些是同一家公司或者同一個項(xiàng)目中提供的若干服務(wù)構(gòu)成的服務(wù)體系些侍。
但是隶症,前端信任不代表用戶信任。如果 Token 不沒有攜帶用戶隱私(比如姓名)岗宣,那么用戶不會關(guān)心信任問題蚂会。但如果 Token 含有用戶隱私的時候,用戶得關(guān)心信任問題了耗式。這時候認(rèn)證服務(wù)就不得不再啰嗦一些胁住,當(dāng)用戶請求 Token 的時候,問上一句刊咳,你真的要授權(quán)給某某某業(yè)務(wù)服務(wù)嗎彪见?而這個“某某某”,用戶怎么知道它是不是真的“某某某”呢娱挨?用戶當(dāng)然不知道余指,甚至認(rèn)證服務(wù)也不知道,因?yàn)楣€已經(jīng)公開了跷坝,任何一個業(yè)務(wù)都可以聲明自己是“某某某”酵镜。
為了得到用戶的信任,認(rèn)證服務(wù)就不得不幫助用戶來甄別業(yè)務(wù)服務(wù)柴钻。所以淮韭,認(rèn)證服器決定不公開公鑰,而是要求業(yè)務(wù)服務(wù)先申請注冊并通過審核贴届。只有通過審核的業(yè)務(wù)服務(wù)器才能得到認(rèn)證服務(wù)為它創(chuàng)建的靠粪,僅供它使用的公鑰蜡吧。如果該業(yè)務(wù)服務(wù)泄漏公鑰帶來風(fēng)險,由該業(yè)務(wù)服務(wù)自行承擔(dān)≌技現(xiàn)在認(rèn)證服務(wù)可以清楚的告訴用戶昔善,“某某某”服務(wù)是什么了。如果用戶還是不夠信任捞慌,認(rèn)證服務(wù)甚至可以問耀鸦,某某某業(yè)務(wù)服務(wù)需要請求 A柬批、B啸澡、C 三項(xiàng)個人數(shù)據(jù),其中 A 是必須的氮帐,不然它不工作嗅虏,是否允許授權(quán)?如果你授權(quán)上沐,我就把你授權(quán)的幾項(xiàng)數(shù)據(jù)加密放在 Token 中……
廢話了這么多皮服,有沒有似曾相識……對了,這類似開放式 API 的認(rèn)證過程参咙。開發(fā)式 API 多采用 OAuth 認(rèn)證龄广,而關(guān)于 OAuth 的探討資源非常豐富,這里就不深究了蕴侧。