概念說明
瀏覽器使用同源策略在提高了安全性的同時也會帶來一些不變,常見,如:不同源間的cookie或其它數據的訪問。
跨站(cross-site)與跨域(cross-origin)是兩個不同的概念。之前的文章同源策略與CORS已對什么是跨域作了說明,不再贅述,本文作為對之前文章的補充,以cookie的訪問為切入點,介紹下跨站(cross-site)、跨域(cross-origin)、SameSite與XMLHttpRequest.withCredentials四個知識點。
⚠️ 瀏覽器的安全策略也在不斷的變化,若干時間后文中所述內容可能不再適用
SameSite與
SameSite主要用於限制cookie的訪問范圍。
The SameSite attribute of the
Set-CookieHTTP response header allows you to declare if your cookie should be restricted to a first-party or same-site context.
XMLHttpRequest.withCredentials主要針對XHR請求是否可以攜帶或者接受cookie。
The
XMLHttpRequest.withCredentialsproperty is a Boolean that indicates whether or not cross-siteAccess-Controlrequests should be made using credentials such as cookies, authorization headers or TLS client certificates. SettingwithCredentialshas no effect on same-site requests.In addition, this flag is also used to indicate when cookies are to be ignored in the response. The default is false.
XMLHttpRequestfrom a different domain cannot set cookie values for their own domain unlesswithCredentialsis set to true before making the request. The third-party cookies obtained by settingwithCredentialsto true will still honor same-origin policy and hence can not be accessed by the requesting sc
什么是同站呢?舉個例子:web.wjchi.com與service.wjchi.com具有相同的二級域名,可以看作是同站不同源(same-site, cross-origin)。但,web.github.io與service.github.io則是不同的站點不同的源(cross-site, cross-origin),因為github.io屬於公共后綴(Public Suffix)。對於跨站問題,這兩篇文章都有講述:當 CORS 遇到 SameSite、【譯】SameSite cookies 理解,可以參考閱讀。
2021-02-21補充:關於SameSite和SameOrigin的對比說明,可參考 Understanding "same-site" and "same-origin"
根據是否區分URL協議,又可分為 schemeful Same-Site 和 scheme-less same-site
測試代碼
首先在本地映射幾個域名:
// 這兩個域名不同站也不同源,cross-site, cross-origin 127.0.0.1 www.web.com 127.0.0.1 www.service.com // 這兩個域名是同站不同源,same-site, cross-origin 127.0.0.1 web.local.com 127.0.0.1 service.local.com
然后創建兩個ASP.NET Core項目,一個作為API,一個作為Web端。
API監聽以下地址:
http://www.service.com:5000
http://service.local.com:5001
https://www.service.com:5002
https://service.local.com:5003
Web端監聽以下地址:
http://www.web.com:5010
http://web.local.com:5011
https://www.web.com:5012
https://web.local.com:5013
API核心代碼如下:
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace cookie { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddCors(options => { options.AddPolicy("default", builder => { builder.AllowAnyHeader().AllowAnyMethod() .WithOrigins("http://www.web.com:5010", "http://web.local.com:5011", "https://www.web.com:5012", "https://web.local.com:5013") .AllowCredentials(); }); }); services.AddControllers(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseHttpsRedirection(); app.UseCors("default"); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } } }
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System; namespace cookie.Controllers { [ApiController] public class CookieController : ControllerBase { [HttpGet("")] public ActionResult Get() { var now = DateTime.Now; var nowFormat = $"{now.Hour}-{now.Minute}-{now.Second}-{now.Millisecond}"; Response.Cookies.Append($"service.cookie.{nowFormat}", $"service.cookie.value:{nowFormat}"); Response.Cookies.Append($"service.cookie.none.{nowFormat}", $"service.cookie.value.none:{nowFormat}", new CookieOptions() { Secure = true, SameSite = SameSiteMode.None }); Response.Cookies.Append($"service.cookie.Strict.{nowFormat}", $"service.cookie.value.Strict:{nowFormat}", new CookieOptions() { SameSite = SameSiteMode.Strict }); return Ok(); } [HttpPost("")] public ActionResult Post() { if (Request.Cookies.TryGetValue("service.cookie", out var cookieValue) == false) { cookieValue = "none"; } return new JsonResult(new { cookieValue }); } } }
Web端靜態頁面,主要代碼如下:
<body>
<div>
<button onclick="getCookie('http://www.service.com:5000')">獲取cookie</button>
<button onclick="getCookie('http://service.local.com:5001')">獲取本地cookie</button>
<button onclick="getCookie('https://www.service.com:5002')">HTTPS獲取cookie</button>
<button onclick="getCookie('https://service.local.com:5003')">HTTPS獲取本地cookie</button>
</div>
<br />
<div>
<button onclick="sendCookie( 'http://www.service.com:5000')">發送cookie</button>
<button onclick="sendCookie( 'http://service.local.com:5001')">發送本地cookie</button>
<button onclick="sendCookie( 'https://www.service.com:5002')">HTTPS發送cookie</button>
<button onclick="sendCookie( 'https://service.local.com:5003')">HTTPS發送本地cookie</button>
</div>
<br />
<div>
<button onclick="getCookie('http://www.web.com:5010/web')">獲取同源cookie</button>
<button onclick="getCookie('https://www.web.com:5012/web')">HTTPS獲取同源cookie</button>
</div>
<br />
<div>
<button onclick="sendCookie( 'http://www.web.com:5010/web')">發送同源cookie</button>
<button onclick="sendCookie( 'https://www.web.com:5012/web')">HTTPS發送同源cookie</button>
</div>
<script>
function getCookie(url) {
var xhr = new XMLHttpRequest();
xhr.onload = function (e) {
console.log(e);
}
xhr.withCredentials = true;
xhr.open('GET', url);
xhr.send();
}
function sendCookie(url) {
var xhr = new XMLHttpRequest();
xhr.onload = function (e) {
console.log(e);
}
xhr.withCredentials = true;
xhr.open('POST', url);
xhr.send();
}
</script>
</body>
控制器代碼如下,用於模擬同源場景:
using Microsoft.AspNetCore.Mvc; namespace web.Controllers { [Route("[controller]")] public class WebController : ControllerBase { [HttpGet] public ActionResult Get() { Response.Cookies.Append("web.cookie."+Request.Scheme, "web.cookie.value:" + Request.Scheme); return Ok(); } [HttpPost] public ActionResult Post() { if (Request.Cookies.TryGetValue("web.cookie", out var cookieValue) == false) { cookieValue = "none"; } return new JsonResult(new { cookieValue }); } } }
cookie訪問測試用例
same-origin
無限制,無論XMLHttpRequest.withCredentials是true還是false,瀏覽器均可存儲cookie,XHR請求中均會帶上cookie。
頂級導航(top-level navigation),即瀏覽器地址欄中直接輸入地址,瀏覽器會存儲cookie,不論cookie的samesite的值是多少。
XMLHttpRequest.withCredentials=false,cross-origin,same-site
這種場景下,cookie不會被瀏覽器存儲。
XMLHttpRequest.withCredentials=false,cross-origin,cross-site
這種場景下,cookie不會被瀏覽器存儲。
XMLHttpRequest.withCredentials=true,cross-origin,cross-site
對於使用HTTP協議的API返回的cookie,瀏覽器不會存儲,在瀏覽器開發者工具,網絡面板中可以看到set-cookie后有告警圖標,鼠標放上后可以看到相關說明:

