最近在准备面试,记录点复习的内容。
什么是跨域
源
一个源由url的协议、域名或ip地址、端口号(如果有设置)组成。如果两个url这三者相同,则认为是同一个源。
例如:
对于url:zscshark.com/index.html
url | 是否同源 |
---|---|
zscshark.com/user/get-in... | 是 |
zscshark.com/user/get-in... | 否(协议不一样) |
zscshark.com:3000/user/get-in... | 否(端口不一样) |
shark.com/index.html | 否(域名不一样) |
同源策略
假设在没有限制的情况下,是不是浏览器可以无限制的访问各种想访问的源呢?当然不行啦,这样会有很多安全性的问题,例如电脑中了木马,木马通过一些方式向你的浏览器植入脚本,脚本抓取你的账号密码并提交到黑客的服务器,导致账号信息泄露等问题。为了解决这些问题,浏览器引入了一个叫"同源策略"的安全方案,这个策略规范了一个源的文档或脚本怎么与另一个源进行资源交互。在浏览器中基本都是要求交互的双方是同一个源才能交互。这样可以很好的防御跨站脚本攻击(XSS)和跨站点请求伪造(CSRF)。在非同源的情况下,浏览器会做出一些限制:
- 无法访问其他源的cookie、localstorage、sessionstorage
- 无法对其他源发起ajax请求
- 无法获取和修改非同源页面的DOM和js对象
- iframe嵌入限制,父页面无法直接操作内嵌的非同源页面
跨域问题产生的原因
同源策略是浏览器安全的基本盘,可以很好的解决了一些安全性的问题,但是同时也带来了其他问题:限制了不同源进行合法的资源交互。在这种限制下,从一个域名的网页去请求另一个域名的资源时,会被判断为不同源间的交互而被浏览器阻止,这种情况就被称为"跨域"。
实际应用情况中,"跨域"的情况是十分常见的,例如:
- 前后端分离的项目中,前端资源和后端资源可能会分开部署在两个域名下,双方一交互就跨域
- 静态资源存放在CDN中,CDN使用独立域名,前端获取CDN资源(非html标签获取)会出现跨域
这些问题确实头疼,所以需要一些手段来实现不违反同源策略情况下完成跨域的合法交互。
解决跨域问题
使用代理
代理的逻辑就是通过一个中间方来实现跨域的限制,一般会分为正向代理和反向代理。
正向代理
正向代理的方法就是在客户端上配置一个代理,所有的前端跨域请求都先发送到这个代理上,由该代理代表前端向目标服务器发起请求,这种的方式,前端是明确知道有代理服务存在。
我们在前端开发环境中使用的devServer的代理就是一种正向代理。
css
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
pathRewrite: {'^/api' : ''}
}
}
}
这种方式在本地开发环境使用还可以,在生产环境,像使用Electron之类的框架构建桌面应用可以使用外,在web端并不合适。
反向代理
反向代理其实就是代请求,在前端与服务器中间增加一个反向代理服务器,对于前端来说,这个代理服务器就是目标服务器。反向代理服务器接收到前端的请求后,会转发给内部的目标服务器,获取到结果后再返回给前端。看起来是不是跟正向代理没有什么区别,两者的区别主要是一个是客户端架设的,一个是服务端架设的。在正向代理中前端是知道有代理存在,但是在反向代理中,前端并不知道最终目标服务器的存在,前端会认为反向代理服务器就是目标服务器。
对于客户端来说,服务1和服务2是隐形的,这样可以更好的隐藏和保护实际的服务。同时在反向代理还可以通过负载均衡、加密、SSL加速等方式来优化性能提高可靠性。
nginx的反向代理就是反向代理的经典用法:
ini
server {
listen 80;
server_name example.com;
location /api {
proxy_pass http://target.com;
}
}
JSONP
由于html上
服务端
php
<?php
echo 'showResult({"name":"shark","age":20})';
?>
前端
xml
<script>
showResult(data) {
console.log(data);
}
</script>
<script src="jsonp.php"></script>
这种方法只能用于GET请求,主要用于兼容旧浏览器或在不支持CORS的场景下请求跨域资源。通过这种方式确实可以不使用ajax来完成数据交互,但是,大家一看就知道,存在以下的问题:
- 结果无法判断:因为是通过
- 结果处理问题:这个方法在返回的结果处理上很麻烦,后端返回的js代码不好控制,要么把结果挂在全局变量,要么使用特定的方法处理,前后端的协同十分痛苦
- 安全性问题:JSONP的安全性也没保证,请求的服务被攻击的话,攻击者可以伪造返回的内容进行攻击。
CORS
跨源资源共享(CORS Cross-Origin Resource Sharing), 是基于HTTP头的一种机制。通过在HTTP头携带特定的信息来判断是否可以跨源获取资源。实现的逻辑比较简单,浏览器在发起跨源的HTTP请求时,会携带相关的信息,服务器接收到后判断是否允许访问。因此,CORS的重点是服务器的实现。
CORS的基本原理
CORS是服务器通过HTTP头部中的一系列字段控制资源的访问权限,这些字段包括:
- Access-Control-Allow-Origin: 指定哪些域可以访问资源。如果是允许多个域访问,则需要设置这些域或者将字段值配置为
*
。 - Access-Control-Allow-Methods: 指定允许访问资源的H方法(如GET, POST)。
- Access-Control-Allow-Headers: 允许自定义的头部字段(如 x-token)。
- Access-Control-Allow-Credentials: 表示是否允许发送Cookie。
浏览器在发起跨源请求的时候会自动携带以下头部:
- Origin:发起请求的源
- Access-Control-Request-Headers:非常规的头部
- Access-Control-Request-Method:请求的HTTP方法(如GET、POST)
服务器接到请求后会根据这三个头部跟内部CORS的配置比对,如果所有头都在允许的范围,则继续执行并返回结果,返回的HTTP头中也会包括CORS的响应头。如果不允许,服务器会拒绝请求(一般返回403),浏览器会终止js的访问,阻止js读取响应的内容(js只能获取到请求失败的结果,但不知道原因),然后再控制台输出CORS请求失败的错误。
请求类型
浏览器将CORS请求分为简单请求 和复杂请求,根据请求类型的不同,浏览器在发起请求的时候需要携带的头也有所不用。
简单请求
符合一下情况的属于简单请求:
- 请求的HTTP方法是:HEAD、GET、POST
- 请求头字段不超过这些字段:
-
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type
这些字段浏览器会自动携带,一般不需要手动设置,浏览器还会携带一些相关的信息:
- Content-Type的值属于:
-
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
符合以上规则的属于简单请求。
浏览器在发起简单请求的时候会默认携带两个头:
- Host 请求的目标源
- Origin 发起请求的源(就是当前页面的源)
服务器在接收到请求的时候会检查Origin是否符合Access-Control-Allow-Origin配置,符合则继续执行,并且在返回头中添加CORS的配置信息:
复杂请求
不符合简单请求的要求的都是复杂请求。在发起复杂请求的时候,会默认添加三个头部:
- Origin:发起请求的源
- Access-Control-Request-Headers:非常规的头部
- Access-Control-Request-Method:请求的HTTP方法
在发送复杂请求时,浏览器会先发送一个预检请求,这是一个OPTIONS的请求,这个请求只携带请求的头部但不会携带相关的数据。发送这个请求的目的是探测服务器情况,是否允许这样的跨域请求。如果允许,服务器会返回成功(一般是204)并携带相关CORS信息的返回头,然后浏览器会根据返回的结果和头信息判断是否发起真正的请求。
options请求:
校验没问题,假如我们发起的是POST请求,正常发起:
如果是个PUT请求,则会被浏览器终止:
请求是直接报错的。
其他头部信息说明
Access-Control-Allow-Credentials
这个主要表示是否允许发送Cookie,默认为false,正常情况下,CORS请求是不会携带Cookie的,但是如果需要携带,则需要服务器允许,将该字段设置为true,表示服务器接受Cookie。同时在前端发起ajax请求的时候需要设置withCredentials属性为true(XMLHttpRequest、Axios都是这个配置)。
设置完携带Cookie后,不管是简单请求还是复杂请求(这属性不影响请求的类型),浏览器会根据服务器响应的头部中是否包含Access-Control-Allow-Credentials:true来判断是否拦截响应。
另外当允许携带Cookie或前端发起携带的请求时,Access-Control-Allow-Origin不能设置为*,需要明确源,不然会直接报错。
在设置withCredentials为true后,访问不允许携带Cookie的服务器:
ini
xhr.withCredentials = true;
访问没有设置Access-Control-Allow-Credentials,Access-Control-Allow-Origin:*的服务:
访问Access-Control-Allow-Origin不是*的服务:
Access-Control-Expose-Headers
服务器用于声明哪些响应头可以被js获取。默认情况下,js发起的跨域请求中,js只能获取到基础的响应头的内容,如果服务器返回一些自定义头部需要js获取的话,就可以用该属性配置。
服务端返回自定义头部X-id:
直接使用xhr.getResponseHeader('X-id')
获取,会提示错误,返回undefined
添加允许头后:
可以正常访问:
例外情况说明
Access-Control-Allow-Methods无效
在demo的过程中,我发现一个很神奇的现象,在服务器的Access-Control-Allow-Methods字段配置只允许GET请求的情况下,POST请求依然可以正常的访问,无论是简单请求还是复杂请求都不受影响。反之依然。
Access-Control-Allow-Methods:POST,发起GET请求:
预检请求
实际请求:
Access-Control-Allow-Methods:GET,发起POST请求:
预检请求:
实际请求:
简单请求
服务器换成其他允许的方式也可以成功
原因
一开始怀疑是缓存的问题,清除浏览器缓存后,依然可以。然后查了很多资料,没找到具体的说明,询问AI也没有明确的结果,而且其他浏览器(火狐、Edge)也有类似的情况。
Edge发起的请求:
因为没找到明确的解释,所以我只能靠猜了,接下来是我个人的猜想:
一、浏览器的限制问题:HEAD、POST、GET 作为最基础的三种请求方式,浏览器为了性能或其他方面的优化,对一些常见的跨域场景采用了一些比较宽松的限制,例如OPTIONS请求缓存之类的,而对于这三种基础类型的请求,应该也是采用了宽松的策略,即不校验请求方法。而对于这三种请求方法外的其他方法采用严格模式。
HEAD请求也是一样的表现:
二、服务端的问题:理论上不符合服务器配置的CORS规则的请求应该都会被拒绝,但是上面的请求都被接受了,我的服务端是nginx+php搭的简单服务,nginx没有配置与CORS相关的内容,php配置了CORS头,理论上没有nginx没启用CORS,应该是php框架提供的服务,翻了一下代码发现,框架的CORS类其实没有做什么特殊的处理,就是简单的检查下头然后返回,在我这个环境里面,服务器只管返回,剩下的都是浏览器来判断处理。
OK,原因猜想就到这,如果你知道这个问题的原因的话,请一定要告诉我。
前后端分离的跨域问题
最后,聊一下比较常见的前后端分离中跨域的问题,主要是看怎么部署和域名的使用。按照我的经验,一般有三种做法:
- 把前端代码和后端代码放在一起,然后前后端部署在一个容器里面。这样前后端就可以直接使用一个域名,访问前端时,通过不同的路径访问前后端资源,至于url问题可以通过url美化之类的手段来处理。这么做就是前后端同源,有些暴力点的还可以通过后端实现转发(类似BFF)来访问其他非同源服务。缺点是前后端发布必须同时发布,容易造成前后端版本混乱。构建也是个麻烦,一起构建时间长,容易一起挂,分开构建,还要合并,除非做一套自动化工具。另外开发环境和其他环境会脱节(不是一样的运行模式)。
- 前后端分别部署在两个容器,但使用同一个域名,通过配置对应的访问路径来实现前后端容器的访问,例如域名根路径访问就是index.html,/api路径就是访问后端的服务。不过这些就需要借助网关、代理等其他可以根据要求将流量导到指定的容器。这个也是前后端同源,可以分开部署,在物理层面也分离。不过这个对服务器的配套设施有要求。
- 前后端分开部署,使用不同的域名,通过CORS来完成跨域。这个应该是比较常见的方式吧。