天天看點

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

概念說明

浏覽器使用同源政策在提高了安全性的同時也會帶來一些不變,常見,如:不同源間的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。

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.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核心代碼如下:

跨站(cross-site)、跨域(cross-origin)、SameSite與XMLHttpRequest.withCredentials
跨站(cross-site)、跨域(cross-origin)、SameSite與XMLHttpRequest.withCredentials
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

跨站(cross-site)、跨域(cross-origin)、SameSite與XMLHttpRequest.withCredentials
跨站(cross-site)、跨域(cross-origin)、SameSite與XMLHttpRequest.withCredentials
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端靜态頁面,主要代碼如下:

跨站(cross-site)、跨域(cross-origin)、SameSite與XMLHttpRequest.withCredentials
跨站(cross-site)、跨域(cross-origin)、SameSite與XMLHttpRequest.withCredentials
<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>      

控制器代碼如下,用于模拟同源場景:

跨站(cross-site)、跨域(cross-origin)、SameSite與XMLHttpRequest.withCredentials
跨站(cross-site)、跨域(cross-origin)、SameSite與XMLHttpRequest.withCredentials
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後有告警圖示,滑鼠放上後可以看到相關說明:

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

對于HTTPS協定的API傳回的cookie,如果設定了屬性:

secure; samesite=none

,則浏覽器會存儲cookie。XHR請求也會帶上目标域的cookie:

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

該場景下,在開發者工具,應用面闆中看不到cookie,可以點選位址欄左側的Not secure标簽,在彈框中檢視存儲的cookie:

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

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:

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

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

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

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"

繼續閱讀