概念說明
浏覽器使用同源政策在提高了安全性的同時也會帶來一些不變,常見,如:不同源間的cookie或其它資料的通路。
跨站(cross-site)與跨域(cross-origin)是兩個不同的概念。之前的文章同源政策與CORS已對什麼是跨域作了說明,不再贅述,本文作為對之前文章的補充,以cookie的通路為切入點,介紹下跨站(cross-site)、跨域(cross-origin)、SameSite與XMLHttpRequest.withCredentials四個知識點。
⚠️ 浏覽器的安全政策也在不斷的變化,若幹時間後文中所述内容可能不再适用
SameSite
與
XMLHttpRequest.withCredentials
針對的是cross-site或者same-site的情況,以下是MDN上對
SameSite
XMLHttpRequest.withCredentials
的概述:
SameSite主要用于限制cookie的通路範圍。
The SameSite attribute of the Set-Cookie
HTTP response header allows you to declare if your cookie should be restricted to a first-party or same-site context.
XMLHttpRequest.withCredentials主要針對XHR請求是否可以攜帶或者接受cookie。
Theproperty is a Boolean that indicates whether or not cross-site
XMLHttpRequest.withCredentials
requests should be made using credentials such as cookies, authorization headers or TLS client certificates. Setting
Access-Control
withCredentials
has 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.
from a different domain cannot set cookie values for their own domain unless
XMLHttpRequest
is set to true before making the request. The third-party cookies obtained by setting
withCredentials
to true will still honor same-origin policy and hence can not be accessed by the requesting sc
withCredentials
什麼是同站呢?舉個例子:
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();
});
}
}
}
View Code
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
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
samesite
samesite
Lax
Strict
- cross-site
secure; samesite=none
跨站一定跨域,反之不成立。文中代碼拷出來跑一跑,有助于了解文中内容。
幾個問題說明
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.錯誤。
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.錯誤。
推薦閱讀
當 CORS 遇到 SameSite
SameSite cookies
XMLHttpRequest.withCredentials
同源政策與CORS
Understanding "same-site" and "same-origin"