携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第N天,点击查看活动详情 >>
什么是跨域
同源策略(same origin policy)
同源 = 协议 + 主机 + 端口 + 子域名 均相同
如果没有同源策略,则会发生在 a.com 的脚本去获取 b.com 的站点里的信息
同源策略限制主体分类
- Ajax同源策略:无法向非同源地址发送 AJAX 请求 或 fetch 请求(可以发送,但浏览器拒绝接受响应)
DOM同源策略也一样,它限制了不同源页面不能获取DOM,这样可以防止一些恶意网站在自己的网站中利用iframe嵌入正gui的网站并迷惑用户,以此来达到窃取用户信息。- storage同源策略:无法获取非同源网页的 cookie、localstorage 和 indexedDB。个人理解这一句话主要说的是 比如浏览完 A 页面后去 浏览 B 页面,那么在 B 页面拿不到 A 页面的 Cookie
为什么要有跨域
- cookie 可以跨域的话必然造成身份信息泄露 → CSRF(跨站请求伪造)
- DOM 可以跨域获取的话:iframe 嵌套 → 获取用户的输入信息
可以跨域的标签
<script>、<img>、<iframe>、<link>;
为什么他们可以跨域?这些标签本质上是去加载一种资源,使用的是GET请求,对于其返回的结果,开发人员的 JS脚本无法去读写,也就不会对客户端造成危害
XHR:同源策略的目标
通过目标域的HTTP头来授权是否允许跨域,因为开发者无法在HTTP到达浏览器之前篡改头部来欺骗浏览器
CORS
基础概念
CORS 是一个 W3C 标准,全称是 跨域资源共享(Cross-origin resource sharing) ,它允许浏览器向跨源服务器,发出XMLHttpRequest请求。
客户端请求头
OPTIONS /oapi/get HTTP/2
Host: position.csdnimg.cn
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:102.0) Gecko/20100101 Firefox/102.0
Access-Control-Request-Method: GET
Access-Control-Request-Headers: content-type
Referer: <https://blog.csdn.net/>
**Origin: <https://blog.csdn.net>**
Connection: keep-alive
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
🚀 Origin 标记发起请求的 源URL,用来判断是否同源~
-
Access-Control-Request-Method: GET
-
Access-Control-Request-Headers: content-type
-
Sec-Fetch-Dest: empty
- 表示请求的目的地,即如何使用这个元素,可以是比如 img / video 之类的
-
Sec-Fetch-Mode: cors
cors:跨域请求;no-cors:限制请求只能使用请求方法(get/post/put)和请求头(accept/accept-language/content-language/content-type);same-origin:如果使用此模式向另外一个源发送请求,显而易见,结果会是一个错误。你可以设置该模式以确保请求总是向当前的源发起的;navigate:表示这是一个浏览器的页面切换请求(request)。 navigate请求仅在浏览器切换页面时创建,该请求应该返回HTML;websocket:建立websocket连接;
💡 具体解释 1. cors表示跨域请求,且要求后端需要设置cors响应头;【OPTION也是这个呢】 2. no-cors并不是代表请求不跨域,而是服务端不设置cors响应头,什么情况下会是这种模式呢,图片/脚本/样式表这些请求是容许跨域且不用设置跨域响应头的,而no-cors也是默认的模式; 4. same-origin表示同源请求,这就限制了不能跨域,前面说的cors和no-cors是容许跨域的,只是要求服务端的设置不同而已,熟悉fetch接口的同学对mode属性应该不陌生,其实跟这里的含义是一样的,只是fetch的mode大家可以手动设置,而Sec-Fetch-Mode不能干预而已;
-
Sec-Fetch-Site: cross-site
-
cross-site:跨域请求; -
same-origin:发起和目标站点源完全一致; -
same-site:有几种判定情况,详见说明;- 一级域名 → 二级域名 【不跨域】
- 二级域名 → 另一个二级域名 【跨域】
- 二级域名 → 一级域名 【跨域】
none:如果用户直接触发页面导航,例如在浏览器地址栏中输入地址,点击书签跳转等,就会设置none;
-
服务端响应头
HTTP/2 204 No Content
date: Tue, 26 Jul 2022 08:23:26 GMT
access-control-allow-origin: <https://blog.csdn.net>
access-control-allow-methods: GET,PUT,POST,DELETE,OPTIONS
access-control-allow-credentials: true
access-control-allow-headers: Accept,Authorization,Cache-Control,
Content-Type,DNT,If-Modified-Since,JWT-TOKEN,Keep-Alive,Origin,
User-Agent,UserName,UserToken,X-Access-Token,X-App-ID,X-CustomHeader,
X-Data-Type,X-Device-ID,X-Device-Signature,X-Mx-ReqToken,X-OS,X-Requested-With,
body,uber-trace-id,x-csrf-token,x-log-apiversion,x-log-bodyrawsize,
x-log-requestid,x-token,x-ca-signature-headers
access-control-max-age: 1728000
- [必含] Access-Control-Allow-Origin : 允许的域名,只能填
*(通配符)或者单域名 - [必含] Access-Control-Allow-Methods : 这允许跨域请求的 http 方法(常见有
POST、GET、OPTIONS)。 - [可选] Access-Control-Allow-Headers : 这是对预请求当中
Access-Control-Request-Headers的回复,和上面一样是以逗号分隔的列表,可以返回所有支持的头部。 - [可选] Access-Control-Allow-Credentials :表示是否允许发送Cookie,只有一个可选值:true(必为小写)如果不包含cookies,请略去该项,而不是填写false。这一项与 XmlHttpRequest 对象当中的
withCredentials属性应保持一致,即 withCredentials 为true时该项也为true;withCredentials 为false时,省略该项不写。反之则导致请求失败。 - [可选] Access-Control-Max-Age: 以秒为单位的缓存时间。在有效时间内,浏览器无须为同一请求再次发起预检请求。
CORS 跨域的判定流程
- 浏览器 判断是否符合同域的限制,同域 → 直接发送;不同域名 → 跨域请求(Sec-Fetch-Site: cross-site)。
- 服务器收到浏览器跨域请求后,根据自身配置返回对应HTTP头。若未配置过任何允许跨域,则HTTP头里不包含
Access-Control-Allow-origin字段,若配置过域名,则返回Access-Control-Allow-origin + 对应配置规则里的域名的方式。 - 浏览器根据接受到的 响应头里的
Access-Control-Allow-origin字段做匹配,若无该字段,说明不允许跨域,从而抛出一个错误;若有该字段,则对字段内容和当前域名做比对,如果同源,则说明可以跨域,浏览器接受该响应;若不同源,则说明该域名不可跨域,浏览器不接受该响应,并抛出一个错误。
上面说到的两种类型的报错,控制台输出是不一样的:
- 服务器允许跨域请求,但是 Origin 指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含
Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被 XMLHttpRequest的onerror 回调函数捕获。注意,这种错误无法通过状态码识别,因为 HTTP 回应的状态码有可能是200。
<!--控制台返回结果-->
XMLHttpRequest cannot load <http://localhost/city.json>.
The 'Access-Control-Allow-Origin' header has a value '<http://segmentfault.com>' that is not equal to the supplied origin.
Origin '<http://www.zhihu.com>' is therefore notallowed access.
复制代码
- 服务器不允许任何跨域请求
<!--控制台返回结果-->
XMLHttpRequest cannot load <http://localhost/city.json>.
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin '<http://www.zhihu.com>' is therefore not allowed access.
复制代码
预检请求 Option
对于简单请求不会发送预检请求,会直接发送请求,此刻要发送的信息已经被服务器接受 → 可能会写入数据库
对于复杂请求会发送预检请求,预检不通过就不发送消息 → 不会危害服务器
简单请求:使用 form 表单可以发出的
简单请求是指满足以下条件的(一般只考虑前面两个条件即可):
-
使用
GET、POST、HEAD其中一种请求方法。 -
HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值
application/x-www-form-urlencoded、multipart/form-data、text/plain
-
请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;
-
XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。 请求中没有使用 ReadableStream 对象。
对于简单请求,浏览器直接发起 CORS 请求,具体来说就是服务器端会根据请求头信息中的 origin 字段(包括了协议 + 域名 + 端口),来决定是否同意这次请求。
如果 origin 指定的源在许可范围内,服务器返回的响应,会多出几个头信息字段:
Access-Control-Allow-Origin: <http://xxx.xxx.com>
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
非简单请求
非简单请求时指那些对服务器有特殊要求的请求,比如请求方法是 put 或 delete,或者 content-type 的类型是 application/json。其实简单请求之外的都是非简单请求了。
非简单请求的 CORS 请求,会在正式通信之前,使用 OPTIONS 方法发起一个预检(preflight)请求到服务器,浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。
一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样了。
前端请求携带Cookie
-
前端请求时在
request对象中配置"withCredentials": true;XMLHttpRequest.withCredentials : 指示了是否该使用类似
cookies,authorization headers(头部授权)或者TLS客户端证书这一类资格证书来创建一个跨域请求。在同一个站点下使用
withCredentials属性是无效的。 -
服务端在
response的header中配置"Access-Control-Allow-Origin", "<http://xxx>:${port}";不能设置为 “*”
-
服务端在
response的header中配置"Access-Control-Allow-Credentials", "true"要么不设置,要么就设置成 “true”, 不能设置成 false
JSONP
JSONP,全称 JSON with Padding,为了解决跨域的问题而出现。利用 <script> 这种标签发起的 资源性请求 不受跨域限制,所以只限于 get 请求。
JSONP 基于两个原理:
- 动态创建
script,使用script.src加载请求跨过跨域 script.src加载的脚本内容为 JSONP: 即PADDING(JSON)格式
例如 script.src = "<https://demo/api/user?id=100&callback=padding>"
此刻浏览器会去加载这个 js 文件,后端要配合,从query部分摘出参数,以及将要执行的回调 callback=padding, 此刻前端一定在 window.padding 设置好了回调函数,只需要后端返回一个脚本字符串,在字符串里调用这个 padding,HTTP响应后,前端自然会执行这个回调
例如后端返回
let response = 'padding({name:'linjunjie'})'
//定义获取数据的回调方法
function getData(data) {
console.log(data);
}
// 创建一个script标签,并且告诉后端回调函数名叫 getData
var body = document.getElementsByTagName('body')[0];
var script = document.gerElement('script');
script.type = 'text/javasctipt';
script.src = 'demo.js?callback=getData';
body.appendChild(script);
//script 加载完毕之后从页面中删除,否则每次点击生成许多script标签
script.onload = function () {
document.body.removeChild(script);
}
webpack-dev-server
Webpack-dev-server
在编译之后不会写入到任何输出文件, 而是将bundle文件保留在内存中
Webpack Dev Middleware
webpack-dev-middleware 是一个封装器,它可以把webpack处理过的文件发送到一个server
- webpack-dev-server在内部使用了它,然而它也可以作为一个单独的package来使用,以便根据需求进行更多自定义配置
- 搭配一个服务器来使用它,比如express.
npm install --save express webpack-dev-middleware
const express = require("express")
const webpack = require("webpack")
const webpackDevMiddleware = require("webpack-dev-middleware")
const app = express()
const config = require("./webpack.config")
const compiler = webpack(config)
app.use(webpackDevMiddleware(compiler,{
publicPath:config.output.publicPath
}),()=>{
console.log("这里是回调函数")
})
app.listen(3000,()=>{
console.log("Server running")
})
Proxy
-
target:标识的是代理到的目标地址,比如/api/moment会被代理到 http://localhost:8888/api/moment
-
pathRewrite:默认情况下,我们的/api也会被写入到URL中,如果希望删除,可以使用
-
secure:默认情况下不接受转发到https的服务器,如果希望支持,设置为false
-
changeOrigin:表示是否更新代理后请求headers中的host地址
-
historyApiFallback:解决SPA页面在路由跳转后,进行页面刷新返回404的错误
- boolean值:默认是false,如果设置为true,刷新的时候,返回404错误时,会自动返回index.html的内容
- object值:可以配置rewrites属性 , 可以配置from来匹配路径,决定要跳到哪个页面,详情查阅官方文档。
document.domain
该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。
只需要给两个页面都添加 document.domain = 'test.com',通过在 a.test.com 创建一个 iframe,去控制 iframe 的 window,从而进行交互。
postMessage
window.postMessage 是一个 HTML5 的 api,允许两个窗口之间进行跨域发送消息。
这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息
// 发送消息端
var receiver = document.getElementById('receiver').contentWindow;
var btn = document.getElementById('send');
btn.addEventListener('click', function (e) {
e.preventDefault();
var val = document.getElementById('text').value;
receiver.postMessage("Hello "+val+"!", "<http://res.42du.cn>");
});
// 接收消息端
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event){
if (event.origin !== "<http://www.42du.cn>")
return;
}