[漫談] 狀態

記錄一下筆者關於狀態的一些相關認知。

1 狀態

在電腦領域,狀態[1]指的是一個系統被設計用來記住之前的事件或用戶交互,那麼就稱之為有狀態的系統,系統記錄的資訊則就是狀態。注意這裡的重點不是說記錄資訊,而是記住之前的

舉個例子:你去樓下的便利店買東西,95元,你給了店家100塊,但是店家暫時沒零錢,這時候你說了,算了,你先記著吧,下次買東西少收我5塊就是了,店家說好嘞。然後第二天你又去買東西了,要求店家減免你5塊錢,店家說憑什麼啊。。。然後你說昨天你沒零錢找我5塊錢,忘了?噢噢噢,對對,不好意思啊,忘了。店家滿臉歉意的說到。

2 客戶端-伺服器 風格

在上述的例子中:店家他沒有找你5塊錢這件事就是狀態。你沒有忘記,但是店家暫時忘記了。由此可以得出狀態的前提條件發生在雙方通訊交互時,雙方維持的一些之前的交互資訊。在電腦領域,雙方通訊時,這種交互風格就叫做客戶端-伺服器,也就是我們通常說的C/S

還有一個B/S,其實也是屬於C/S,只是它的C是特定的Browser這種客戶端。

客戶端-伺服器 風格[2]其背後的原則是分離關注點,使得客戶端和伺服器分別只關注各自領域的問題。通過此舉可以簡化伺服器的實現(分離一部分邏輯到客戶端),使得伺服器可以獨立部署維護,從而改善系統的可伸縮性

3 無狀態

無狀態[3]並不是說我們徹底不要狀態了,而僅僅只是說在雙方通訊時:從客戶端到伺服器的每次請求都必須包含理解該請求所必須的所有資訊,不能利用伺服器存儲會話的上下文資訊,會話狀態全部保存在客戶端。重點在於把狀態的維護從服務端轉移到客戶端來。這樣做可以改善可見性(監視系統不必為了確定一個請求的全部性質而去查看請求之外的其他請求);改善可靠性(減輕了從局部故障中恢復的任務量);改善可伸縮性(服務端不必在多個請求直接保存狀態,從而允許伺服器迅速釋放資源)。但是無狀態也有相應的缺點,由於伺服器不能保持會話狀態數據,則會造成在每一次請求中發送大量重複的數據,可能會降低網路性能

3.1 無狀態的協議

在電腦通訊方面,絕大部分的協議都是無狀態的,比如IP協議,UDP協議,HTTP協議。拿上面的例子來說,比如店家寫個欠條,按上手印,交給你。那麼這時候對於店家來說就不必再維護狀態了,而全部轉移到了你的手中,下次你只需拿著欠條找店家即可,或者你朋友代你拿著欠條也可以。店家自己的手印在,是不可抵賴不認賬的。

3.2 有狀態的協議

TCP協議[4]是有狀態的協議,通訊雙方事先需要實現建立連接,維持通訊的狀態。還是上面的例子,店家見到了你,你倆都需要記得欠你5塊錢這件事才行,一方忘了(對方再也記不起來了),那就再也要不回來了。TCP中的RST標記就是這樣的,客戶端拿著一個SYN要求和伺服器建立連接,由於種種原因,伺服器不記得了,認為這個SYN無效,然後就對客戶端說:滾蛋,我不認識你。。。

4 OAuth2 OIDC JWT

OAuth2 OIDC JWT是認證和授權相關方面的協議。有人說了,狀態與認證和授權[5]有什麼關係啊?眾所周知,HTTP協議是無狀態的協議,OAuth2和OIDC則都是基於HTTP的協議。但是認證和授權都是有狀態的行為,也就是會產生狀態出來,OIDC會產生認證的結果(id_token),授權會得到授權的結果(access_token),然後拿著這些*_token來維持後續的交互的狀態。那麼這時候問題來了,誰來維護這些狀態?

