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來研究,相關內容我會在下一篇部落格詳細介紹。本文內容到此結束,謝謝閱讀,歡迎關注下一篇部落格的內容。