1. 程式人生 > 實用技巧 >跨站(cross-site)、跨域(cross-origin)、SameSite與XMLHttpRequest.withCredentials

跨站(cross-site)、跨域(cross-origin)、SameSite與XMLHttpRequest.withCredentials

概念說明

瀏覽器使用同源策略在提高了安全性的同時也會帶來一些不變,常見,如:不同源間的cookie或其它資料的訪問。

跨站(cross-site)與跨域(cross-origin)是兩個不同的概念。之前的文章同源策略與CORS已對什麼是跨域作了說明,不再贅述,本文作為對之前文章的補充,以cookie的訪問為切入點,介紹下跨站(cross-site)、跨域(cross-origin)、SameSite與XMLHttpRequest.withCredentials四個知識點。

⚠️ 瀏覽器的安全策略也在不斷的變化,若干時間後文中所述內容可能不再適用

SameSiteXMLHttpRequest.withCredentials

針對的是cross-site或者same-site的情況,以下是MDN上對SameSiteXMLHttpRequest.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。

The XMLHttpRequest.withCredentials property is a Boolean that indicates whether or not cross-site Access-Control requests should be made using credentials such as cookies, authorization headers or TLS client certificates. Setting 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. XMLHttpRequest from a different domain cannot set cookie values for their own domain unless withCredentials 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

什麼是同站呢?舉個例子:web.wjchi.comservice.wjchi.com具有相同的二級域名,可以看作是同站不同源(same-site, cross-origin)。但,web.github.ioservice.github.io則是不同的站點不同的源(cross-site, cross-origin),因為github.io屬於公共字尾(Public Suffix)。對於跨站問題,這兩篇文章都有講述:當 CORS 遇到 SameSite【譯】SameSite cookies 理解,可以參考閱讀。

測試程式碼

首先在本地對映幾個域名:

// 這兩個域名不同站也不同源,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 });
        }
    }
}
View Code

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>
View Code

控制器程式碼如下,用於模擬同源場景:

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 });
        }
    }
}
View Code

cookie訪問測試用例

same-origin

無限制,無論XMLHttpRequest.withCredentialstrue還是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的值為LaxStrict的cookie;

XHR請求會帶上目標域的cookie;

小結

同源時cookie的儲存與傳送沒有問題,頂級導航的情況可以看作是同源場景;

不同源場景,若XMLHttpRequest.withCredentials=false,則瀏覽器不會儲存cookie;

不同源場景,且XMLHttpRequest.withCredentials=true,又可分為以下場景:

  • same-site

    對於使用HTTPS協議的API,瀏覽器會儲存cookie,不論samesite的值;

    對於使用HTTP協議的API,瀏覽器會儲存samesite的值為LaxStrict的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的值為trueAccess-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請求中設定withCredentialstrue,但後臺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請求中設定withCredentialstrue,但後臺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