http是無狀態的,一次請求結束,連接斷開,下次伺服器再收到請求,它就不知道這個請求是哪個用戶發過來的。當然它知道是哪個客戶端地址發過來的,但是對於我們的應用來說,我們是靠用戶來管理,而不是靠客戶端。所以對我們的應用而言,它是需要有狀態管理的,以便服務端能夠準確的知道http請求是哪個用戶發起的,從 ...
http是無狀態的,一次請求結束,連接斷開,下次伺服器再收到請求,它就不知道這個請求是哪個用戶發過來的。當然它知道是哪個客戶端地址發過來的,但是對於我們的應用來說,我們是靠用戶來管理,而不是靠客戶端。所以對我們的應用而言,它是需要有狀態管理的,以便服務端能夠準確的知道http請求是哪個用戶發起的,從而判斷他是否有許可權繼續這個請求。這個過程就是常說的會話管理。它也可以簡單理解為一個用戶從登錄到退出應用的一段期間。本文總結了3種常見的實現web應用會話管理的方式:
1)基於server端session的管理方式
2)cookie-base的管理方式
3)token-base的管理方式
這些內容可以幫助加深對web中用戶登錄機制的理解,對實際項目開發也有參考價值,歡迎閱讀與指正。
1. 基於server端session的管理
在早期web應用中,通常使用服務端session來管理用戶的會話。快速瞭解服務端session:
1) 服務端session是用戶第一次訪問應用時,伺服器就會創建的對象,代表用戶的一次會話過程,可以用來存放數據。伺服器為每一個session都分配一個唯一的sessionid,以保證每個用戶都有一個不同的session對象。
2)伺服器在創建完session後,會把sessionid通過cookie返回給用戶所在的瀏覽器,這樣當用戶第二次及以後向伺服器發送請求的時候,就會通過cookie把sessionid傳回給伺服器,以便伺服器能夠根據sessionid找到與該用戶對應的session對象。
3)session通常有失效時間的設定,比如2個小時。當失效時間到,伺服器會銷毀之前的session,並創建新的session返回給用戶。但是只要用戶在失效時間內,有發送新的請求給伺服器,通常伺服器都會把他對應的session的失效時間根據當前的請求時間再延長2個小時。
4)session在一開始並不具備會話管理的作用。它只有在用戶登錄認證成功之後,並且往sesssion對象裡面放入了用戶登錄成功的憑證,才能用來管理會話。管理會話的邏輯也很簡單,只要拿到用戶的session對象,看它裡面有沒有登錄成功的憑證,就能判斷這個用戶是否已經登錄。當用戶主動退出的時候,會把它的session對象里的登錄憑證清掉。所以在用戶登錄前或退出後或者session對象失效時,肯定都是拿不到需要的登錄憑證的。
主流的web開發平臺(java,.net,php)都原生支持這種會話管理的方式,而且開發起來很簡單,相信大部分後端開發人員在入門的時候都瞭解並使用過它。它還有一個比較大的優點就是安全性好,因為在瀏覽器端與伺服器端保持會話狀態的媒介始終只是一個sessionid串,只要這個串夠隨機,攻擊者就不能輕易冒充他人的sessionid進行操作;除非通過CSRF或http劫持的方式,才有可能冒充別人進行操作;即使冒充成功,也必須被冒充的用戶session裡面包含有效的登錄憑證才行。但是在真正決定用它管理會話之前,也得根據自己的應用情況考慮以下幾個問題:
1)這種方式將會話信息存儲在web伺服器裡面,所以在用戶同時線上量比較多時,這些會話信息會占據比較多的記憶體;
2)當應用採用集群部署的時候,會遇到多台web伺服器之間如何做session共用的問題。因為session是由單個伺服器創建的,但是處理用戶請求的伺服器不一定是那個創建session的伺服器,這樣他就拿不到之前已經放入到session中的登錄憑證之類的信息了;
3)多個應用要共用session時,除了以上問題,還會遇到跨域問題,因為不同的應用可能部署的主機不一樣,需要在各個應用做好cookie跨域的處理。
針對問題1和問題2,我見過的解決方案是採用redis這種中間伺服器來管理session的增刪改查,一來減輕web伺服器的負擔,二來解決不同web伺服器共用session的問題。針對問題3,由於服務端的session依賴cookie來傳遞sessionid,所以在實際項目中,只要解決各個項目裡面如何實現sessionid的cookie跨域訪問即可,這個是可以實現的,就是比較麻煩,前後端有可能都要做處理。
如果不考慮以上三個問題,這種管理方式比較值得使用,尤其是一些小型的web應用。但是一旦應用將來有擴展的必要,那就得謹慎對待前面的三個問題。如果真要在項目中使用這種方式,推薦結合單點登錄框架如CAS一起用,這樣會使應用的擴展性更強。
2. cookie-based的管理方式
由於前一種方式會增加伺服器的負擔和架構的複雜性,所以後來就有人想出直接把用戶的登錄憑證直接存到客戶端的方案,當用戶登錄成功之後,把登錄憑證寫到cookie裡面,並給cookie設置有效期,後續請求直接驗證存有登錄憑證的cookie是否存在以及憑證是否有效,即可判斷用戶的登錄狀態。使用它來實現會話管理的整體流程如下:
1)用戶發起登錄請求,服務端根據傳入的用戶密碼之類的身份信息,驗證用戶是否滿足登錄條件,如果滿足,就根據用戶信息創建一個登錄憑證,這個登錄憑證簡單來說就是一個對象,最簡單的形式可以只包含用戶id,憑證創建時間和過期時間三個值。
2)服務端把上一步創建好的登錄憑證,先對它做數字簽名,然後再用對稱加密演算法做加密處理,將簽名、加密後的字串,寫入cookie。cookie的名字必須固定(如ticket),因為後面再獲取的時候,還得根據這個名字來獲取cookie值。這一步添加數字簽名的目的是防止登錄憑證里的信息被篡改,因為一旦信息被篡改,那麼下一步做簽名驗證的時候肯定會失敗。做加密的目的,是防止cookie被別人截取的時候,無法輕易讀到其中的用戶信息。
3)用戶登錄後發起後續請求,服務端根據上一步存登錄憑證的cookie名字,獲取到相關的cookie值。然後先做解密處理,再做數字簽名的認證,如果這兩步都失敗,說明這個登錄憑證非法;如果這兩步成功,接著就可以拿到原始存入的登錄憑證了。然後用這個憑證的過期時間和當前時間做對比,判斷憑證是否過期,如果過期,就需要用戶再重新登錄;如果未過期,則允許請求繼續。
這種方式最大的優點就是實現了服務端的無狀態化,徹底移除了服務端對會話的管理的邏輯,服務端只需要負責創建和驗證登錄cookie即可,無需保持用戶的狀態信息。對於第一種方式的第二個問題,用戶會話信息共用的問題,它也能很好解決:因為如果只是同一個應用做集群部署,由於驗證登錄憑證的代碼都是一樣的,所以不管是哪個伺服器處理用戶請求,總能拿到cookie中的登錄憑證來進行驗證;如果是不同的應用,只要每個應用都包含相同的登錄邏輯,那麼他們也是能輕易實現會話共用的,不過這種情況下,登錄邏輯裡面數字簽名以及加密解密要用到的密鑰文件或者密鑰串,需要在不同的應用裡面共用,總而言之,就是需要演算法完全保持一致。
這種方式由於把登錄憑證直接存放客戶端,並且需要cookie傳來傳去,所以它的缺點也比較明顯:
1)cookie有大小限制,存儲不了太多數據,所以要是登錄憑證存的消息過多,導致加密簽名後的串太長,就會引發別的問題,比如其它業務場景需要cookie的時候,就有可能沒那麼多空間可用了;所以用的時候得謹慎,得觀察實際的登錄cookie的大小;比如太長,就要考慮是非是數字簽名的演算法太嚴格,導致簽名後的串太長,那就適當調整簽名邏輯;比如如果一開始用4096位的RSA演算法做數字簽名,可以考慮換成1024、2048位;
2)每次傳送cookie,增加了請求的數量,對訪問性能也有影響;
3)也有跨域問題,畢竟還是要用cookie。
相比起第一種方式,cookie-based方案明顯還是要好一些,目前好多web開發平臺或框架都預設使用這種方式來做會話管理,比如php裡面yii框架,這是我們團隊後端目前用的,它用的就是這個方案,以上提到的那些登錄邏輯,框架也都已經封裝好了,實際用起來也很簡單;asp.net裡面forms身份認證,也是這個思路,這裡有一篇好文章把它的實現細節都說的很清楚:
http://www.cnblogs.com/fish-li/archive/2012/04/15/2450571.html
前面兩種會話管理方式因為都用到cookie,不適合用在native app裡面:native app不好管理cookie,畢竟它不是瀏覽器。這兩種方案都不適合用來做純api服務的登錄認證。要實現api服務的登錄認證,就要考慮下麵要介紹的第三種會話管理方式。
3. token-based的管理方式
這種方式從流程和實現上來說,跟cookie-based的方式沒有太多區別,只不過cookie-based裡面寫到cookie裡面的ticket在這種方式下稱為token,這個token在返回給客戶端之後,後續請求都必須通過url參數或者是http header的形式,主動帶上token,這樣服務端接收到請求之後就能直接從http header或者url裡面取到token進行驗證:
這種方式不通過cookie進行token的傳遞,而是每次請求的時候,主動把token加到http header裡面或者url後面,所以即使在native app裡面也能使用它來調用我們通過web發佈的api介面。app裡面還要做兩件事情:
1)有效存儲token,得保證每次調介面的時候都能從同一個位置拿到同一個token;
2)每次調介面的的代碼里都得把token加到header或者介面地址裡面。
看起來麻煩,其實也不麻煩,這兩件事情,對於app來說,很容易做到,只要對介面調用的模塊稍加封裝即可。
這種方式同樣適用於網頁應用,token可以存於localStorage或者sessionStorage裡面,然後每發ajax請求的時候,都把token拿出來放到ajax請求的header里即可。不過如果是非介面的請求,比如直接通過點擊鏈接請求一個頁面這種,是無法自動帶上token的。所以這種方式也僅限於走純介面的web應用。
這種方式用在web應用里也有跨域的問題,比如應用如果部署在a.com,api服務部署在b.com,從a.com裡面發出ajax請求到b.com,預設情況下是會報跨域錯誤的,這種問題可以用CORS(跨域資源共用)的方式來快速解決,相關細節可去閱讀前面給出的CORS文章詳細瞭解。
這種方式跟cookie-based的方式同樣都還有的一個問題就是ticket或者token刷新的問題。有的產品裡面,你肯定不希望用戶登錄後,操作了半個小時,結果ticket或者token到了過期時間,然後用戶又得去重新登錄的情況出現。這個時候就得考慮ticket或token的自動刷新的問題,簡單來說,可以在驗證ticket或token有效之後,自動把ticket或token的失效時間延長,然後把它再返回給客戶端;客戶端如果檢測到伺服器有返回新的ticket或token,就替換原來的ticket或token。
4. 安全問題
在web應用裡面,會話管理的安全性始終是最重要的安全問題,這個對用戶的影響極大。
首先從會話管理憑證來說,第一種方式的會話憑證僅僅是一個session id,所以只要這個session id足夠隨機,而不是一個自增的數字id值,那麼其它人就不可能輕易地冒充別人的session id進行操作;第二種方式的憑證(ticket)以及第三種方式的憑證(token)都是一個在服務端做了數字簽名,和加密處理的串,所以只要密鑰不泄露,別人也無法輕易地拿到這個串中的有效信息並對它進行篡改。總之,這三種會話管理方式的憑證本身是比較安全的。
然後從客戶端和服務端的http過程來說,當別人截獲到客戶端請求中的會話憑證,就能拿這個憑證冒充原用戶,做一些非法操作,而伺服器也認不出來。這種安全問題,可以簡單採用https來解決,雖然可能還有http劫持這種更高程度的威脅存在,但是我們從代碼能做的防範,確實也就是這個層次了。
最後的安全問題就是CSRF(跨站請求偽造)。這個跟代碼有很大關係,本質上它就是代碼的漏洞,只不過一般情況下這些漏洞,作為開發人員都不容易發現,只有那些一門心思想搞些事情的人才會專門去找這些漏洞,所以這種問題的防範更多地還是依賴於開發人員對這種攻擊方式的瞭解,包括常見的攻擊形式和應對方法。不管憑證信息本身多麼安全,別人利用CSRF,就能拿到別人的憑證,然後用它冒充別人進行非法操作,所以有時間還真得多去瞭解下它的相關資料才行。舉例來說,假如我們把憑證直接放到url後面進行傳遞,就有可能成為一個CSRF的漏洞:當惡意用戶在我們的應用內上傳了1張引用了他自己網站的圖片,當正常的用戶登錄之後訪問的頁面裡面包含這個圖片的時候,由於這個圖片載入的時候會向惡意網站發送get請求;當惡意網站收到請求的時候,就會從這個請求的Reffer header裡面看到包含這個圖片的頁面地址,而這個地址正好包含了正常用戶的會話憑證;於是惡意用戶就拿到了正常用戶的憑證;只要這個憑證還沒失效,他就能用它冒充用戶進行非法操作。
5. 總結
前面這三種方式,各自有各自的優點及使用場景,我覺得沒有哪個是最好的,做項目的時候,根據項目將來的擴展情況和架構情況,才能決定用哪個是最合適的。本文的目的也就是想介紹這幾種方式的原理,以便掌握web應用中登錄驗證的關鍵因素。
作為一個前端開發人員,本文雖然介紹了3種會話管理的方式,但是與前端關係最緊密的還是第三種方式,畢竟現在前端開發SPA應用以及hybrid應用已經非常流行了,所以掌握好這個方式的認證過程和使用方式,對前端來說,顯然是很有幫助的。好在這個方式的技術其實早就有很多實現了,而且還有現成的標準可用,這個標準就是JWT(json-web-token)。
JWT本身並沒有做任何技術實現,它只是定義了token-based的管理方式該如何實現,它規定了token的應該包含的標準內容以及token的生成過程和方法。目前實現了這個標準的技術已經有非常多:
更多可參閱:https://jwt.io/#libraries-io
為了對第三種會話管理方式的實現有個更全面的認識,我選擇用express和上面眾多JWT實現中的jsonwebtoken來研究,相關內容我會在下一篇博客詳細介紹。本文內容到此結束,謝謝閱讀,歡迎關註下一篇博客的內容。