先問小伙伴們一個問題,登錄難嗎?“登錄有什么難得?輸入用戶名和密碼,后台檢索出來,校驗一下不就行了。”凡是這樣回答的小伙伴,你明顯就是產品思維,登錄看似簡單,用戶名和密碼,后台校驗一下,完事了。但是,登錄這個過程涵蓋的知識點是非常多的,絕不是檢索數據,校驗一下這么簡單的事。
那么登錄都要哪些實現方式呢?i最傳統的就要是Cookie-Session這種方式了,最早的登錄方式都是這樣實現的。但是隨着手機端、H5端的興起,前后端分離的模式越來越流行,基於Cookie-Session這種登錄方式不是很方便,漸漸的JTW開始流行,現在大部分項目的登錄方式都是基於JWT的了。那么Cookie和JWT都是怎樣實現登錄的呢?這兩種方式有什么區別呢?我們在做登錄的x時候該怎么選擇呢?咱們先看看這兩種方式的原理。
Cookie方式
因為Http協議是無狀態的,我們后台的服務(如Tomcat)在接收到前端發送過來的Http請求時,是區分不出哪個請求是誰發出的,這和我們的登錄功能是相違背的,登錄的功能就是要區分每一個請求是由哪個用戶發出的,這就變成了有狀態,那怎么辦呢?Cookie應運而生,Cookie是存儲在瀏覽器端的,在Cookie中存儲的內容是鍵值對,也就是name-value。瀏覽器在向后台發送請求的時候,會把Cookie放在請求頭中,傳送給后台的服務,后台的服務會從請求頭中取到Cookie,再從Cookie中取出鍵值對中jsessionid對應的值。這個jsessionid的值就是你這次會話的id,對應着服務端的一個session。
好了,到這里session這個概念出來了,session是什么呢?session是存儲在服務端的,每一個會話對應服務中的一個session。咱們可以把session理解為一個Map,它的key存儲的session的id,value存儲的東西就隨便了,我們在寫程序時想存啥就存啥。它的key存儲的值就是Cookie中存儲的jsessionid的值,這樣,瀏覽器發送請求到后台服務,后台才能根據Cookie中的jsessionid取到對應的session,再從session中取到之前存儲的狀態,如存儲在session中的登錄狀態、用戶id等。Cookie-Session機制是通用的,所有的瀏覽器都支持Cookie,就連最低端的IE都支持,你說他普遍不普遍。Session是后端容器必須支持的,如Tomcat,還有像其他的如Resin、jetty等。這些對開發人員都是透明的,無需過多關注。
Cookie-Session的由來給大家說完了,我們看看基於Cookie這種方式的登錄流程,
- 用戶在瀏覽器輸入用戶名、密碼,點擊登錄,發送請求到后台服務;
- 后台服務校驗用戶名、密碼,將登錄狀態狀態和用戶id存儲在session中;
- 將session的id存儲在Cookie中,通過響應頭返回到瀏覽器;
- 當用戶點擊其他功能時,向后台發送的請求中會自動帶上Cookie;
- 后台通過Cookie中的jsessionid找到對應的session,開發人員可從session中取出當前會話的登錄狀態和用戶id。
基於Cookie-Session機制的登錄實現方式的整體流程就是這個樣子。看上去很完美,但還是存在不少問題的,我們來看看這些問題。
分布式會話
上面的示例,我們的后台服務只有一個,一個服務往往很難支撐服務,為了保障可靠性,最少都是部署兩個后台服務。但是當部署多個后台服務時,我們的session就會出現問題,看看下面的圖,
- 假如用戶登錄的請求,分配到了后台服務1,后台服務1的session存了用戶的登錄狀態和用戶id。
- 用戶在點擊其他功能時,請求分配到了后台服務2,可是后台服務2的session並沒有存儲登錄狀態和用戶id。
我們怎么解決這個問題呢?其實也很簡單,第一,session集中管理,比如使用Redis;第二,所有的后台服務在獲取session時,統一從Redis中獲取。如下所示,
我們可以使用中間件Spring-Session和Redis就可以解決這個問題。
CORS
使用Cookie實現登錄的另外一個問題就是跨域,現在往往都采用前后端分離的方式進行開發,在開發的過程中,前端和后端通常不在一個域下,由於瀏覽器的同源策略,Cookie不能傳入到后端。至於同源策略,不明白的小伙伴可以問一下度娘,這里不過多介紹了。要解決這個問題,在前端、后端都要進行設置,在我的另一篇文章《前后端分離|關於登錄狀態那些事》中有詳細的介紹。總體歸納為:
- 后端設置CORS允許跨域的域名,並且
withCredentials
設置為true; - 前端在向后端發送請求時,也需要設置
withCredentials = true
;
這樣,我們的Cookie就可以實現跨域了。不進行這些設置,Cookie跨域是不可能的,同源策略保證了我們Cookie的安全。
CSRF
CSRF,這個CORS是不一樣的,長的比較像,也比較容易混。CSRF往往和系統的安全扯上聯系,也是等保測試中比較重要的測試內容,它也是和Cookie有關的,大體的流程是這樣的,
- 用戶登錄了A網站,並沒有退出;
- 此時,用戶又訪問了B網站;
- 在B網站有個隱藏的請求,請求了A網站的一個重要的接口,比如:轉賬、支付等。
- 在請求A網站的同時,帶上了A網站的Cookie,所以一些危險的操作就成功了。
關於CSRF的攻防,在我前面的文章《CSRF的原理與防御 | 你想不想來一次CSRF攻擊?》中有詳細的介紹。總之,使用Cookie實現登錄是需要重點防范一下CSRF攻擊的。
JWT方式
近年來,由於手機端的興起,前后端分離開發方式的流行,JWT這種登錄的實現方式悄然興起,那么什么是JWT呢?JWT是英文JSON Web Token的縮寫,它由3部分組成,
- header,一般情況下存儲兩個信息,1類型,一般都是JWT;2加密算法,比如:HMAC、RSA等;
- payload,這里就存儲登錄的相關信息了,比如:登錄狀態、用戶id、過期時間等。
- signature,簽名,這個是將header、payload和密鑰的信息做一次加密,后台在接收到JWT的時候,一定要驗簽,謹防JWT的偽造。
下面咱們看看JWT的登錄實現,
我們看到整體的流程和Cookie的實現方式是一樣的,只不過是沒有用到Cookie、Session。那么它與Cookie-Session的區別是什么呢?
- 登錄狀態、用戶id並沒有存儲到session,而是存在JWT的payload里,返回給了前端。
- 在前端JWT不會自動存儲到Cookie中,前端開發人員要處理JWT的存儲問題,比如LocalStorage
- 再次發起請求,JWT不會自動放到請求頭中,需前端同學手動設置
- 后端從請求頭中取出JWT,驗簽通過后,拿到登錄狀態、用戶id,不是從session中取
相比Cookie的方式,JWT的方式需要更多的開發工作量。那么其他的問題存在嗎?我們一個一個看。
分布式會話
我們后台部署多個服務,會有分布式會話的問題嗎?
無論請求被分配到哪一個后台服務中,登錄狀態和用戶id都是從JWT中取出來的,不會出現分布式會話的問題。我們在后台部署集群的時候,根本不用care這個問題。
CORS
Cookie的跨域受到同源策略的保護,不經過特殊的設置,是不能夠跨域的。那么JWT呢?JWT是前端同學手動在請求頭中設置的,如果向其他的域發送請求要注意,稍不注意,在請求的時候,調用了封裝的公共方法,就會把JWT發送給其他域的后台,前端的小伙伴要打起精神啊。
CSRF
Cookie的方式,B訪問A網站,會把A的Cookie帶上,從而造成了安全隱患。那么JWT呢?JWT在前端存儲在A網站的域下,B在訪問A網站時,是拿不到A網站的JWT的,那么也不可能在請求頭中設置JWT,A網站的后台拿不到JWT,也不會做其他操作。所以,JWT可以很好的防止CSRF攻擊。
總結
通過前面我們對Cookie和JWT的分析,可以總結成如下的表格,
Cookie-Session | JWT | |
---|---|---|
工作量 | 瀏覽器和容器天然支持 | 需要額外開發,有一定工作量 |
分布式會話 | 需要借助中間件 | 無需關心,登錄信息從JWT解出 |
CORS | 不支持跨域、需特殊設置 | 開發人員設置請求頭,可以跨域 |
CSRF | 需特殊防范 | 無需防范,第三方拿不到JWT |
好了,Cookie和JWT的特點都總結出來了,大家在實現登錄的時候,就各取所需吧。結合自己的項目,選擇適合自己項目的實現方式吧。