寫在前面
最近小楓在工作時遇到一個問題,經過幾天的思索和探究終於找到了問題所在,覺得有點價值便寫了這篇文章記錄下來,分享給熱愛學習、樂於思考的各位,希望每個遇到相同情況的人可以通過閲讀這篇文章得到解答。
話不多説,開始吧!
問題
小楓最近在開發一個平台,這裏統稱為A平台;A平台由兩個部分構成:
- 前端部分
- 後端部分
可以看出,這是前後端分離開發的模式,前端和後端的正式環境都為:http://bin.ruofee.cn(題外話:SN戰隊很棒了,希望明年再度捧起LPL的榮光)。PS:本篇文章中的域名都是虛構的,如有雷同,純屬巧合。(-.-)
因為公司還有許多平台,考慮到用户信息的安全性,需要登錄平台統一進行登錄狀態管理,因此也有個統一登錄平台: http://login.ruofee.cn,這裏統稱為Login平台。
平台的登錄流程如下:
- 瀏覽器打開A平台,首先進行用户狀態判斷:判斷是否存在auth_token(auth_token是Login平台設置到瀏覽器中的cookie,用於登錄狀態保持),如果auth_token不存在則表示A平台未進行登錄操作(當然也有可能進行過登錄操作,但cookie已經過期),通知瀏覽器跳轉到Login平台進行賬號密碼登錄,如(4);如果auth_token存在則表示A平台已經進行過登錄操作,進行下一步操作;
- A平台後端接口根據業務提供了一個接口:http://bin.ruofee.cn/api/validate,用於驗證auth_token是否有效,如果有效則返回用户的個人信息,如果無效則通知瀏覽器跳轉Login平台重新登錄,如(3)。/validate接口的邏輯很簡單,直接訪問Login平台提供的驗證接口(http://login.ruofee.cn/auth)進行auth_token驗證。
- 瀏覽器不存在auth_token或者auth_token失效時都會跳轉到Login平台進行賬號密碼登錄,登錄成功時Login平台會將新的auth_token設置為cookie,保存在瀏覽器中,用於保持當前的登錄狀態;
以上就是平台登錄的大致流程,可以看出,平台如果想接入統一登錄服務,關鍵的點在於A平台前端在訪問A平台後端時,是否可以自動帶上Login平台設置的cookie(auth_token),從而進行後面的auth_token校驗流程。
那麼來進行簡單的分析:
Login平台在登錄成功時,瀏覽器通過Response Headers中的Set-Cookie進行cookie設置:
# Login平台登錄成功時設置cookie
Set-Cookie: auth_token=xxx; domain=ruofee.cn; path=/;
從Set-Cookie的結構可以看出:
- auth_token的domain為http://ruofee.cn,而不是login.ruofee.cn;
- path為/;
因為auth_token設置的domain為http://ruofee.cn,所以訪問http://ruofee.cn或是http://ruofee.cn的子域名時都將會自動帶上auth_token。但由於同源策略對cookie的限制,分為以下兩種情況:
- 同源:請求將會自動帶上對應的cookie,無需做其他設置;
- 非同源:需要做跨域處理,在Response Headers做以下配置:
# Response Header
Access-Control-Allow-Origin: 前端Origin # 注意,這裏不能為*,如果設置為*將不會帶上cookie
Access-Control-Allow-Headers: 允許接收的Request Headers # 根據需要進行設置
Access-Control-Allow-Credentials: true
Ajax中需要把withCredentials設置為true,如下:
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/', true);
xhr.withCredentials = true;
xhr.send(null);
MDN中對Access-Control-Allow-Credentials的解釋如下:
Access-Control-Allow-Credentials頭 工作中與XMLHttpRequest.withCredentials或Fetch API中的Request()構造器中的credentials選項結合使用。Credentials必須在前後端都被配置(即theAccess-Control-Allow-Credentialsheader 和 XHR 或Fetch request中都要配置)才能使帶credentials的CORS請求成功。
進行以上設置之後便可以自動帶上cookie了!
| 地址 | 協議 | Host | Port |
|---|---|---|---|
| http://bin.ruofee.cn | HTTP | bin.ruofee.cn | 80 |
| http://bin.ruofee.cn/api | HTTP | bin.ruofee.cn | 80 |
從上表可以看出,A平台前端和後端屬於第一種情況:滿足同源策略;因此從理論上來説,A平台前端請求A平台後端接口時,會自動帶上cookie(auth_token)。而事實上確實也成功了,小楓因此決定吃一頓麥當勞獎勵一下自己。(^__^)
為了方便開發,小楓決定將平台前端部署到本地(總不能在服務器上開發吧),開啓服務:localhost:8080。
| 地址 | 協議 | Host | Port |
|---|---|---|---|
| http://localhost:8080 | HTTP | localhost | 8080 |
| http://bin.ruofee.cn/api | HTTP | bin.ruofee.cn | 80 |
從上表可以知道,本地開啓的前端服務與線上的後端服務存在跨域,因此需要參考上面講的非同源情況,設置跨域相關Headers;在全部設置完畢之後,本地開發一切正常,cookie能順利帶上,小楓加快步伐,逐步完成開發。
這時候讀者肯定就鬱悶了,説好的問題呢?莫急,接下來進入正題!
就在2020年5月份的某一天,小楓如往常一樣打開本地的前端服務,準備開始一天的忙碌,卻突然一直重複登錄;打開chrome的devtools查看http請求發送情況才發現,本地前端請求接口時(http://bin.ruofee.cn/api/validate),auth_token沒有自動帶上,因此後端判斷前端平台為未登錄狀態,於是重定向到Login平台進行登錄,Login平台登錄成功跳轉回本地前端,再次訪問接口(http://bin.ruofee.cn/api/validate),auth_token沒有自動帶上,於是再次重定向到Login平台進行登錄...
小楓撓頭,開始一步一步進行檢查,發現問題卡在流程中的(5),雖然登錄成功了,返回的Response Headers如下:
Set-Cookie: auth_token=xxx; domain=ruofee.cn; path=/;
Response Headers很正常,但是請求後端接口時沒有自動帶上cookie,小楓陷入思考,發現有兩種可能:
- cookie沒有設置到瀏覽器中;
- cookie設置成功,但因為一些原因未在接口請求時自動帶上;
為了先驗證cookie是否成功設置到瀏覽器中,小楓先清除了chrome中所有的cookie(從零開始嘛),在本地進行登錄操作,此時平台一直重複登錄,接着打開線上的A平台,發現A平台並未跳轉到Login平台,説明此時Login平台的auth_token已經設置到瀏覽器中。所以可以得出結論:cookie設置成功,但因為一些原因未在接口請求時自動帶上。從這個方向出發進行思索和探究,終於找到了原因!
這裏我們先科普cookie的幾個屬性:
| 屬性 | 描述 | 值 | 默認值 |
|---|---|---|---|
| domain | Domain 指定了哪些主機可以接受 Cookie。如果不指定,默認為 origin,不包含子域名。如果指定了Domain,則一般包含子域名 | 當前origin或者父origin | 當前origin |
| path | Path 標識指定了主機下的哪些路徑可以接受 Cookie | 任意值 | 當前路徑 |
| SameSite | SameSite Cookie 允許服務器要求某個 cookie 在跨站請求時不會被髮送,(其中 Site 由可註冊域定義),從而可以阻止跨站請求偽造攻擊(CSRF)。 | 1. None。瀏覽器會在同站請求、跨站請求下繼續發送 cookies,設置為None時需要Secure同時設置為true才生效; 2. Strict。瀏覽器將只在訪問相同站點時發送 cookie;3. Lax。與 Strict 類似,但用户從外部站點導航至URL時(例如通過鏈接)除外。 在新版本瀏覽器中,為默認選項,Same-site cookies 將會為一些跨站子請求保留,如圖片加載或者 frames 的調用,但只有當用户從外部站點導航到URL時才會發送。如 link 鏈接 | 默認為空,但部分瀏覽器為Lax |
| Expires/Max-Age | cookie的生命週期 | session | |
| Secure | 標記為 Secure 的 Cookie 只應通過被 HTTPS 協議加密過的請求發送給服務端 | true/false | 默認為空 |
| HttpOnly | JavaScript 中的Document.cookie API 無法訪問帶有 HttpOnly 屬性的cookie;此類 Cookie 僅作用於服務器 | true/false | 默認為空 |
看了上面的Cookie屬性表,聰明的同學應該已經知道原因了~原因就出在SameSite:
當SameSite為Strict或是Lax時,通過Ajax進行cross-site的接口請求時,將不會自動帶上cookie;SameSite的值默認為空,但部分瀏覽器默認為”SameSite=Lax“小楓通過上網查閲資料,終於在Chrome 80版本的更新日誌中找到原因:
Chrome 80: February 4, 2020 Updates to cookies with SameSite
Starting in Chrome 80, cookies that don’t specify a SameSite attribute will be treated as if they were SameSite=Lax. Cookies that still need to be delivered in a cross-site context can explicitly request SameSite=None. Cookies with SameSite=None must also be marked Secure and delivered over HTTPS. To reduce disruption, the updates will be enabled gradually, so different users will see it at different times. We recommend that you test critical sites using the instructions for testing.
中文翻譯為:
Chrome 80: February 4, 2020 更新Cookie的SameSite
從Chrome 80開始,未指定SameSite屬性的Cookie將被視為SameSite = Lax。仍然需要在跨站點中傳遞的Cookie可以設置為SameSite = None。具有SameSite = None的Cookie也必須設置為Secure,並通過HTTPS傳送。為了減少中斷,將逐步啓用更新,因此不同的用户將在不同的時間看到它。我們建議您按照測試説明來測試關鍵站點。
所以在2020年5月之前跨站的cookie仍能正常攜帶,而在2020年5月之後Chrome更新之後,Chrome 80將Cookie的SameSite默認設置為Lax,因此本地部署的平台在訪問跨站的線上後端接口時,cookie將不再可以自動在Headers中帶上。
這裏再貼一下SameSite=Lax的作用:
當SameSite=Lax時,瀏覽器只在訪問相同站點時發送 cookie,但用户從外部站點導航至URL時(例如通過鏈接)除外。 在新版本瀏覽器中,為默認選項,Same-site cookies 將會為一些跨站子請求保留,如圖片加載或者 frames 的調用,但只有當用户從外部站點導航到URL時才會發送。如 link 鏈接
事實上,如果是跨站點設置cookie,Chrome甚至會阻攔Set-Cookie生效:
使用Express在線上服務器搭建一個簡單的Node服務器,並提供一個設置cookie的接口:
app.use('/cookie', (req, res) => {
res.append('Set-Cookie', 'cookie=test_cookie; domain=ruofee.cn; path=/;');
res.send('success');
});
在解決了跨域問題之後,在本地搭建的前端平台訪問該線上接口(http://bin.ruofee.cn/api/cookie),從chrome的devtools - network中查看Response Headers的Set-Cookie,發現有着這麼一段話:
This Set-Cookie didn't specify a "SameSite" attribute and was defaulted to "SameSite=Lax" and was blocked because it came from a cross-site response which not the response to a top-level navigation. The Set-Cookie had to have been set with "SameSite=None" to enable cross-site usage.
中文翻譯為:
當Set-Cookie中的”SameSite“沒有設置值時,默認為”SameSite=Lax“,並且因為Set-Cookie來自於一個跨站點的響應,導致Set-Cookie被阻攔。如果需要跨站點設置cookie,Set-Cookie必須設置為”SameSite=None“。
chrome瀏覽器提示
這裏再科普一下cross-site和cross-origin的區別:
- cross-site,意為跨站;site指的是ETLD+1(有效頂級域名左邊加一個子域名,例如http://ruofee.cn即為一個ETLD+1),若是兩個url的site不同,則表示跨站;
- cross-origin,意為跨域;origin是協議頭、主機名、端口的合併,因此若是兩個url協議頭、主機名、端口中有一個不相同則表示跨域;
貼一篇總結cross-site、cross-origin的文章:
Understanding "same-site" and "same-origin"web.dev
總結
小楓遇到的本地環境重複登錄的根本原因在於:Chrome 80將SameSite的默認值修改為Lax,導致cookie無法在跨站的情況下發送。而Chrome之所以對cookie作出調整是出於安全性的考量,具體可以參考這篇文章:
back2wild:即將到來的Chrome新的Cookie策略zhuanlan.zhihu.com
SameSite=Strict/Lax使用户基本杜絕CSRF攻擊,並且避免因為cookie可以跨站發送而導致用户行為被追蹤。
總而言之,Cookie默認設置”SameSite=Lax“是瀏覽器的行為,是Chrome 80的一次更新;Chrome以推動者的身份進行這項改動,或許在今後,所有的瀏覽器都將會跟進,並完善web安全,讓用户真正擁有隱私,又或許在今後,Chrome從屠龍少年變成惡龍,走上IE的道路……
解決方案
- 通過代理或是修改Host文件的方式,將本地前端地址的ETLD+1修改為http://ruofee.cn;cookie設置為”SameSite=Lax“時不能跨站發送Cookie,因此只要避免跨站即可解決問題;
- 本地搭建一個轉發服務器,將本地前端發送的請求轉發到線上後端服務;同樣避免跨站,本地的轉發服務器和本地前端平台的site都是localhost,因此不存在跨站現象;
- 更換瀏覽器;當前只有Chrome 80默認修改了SameSite的值,其他瀏覽器仍然保持原樣,因此不存在cookie不能跨站發送的情況;
結尾
完美撒花,感謝大家的閲讀,以上就是《關於我因為登錄失敗開始探究Cookie這檔事》。
”長按點贊可以一鍵三連哦!“
(^__^)