token位置


轉載:https://blog.lishunyang.com/2021/09/where-is-token.html

 

最近正好在梳理系統里的登錄邏輯,有關登錄的話題,那真的是三天三夜也講不完,今天僅僅挑其中的一個小細節聊聊。

# 從用戶的視角看

假設我們使用最基本的用戶名密碼登錄。登錄過程是什么樣的呢?從用戶的視角看,大致是這樣的:

首先,進入登錄頁面。

接着,輸入用戶名和密碼,點擊登錄。

最后,登錄成功,進入系統內,此后一段時間內就可以正常使用系統了。

上面的登錄過程大致可以分為兩個部分:

  1. 登錄動作:輸入用戶名密碼后點擊登錄按鈕。
  2. 登錄狀態維持:后續一段時間內無需再次登錄。

我們通常所講的登錄,就是要實現上面兩個部分。實際上如果從技術實現的角度上看,這兩個部分本質上是一樣的,只是形態不同而已。

# 身份認證

比如,登錄動作,就是向后端發起一個請求(附帶着用戶名和密碼),后端通過用戶名和密碼這兩個信息校驗確定用戶身份。

而登錄狀態維持,其實也是通過某種方式,讓后端能確定出該請求所屬的用戶身份是誰。

可以看到,上面提到的兩個過程,其實都是在做一件事情:“確定當前用戶是誰”,而這就是認證過程(Authentication),也叫做認證、鑒權。

怎么做到身份認證呢?就有很多辦法了。

比如上面提到的登錄狀態維持,如果是一般的 http 請求,可以用 cookie-session 的機制實現,也可以用 jwt 的方式做。而如果所有請求都是走一個連接,例如 websocket,那登錄狀態天然就是可維持的,什么都不用做(因為相同連接可以直接復用登錄上下文信息)。

我曾經特別喜歡問的一個關於 websocket 的面試問題就是:ws 是如何做到登錄狀態維持的呢?現在你知道答案了,ws 不需要登錄狀態維持。

# 如何身份認證

這里我們重點聊聊 http 請求的認證問題。

http 請求要想被認證,那請求里一定得帶點什么標識用戶身份信息的東西對吧,這樣后端才能根據這個東西確定你到底是誰,這個東西我們叫做 token。有很多種類的 token,不過這個話題不是本文的重點。

那么 http 請求能夠怎樣攜帶 token 呢?有以下幾種方式:

  1. 放在請求 url 里。
  2. 放在請求 header 里。
  3. 放在請求 body 里。

這三種方式都是可以的,但是各有利弊。

# token 放在 url 里

比如放在 url 里,不太安全,畢竟 url 是什么很容易就被看到了。倒不是說不能放在 url 里,只是說如果放在 url 里,那你的 token 本身需要帶有一定的安全限制,比如說有限使用次數、有使用期限、只能具有受限的權限等等。

把 token 放在 url 里的認證方式,學名叫做 “bearer token”,翻譯成中文就是:票據攜帶者。感興趣的同學可以自行搜索這個名字的含義,RFC 也有對應的規范可以閱讀。

用一次就失效的 bearer token,通常叫 code,比如 OAuth2 協議中的 code。

bearer token 最大的好處就是簡單,當然這里的簡單指的是用戶使用起來很簡單,只需要訪問一個 url 就可以了,因此 bearer token 非常常見。例如只要點擊一個連接或者點擊一個按鈕就可以登錄的方式,都是基於 bearer token 實現的。

bearer token 除了簡單以外,還有一個好處是它不需要被前端緩存。因為 token 就保存在 url 里,每次取的時候直接從 url 里取就行了,無需用 sessionStorage、localStorage 等做緩存。因此特別適合用在一些臨時登錄的場景里。比如我需要借用一下公共電腦登錄一下,完事兒了只需要清理一下瀏覽記錄即可,不需要清理 cookie、localStorage 等亂七八糟的地方。

