跨站(cross-site)、跨域(cross-origin)、SameSite与XMLHttpRequest.withCredentials
概念说明
浏览器使用同源策略在提高了安全性的同时也会带来一些不变,常见,如:不同源间的cookie或其它数据的访问。
跨站(cross-site)与跨域(cross-origin)是两个不同的概念。之前的文章同源策略与CORS已对什么是跨域作了说明,不再赘述,本文作为对之前文章的补充,以cookie的访问为切入点,介绍下跨站(cross-site)、跨域(cross-origin)、SameSite与XMLHttpRequest.withCredentials四个知识点。
⚠️ 浏览器的安全策略也在不断的变化,若干时间后文中所述内容可能不再适用
SameSite
与
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-siteAccess-Control
requests should be made using credentials such as cookies, authorization headers or TLS client certificates. SettingwithCredentials
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 unlesswithCredentials
is set to true before making the request. The third-party cookies obtained by settingwithCredentials
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核心代码如下:
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.错误。