對於HTTPS協議的API返回的cookie,如果設置了屬性:secure; samesite=none,則瀏覽器會存儲cookie。XHR請求也會帶上目標域的cookie:

該場景下,在開發者工具,應用面板中看不到cookie,可以點擊地址欄左側的Not secure標簽,在彈框中查看存儲的cookie:

XMLHttpRequest.withCredentials=true,cross-origin,same-site
對於使用HTTPS協議的API,瀏覽器會存儲cookie,不論samesite的值;
對於使用HTTP協議的API,瀏覽器會存儲samesite的值為Lax和Strict的cookie;
XHR請求會帶上目標域的cookie;
小結
同源時cookie的存儲與發送沒有問題,頂級導航的情況可以看作是同源場景;
不同源場景,若XMLHttpRequest.withCredentials=false,則瀏覽器不會存儲cookie;
不同源場景,且XMLHttpRequest.withCredentials=true,又可分為以下場景:
-
same-site
對於使用HTTPS協議的API,瀏覽器會存儲cookie,不論
samesite的值;對於使用HTTP協議的API,瀏覽器會存儲
samesite的值為Lax和Strict的cookie;XHR請求會帶上目標域的cookie;
-
cross-site
對於HTTPS協議的API返回的cookie,如果設置了屬性:
secure; samesite=none,則瀏覽器會存儲cookie。XHR請求也會帶上目標域的cookie:
跨站一定跨域,反之不成立。文中代碼拷出來跑一跑,有助於理解文中內容。
幾個問題說明
HTTPS vs HTTP
HTTPS頁面發送的XHR請求目標地址也必須是HTTS協議,否則會報 Mixed Content: The page at 'https://www.web.com:5012/index.html' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint 'http://www.web.com:5010/web'. This request has been blocked; the content must be served over HTTPS.錯誤。
瀏覽器不信任信任ASP.NET Core自帶CA證書
ASP.NET Core自帶的CA證書會被瀏覽器認為不安全,在頁面上通過XHR請求調用HTTPS接口時會出現ERR_CERT_COMMON_NAME_INVALID錯誤,瀏覽器網絡面板中請求頭也會出現警告Provisional headers are shown:

我們可以通過在瀏覽器地址欄中直接輸入GET請求的接口地址,然后選擇繼續訪問即可解決該問題:

XMLHttpRequest.withCredentials與Access-Control-Allow-Credentials、Access-Control-Allow-Origin
后端API同時設置Access-Control-Allow-Credentials的值為true,Access-Control-Allow-Origin的值為*會報The CORS protocol does not allow specifying a wildcard (any) origin and credentials at the same time. Configure the CORS policy by listing individual origins if credentials needs to be supported.錯誤。
若前端XHR請求中設置withCredentials為true,但后台API未設置Access-Control-Allow-Credentials,則會報The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.錯誤。
若前端XHR請求中設置withCredentials為true,但后台API配置Access-Control-Allow-Origin的值為*,則會報The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.錯誤。