在 url 中使用 bearer token 需要注意頁面其他請求(例如 img)的 referrer 可能會泄露你的 token

# token 放在 header 里

放在 header 里的 token,大致有兩種方式:

  1. 放在自定義 header 里。
  2. 放在 cookie header 里。

看似都是在 header 里,實際上有很大區別。

注意,為了描述起來方便,以下用 cookie 方案指代 cookie header 方案,用 header 指代自定義 header 方案。

cookie 的維護嚴重依賴瀏覽器行為。

  • cookie 是怎么種上的?是瀏覽器看到 response header 中的 set-cookie 然后記錄下來的。這個過程無需其他人干預。

  • cookie 是怎么管理有效期的?是瀏覽器看到 set-cookie 里的 age 屬性然后管理的。這個過程也無需其他人干預。

  • cookie 是怎么附帶在請求上的?是瀏覽器在發請求的時候自動附加在請求上的。這個過程還是無需其他人干預。

前端構造 ajax 請求的時候雖然可以手動設置 cookie 這個 header,但實際上請求發出去的時候會被瀏覽器覆蓋掉,所以你想干預都干預不了。

甚至對於具有 http-only 屬性的 cookie,瀏覽器全權托管,禁止 js 訪問,他們對前端來說就是透明的(transparent)。

其實 cookie 被發明之初並不是用來做認證的,只不過大家一看,好家伙,瀏覽器這么貼心什么都幫你做好了,這不省事兒嘛,所以大家就把 token 放到 cookie 里了。

沒錯,使用 cookie 方案最大的好處就是簡單,省心,瀏覽器幫你做完了,但最大的問題就是你得是在瀏覽器環境里,如果你是在 android、ios 原生應用或者小程序里面,不好意思,有關 cookie 的那一堆破事兒就得你自己打補丁做了。

cookie 方案另外一個問題是,出於安全考慮,瀏覽器對 cookie 有很多限制,比如同源限制,same-site 限制,跨域限制等等,有時候這些限制會讓你很難受,比如明明開發測試環境都是好的,發布到生產環境就壞了,你得去檢查這些環境有關 cookie 的限制以及配置是否都是一致的。

而 header 方案就正相反,跟瀏覽器就沒什么關系。token 怎么獲取,怎么注入請求,怎么管理有效期,全都得自己寫代碼實現。累確實是累,而且寫不好一旦出 bug 影響還很嚴重。不過好在不受瀏覽器的限制。

這是非常蛋疼的地方。

什么場景是 cookie 無能為力的呢?禁用 cookie 這種就不提了,有一種常見的支持不了的場景是登錄隔離。比如你需要在瀏覽器中同時登錄好多賬號進行操作。因為 cookie 是根據域名綁定的,而且同名 cookie 只能保存一個,也就意味着 cookie 里面只能保留一個最新的 token,不同賬號的登錄狀態會相互覆蓋。不過有時候這反倒是一個優勢。。比如你就是不想讓用戶多開,那么這種方式可以稍微提高一些用戶多開的成本(用不同瀏覽器或者不同電腦仍然是可以多開的)。

什么場景是 header 無能為力的呢?不受前端代碼影響的請求就沒轍。比如 img 標簽、link 標簽、script 標簽發出的 GET 請求,這些都是瀏覽器自己發出去的,無法注入 header 在這上面。再比如當用戶在瀏覽器中輸入一個 url 並按下回車的時候,這個 url 實際上是向后端請求了一個 html 文件,而這個請求也是無法注入 header 的。再比如使用原始的 form 表單提交的時候,表單提交的那個 post 請求也是瀏覽器發出去的,所以也不能注入 header。鬧了半天,header 只能用在 ajax 請求中,只要某個請求不是 ajax 請求,header 沒法控制了。