在OAuth2協議中,access_token對於客戶端來說是一個黑盒的字元串。那麼為何是一個黑盒的字元串?在回答這個問題前有件事需要先搞清楚,access_token的客戶端是誰?有人說了,廢話,當然是第三方client啊!非也非也,並不是,第三方client是access_token的持有者,但是並不是它的客戶端。access_token是授權伺服器頒發的,受保護的資源伺服器才是access_token的客戶端。第三方client只是受保護的資源的伺服器的客戶端。受保護的資源伺服器拿著第三方client傳遞過來的access_token去授權伺服器檢查是否有效。在這種交互模式下,第三方client和受保護的資源伺服器都完全不必關心access_token的內容是什麼,統統交給授權伺服器即可。所以答案就來了:黑盒字元串足以,授權伺服器維護access_token的狀態

在OIDC協議中,id_token對於客戶端來說,一個黑盒字元串就遠遠不夠了,想一下認證的目的是什麼?告訴客戶端你是誰!這時候你給客戶端一個黑盒的字元串能有個屁用。。。所以這時候就用上了JWT這個數據格式來告訴客戶端當然通過認證的用戶是誰。那麼此時,狀態則僅僅位於持有id_token的客戶端了。認證伺服器認證完了(id_token會包含有效時間範圍和用戶的id,也可以包含用戶的名字、頭像等資訊),後續也就不必再維護任何狀態了。

回看一下OAuth2有沒有什麼可以改進的地方?有!就是授權伺服器維護access_token的狀態,這會使得授權伺服器壓力大增。那麼這時候怎麼辦?把壓力轉移出去唄,在OAuth2協議發布之後誕生了JWT這種數據格式,得益於這個格式的特性,用它來保存access_token的狀態就再合適不過了。這時受保護的資源伺服器在收到access_token之後,就可以解析出來其中的資訊,獨立的完成驗證,不必再去授權伺服器檢查了。

不過這時又有了新的問題,id_tokenaccess_token不再去認證伺服器和授權伺服器去檢查,這時候我要作廢*_token怎麼辦?解決辦法有兩個:

  1. 縮短*_token的有效時間,比如30分鐘,結合自動刷新機制,降低作廢的必要性。
  2. 不行,必須要實時作廢。那也可以,CA領域已經有很成熟的方案,CRL和OCSP協議可以借鑒參考,維護一個作廢的列表即可。JWT中有jti這樣的一個唯一標識符,維護一個提前作廢的jti列表就可以了。這時*_token的客戶端可以通過檢查這個列表來阻止提前作廢的*_token。有人說了,這不是還是需要每次都調用檢查,又回到了老路子上。其實並不是的,這次是只是作廢檢查,客戶端可以根據自己的業務需要來快取,比如你的qps是100,那麼即使你一秒刷新一次這個作廢列表,也可以節省99次的檢查;其次,作廢列表的量級會小很多(結合*_token中的有效時間範圍,那些本來已經過期的還可以進一步的剔除);再次,這個作廢檢查可以完全的獨立於認證伺服器和授權伺服器,因為它無需關心認證和授權內部的細節,只是一個作廢列表罷了,邏輯非常簡單通用。

5 總結

分析了一下狀態相關的一些概念和問題和對應的解決方案。

6 參考

本文首發於://linianhui.github.io/talk/stateless


  1. 狀態 : //en.wikipedia.org/wiki/State_(computer_science) ↩︎

  2. 客戶端-伺服器 : //www.cnblogs.com/linianhui/p/rest_web-and-rest.html#auto-id-8 ↩︎

  3. 無狀態 : //www.cnblogs.com/linianhui/p/rest_web-and-rest.html#auto-id-9 ↩︎

  4. TCP : //linianhui.github.io/computer-networking/tcp/ ↩︎

  5. 認證和授權 //www.cnblogs.com/linianhui/category/929878.html/ ↩︎