寫這一篇文章的來源是因為某一天的我被面試官提問:讓你設計一個登錄頁面,你會如何設計?
我當時的腦子只有???
不就是提交賬號、密碼給后台就搞定了呢?
不可能那么簡單,我弱弱的想,難道要對密碼加密??
之后他繼續提問,當你登錄成功后,去請求購物車時,后台為什么會返回的是你的購物車,而不是別人的?
不是匹配用戶id嗎??我還沒領悟到面試官想讓我回答的是Token。。。。
參考鏈接:https://juejin.im/post/5a6c60166fb9a01caf37a5e5
對稱加密與非對稱加密具體應用(https原理):https://juejin.im/entry/5942061dda2f600067541114
什么是Token?
token的意思是“令牌”,是服務端生成的一串字符串,作為客戶端進行請求的一個標識。
當用戶第一次登錄后,服務器生成一個token並將此token返回給客戶端,以后客戶端只需帶上這個token前來請求數據即可,無需再次帶上用戶名和密碼。
簡單token的組成;uid(用戶唯一的身份標識)、time(當前時間的時間戳)、sign(簽名,token的前幾位以哈希算法壓縮成的一定長度的十六進制字符串。為防止token泄露)。
為什么要用Token?
- Token 完全由應用管理,所以它可以避開同源策略
- Token 可以避免 CSRF 攻擊
- Token 可以是無狀態的,可以在多個服務間共享
基於token機制的身份認證
使用token機制的身份驗證方法,在服務器端不需要存儲用戶的登錄記錄。大概的流程:
- 客戶端使用用戶名和密碼請求登錄。
- 服務端收到請求,驗證用戶名和密碼。
- 驗證成功后,服務端會生成一個token,然后把這個token發送給客戶端。
- 客戶端收到token后把它存儲起來,可以放在cookie或者Local Storage(本地存儲)里。
- 客戶端每次向服務端發送請求的時候都需要帶上服務端發給的token。
- 服務端收到請求,然后去驗證客戶端請求里面帶着token,如果驗證成功,就向客戶端返回請求的數據。(如果這個 Token 在服務端持久化(比如存入數據庫),那它就是一個永久的身份令牌。)
Token需要設置有效期嗎?
對於這個問題,我們不妨先看兩個例子。一個例子是登錄密碼,一般要求定期改變密碼,以防止泄漏,所以密碼是有有效期的;另一個例子是安全證書。SSL 安全證書都有有效期,目的是為了解決吊銷的問題。所以無論是從安全的角度考慮,還是從吊銷的角度考慮,Token 都需要設有效期。
那么有效期多長合適呢?
只能說,根據系統的安全需要,盡可能的短,但也不能短得離譜——想像一下手機的自動熄屏時間,如果設置為 10 秒鍾無操作自動熄屏,再次點亮需要輸入密碼,會不會瘋?
然后新問題產生了,如果用戶在正常操作的過程中,Token 過期失效了,要求用戶重新登錄……用戶體驗豈不是很糟糕?
解決Token失效的問題
上面的時序圖中並未提到 Refresh Token 過期怎么辦。不過很顯然,Refresh Token 既然已經過期,就該要求用戶重新登錄了。
當然還可以把這個機制設計得更復雜一些,比如,Refresh Token 每次使用的時候,都更新它的過期時間,直到與它的創建時間相比,已經超過了非常長的一段時間(比如三個月),這等於是在相當長一段時間內允許 Refresh Token 自動續期。
到目前為止,Token 都是有狀態的,即在服務端需要保存並記錄相關屬性。那說好的無狀態呢,怎么實現?
無狀態Token
如果我們把所有狀態信息都附加在 Token 上,服務器就可以不保存。但是服務端仍然需要認證 Token 有效。不過只要服務端能確認是自己簽發的 Token,而且其信息未被改動過,那就可以認為 Token 有效——“簽名”可以作此保證。平時常說的簽名都存在一方簽發,另一方驗證的情況,所以要使用非對稱加密算法。但是在這里,簽發和驗證都是同一方,所以對稱加密算法就能達到要求,而對稱算法比非對稱算法要快得多(可達數十倍差距)。更進一步思考,對稱加密算法除了加密,還帶有還原加密內容的功能,而這一功能在對 Token 簽名時並無必要——既然不需要解密,摘要(散列)算法就會更快。可以指定密碼的散列算法,自然是 HMAC。
上面說了這么多,還需要自己去實現嗎?不用!JWT 已經定義了詳細的規范,而且有各種語言的若干實現。
不過在使用無狀態 Token 的時候在服務端會有一些變化,服務端雖然不保存有效的 Token 了,卻需要保存未到期卻已注銷的 Token。如果一個 Token 未到期就被用戶主動注銷,那么服務器需要保存這個被注銷的 Token,以便下次收到使用這個仍在有效期內的 Token 時判其無效。有沒有感到一點沮喪?
在前端可控的情況下(比如前端和服務端在同一個項目組內),可以協商:前端一但注銷成功,就丟掉本地保存(比如保存在內存、LocalStorage 等)的 Token 和 Refresh Token。基於這樣的約定,服務器就可以假設收到的 Token 一定是沒注銷的(因為注銷之后前端就不會再使用了)。
如果前端不可控的情況,仍然可以進行上面的假設,但是這種情況下,需要盡量縮短 Token 的有效期,而且必須在用戶主動注銷的情況下讓 Refresh Token 無效。這個操作存在一定的安全漏洞,因為用戶會認為已經注銷了,實際上在較短的一段時間內並沒有注銷。如果應用設計中,這點漏洞並不會造成什么損失,那采用這種策略就是可行的。
在使用無狀態 Token 的時候,有兩點需要注意:
- Refresh Token 有效時間較長,所以它應該在服務器端有狀態,以增強安全性,確保用戶注銷時可控
- 應該考慮使用二次認證來增強敏感操作的安全性
到此,關於 Token 的話題似乎差不多了——然而並沒有,上面說的只是認證服務和業務服務集成在一起的情況,如果是分
分離認證服務
當 Token 無狀態之后,單點登錄就變得容易了。前端拿到一個有效的 Token,它就可以在任何同一體系的服務上認證通過——只要它們使用同樣的密鑰和算法來認證 Token 的有效性。就樣這樣:
當然,如果 Token 過期了,前端仍然需要去認證服務更新 Token:
可見,雖然認證和業務分離了,實際即並沒產生多大的差異。當然,這是建立在認證服務器信任業務服務器的前提下,因為認證服務器產生 Token 的密鑰和業務服務器認證 Token 的密鑰和算法相同。換句話說,業務服務器同樣可以創建有效的 Token。
如果業務服務器不能被信任,該怎么辦?
不受信的業務服務器
遇到不受信的業務服務器時,很容易想到的辦法是使用不同的密鑰。認證服務器使用密鑰1簽發,業務服務器使用密鑰2驗證——這是典型非對稱加密簽名的應用場景。認證服務器自己使用私鑰對 Token 簽名,公開公鑰。信任這個認證服務器的業務服務器保存公鑰,用於驗證簽名。幸好,JWT 不僅可以使用 HMAC 簽名,也可以使用 RSA(一種非對稱加密算法)簽名。
不過,當業務服務器已經不受信任的時候,多個業務服務器之間使用相同的 Token 對用戶來說是不安全的。因為任何一個服務器拿到 Token 都可以仿冒用戶去另一個服務器處理業務……悲劇隨時可能發生。
為了防止這種情況發生,就需要在認證服務器產生 Token 的時候,把使用該 Token 的業務服務器的信息記錄在 Token 中,這樣當另一個業務服務器拿到這個 Token 的時候,發現它並不是自己應該驗證的 Token,就可以直接拒絕。
現在,認證服務器不信任業務服務器,業務服務器相互也不信任,但前端是信任這些服務器的——如果前端不信任,就不會拿 Token 去請求驗證。那么為什么會信任?可能是因為這些是同一家公司或者同一個項目中提供的若干服務構成的服務體系。
但是,前端信任不代表用戶信任。如果 Token 不沒有攜帶用戶隱私(比如姓名),那么用戶不會關心信任問題。但如果 Token 含有用戶隱私的時候,用戶得關心信任問題了。這時候認證服務就不得不再啰嗦一些,當用戶請求 Token 的時候,問上一句,你真的要授權給某某某業務服務嗎?而這個“某某某”,用戶怎么知道它是不是真的“某某某”呢?用戶當然不知道,甚至認證服務也不知道,因為公鑰已經公開了,任何一個業務都可以聲明自己是“某某某”。
為了得到用戶的信任,認證服務就不得不幫助用戶來鑒別業務服務。所以,認證服器決定不公開公鑰,而是要求業務服務先申請注冊並通過審核。只有通過審核的業務服務器才能得到認證服務為它創建的,僅供它使用的公鑰。如果該業務服務泄漏公鑰帶來風險,由該業務服務自行承擔。現在認證服務可以清楚的告訴用戶,“某某某”服務是什么了。如果用戶還是不夠信任,認證服務甚至可以問,某某某業務服務需要請求 A、B、C 三項個人數據,其中 A 是必須的,不然它不工作,是否允許授權?如果你授權,我就把你授權的幾項數據加密放在 Token 中……