非 ajax 請求無法使用 header 方案,這個限制的影響面要更大一些。比如當用戶訪問某個頁面的時候:

  • 如果是 cookie 方案,那么在請求 html 的時候,后端就已經可以根據 cookie 驗證當前用戶是誰,是否需要登錄,如果不需要登錄就自動跳轉到登錄頁面。

  • 如果是 header 方案,因為 html 請求無法攜帶 header,只能等 html 返回以后,瀏覽器解析 js,js 再發出一個 ajax 請求到后端,后端才可以得知當前用戶是誰,是否需要登錄。如果還未登錄,再由前端 js 跳轉到登錄頁面(因為 ajax 請求無法讓頁面跳轉)。

總之就是會讓登錄校驗過程變得前后端耦合在一起,而且整個流程變得很長(html 請求返回、解析 html、解析 js、執行 js、發出 ajax 請求、處理返回結果),耗時也會更久。

另一個受影響的場景是灰度訪問。如果你是需要根據用戶的身份信息來決定用戶打開的是 A 版本還是 B 版本,那么基於 header 的認證方案就無法對 html 做灰度,也就是說 html 永遠只能是一個版本。當然這個就說來話長了,以后有機會再聊(挖坑+1)。

cookie 是后端方案,header 是前端方案。這句話怎么理解呢?

  • 如果你選擇 header 的方式,那么在發請求的時候就需要手動設置自定義 header。這個過程是前端代碼控制的,前端不寫點代碼是做不到的。

  • 如果你選擇 cookie 的方式,正如前面所說,cookie 的各種操作都是瀏覽器幫你做了。而通常 cookie 又是后端通過 set-cookie 種上去的(尤其是對於 http-only 的 cookie)。所以說,這個過程可以認為是后端代碼控制的,前端可以不寫任何代碼。

因為認證過程一定是有后端工作量的,既然如此,前端就別來摻和了,全部由后端搞定即可,這樣也更有利於維護。所以我是更推薦使用后端方案的,即使用 cookie 方案。但實際上我發現很多后端工程師對 cookie 的理解非常薄弱(其實不少前端工程師也一樣。。),畢竟 cookie 的管理機制非常依賴瀏覽器,算是前端知識了。這導致很多時候他們都不知道如何正確使用 cookie,設計出來的方案也是非常奇怪。。

除了開發和維護,前端方案和后端方案的另一個區別就在於調試,前端方案調試起來相對容易些。pc 端瀏覽器二者區別不大,可如果是移動端瀏覽器,或者是一些 webview 環境中,采用前端方案,你還可以僅通過改變前端代碼或者修改前端 token 緩存來調試登錄過程,后端方案嘛,如果不拉着后端工程師或者親自改后端那就真的是無能為力了。

畢竟,責任和權利總是相匹配的。

# token 放在 body 里

除了 url、header,最后一種選擇就是放在 body 里了,不過實際上很少有人這么做,有點憨憨。原因也很簡單:

首先 GET 請求是沒有 body 的,這意味放在 body 的方式無法支持 GET 請求的認證。

其次 body 通常是用來傳輸數據的,格式五花八門,有 json、text、form,甚至還可能是二進制,怎么跟數據共存是個蛋疼問題。

總之,通常沒人會把 token 放 body 里,除非是有一些非常特殊的情況。

# 總結

上面啰嗦了一大堆,其實我們只是在講一個問題:token 要放在哪里。至於 token 怎么生成?token 在后端如何校驗?多種 token 方案如何共存?如何設計登錄方案?等等等等,真的是三天三夜也講不完。

最后總結一下全文的重點:

  • 認證過程就是回答這是哪個用戶的問題。

  • 登錄和登錄狀態維持其實都是認證過程。

  • 標識用戶身份的唯一標識,叫做 token。

  • 對於 http 請求的認證,可以把 token 放在 url、header、body 中。

  • 放在 url 中的 token 叫做 bearer token。

  • 放在 header 中的 token 有自定義 header 和 cookie header 兩種方案,各有利弊。

  • 一般沒人把 token 放在 body 中。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM