同源策略和跨域
什么是跨域问题?
为什么会出现跨域问题?
因为浏览器的同源策略
(同源指的是:协议+域名+端口
相同)。
同源策略
是浏览器的安全策略,限制某一个域的网站或者网站加载的脚本如何与另一个域的资源进行交互。
也就是浏览器
为了保护当前的网站用户的信息安全,限制了当前网站与其他域的服务的交互。所谓的限制并非阻止,而是通过一些约定好的方式,决定是否进行这次跨域交互。
为什么浏览器要有同源策略
没有同源策略限制的接口请求
先补充一个context:存储在浏览器上的cookie会在浏览器向同一服务器再次发起请求时被自动携带,并发往server。
如果没有同源策略的限制,在下面这种csrf攻击的情况下,你将会受到很大的影响
- 你使用用户名和密码登录了
www.yinhang.com
网站,并且获取到了cookic并存放在了本地。 - 之后给这个银行server发送的
www.yinhang.com/getMoney
等请求都会自动的携带这个cookie作为用户凭证给server发送请求 - 突然你某一个地方诱导你点击了一个坏网站
www.yinghangbad.com
,这时候你点进去,这个网站偷偷的给银行的server发送了请求www.yinhang.com/getMoney
获取你的账户信息,显而易见,这个请求和网站的域名并不相同,如果没有同源策略的限制,这个请求会被轻松的携带你的cookie从银行server成功的获取money信息
没有同源策略限制的Dom查询
某一个坏的网站利用<iframe>将银行网站嵌入到自己的网站中,想要通过自己的写的js文件,拿到嵌入的银行网站的HTML username和password所在的input框的value。
如果没有浏览器的同源策略:只能access同源网站上的DOM元素
, 那么坏网站上的JS就可以access银行网站上的DOM元素,这将会造成用户名和密码泄露。
由此可见浏览器的同源策略确实能在某种程度上保护了你的网站。
同源策略导致了:
-
非同源的请求无法通信,真实流程是:
- 浏览器发现当前的网站正在给一个非同源的服务发送请求,浏览器在请求的header中加上一个
Origin
标识来自的源,帮助service判断是否需要相应来自这个域的请求 - 当请求的response回到浏览器,浏览器会首先检查header中是否带有
Access-Control-Allow-Origin
, 并且检查该字段的value中是否包含当前网站所在的域(也就是由server决定是否允许这个域的请求),只有包含,浏览器才会将请求的response返回给网站。
- 浏览器发现当前的网站正在给一个非同源的服务发送请求,浏览器在请求的header中加上一个
-
非同源的DOM或者JS无法访问
-
非同源的cookie、LocalStorage、IndexDB无法读写
但是DOM的同源策略可以理解,对于网站上的所有请求总不能因为不同源而被全部阻断,那么如何允许跨域的请求呢?
会受到同源策略影响的资源
以下是会加载外部资源的HTML标签:
- script标签对JS文件的请求
- link标签对css文件的请求
- Img对image的请求
- 通过 @font-face 引入的字体。一些浏览器允许跨域字体( cross-origin fonts),一些需要同源字体(same-origin fonts)。
- 通过 <video> 和 <audio> 播放的多媒体资源。
- 通过 <object>、 <embed> 和 <applet> 嵌入的插件。
- 通过 <iframe> 载入的任何资源。站点可以使用 X-Frame-Options 消息头来阻止这种形式的跨域交互。
会受到同源策略影响的资源
- font
- 所有ajax请求
在浏览器中, <script> 、<img>、<iframe>、<link>等标签都可以跨域加载,而不受浏览器的同源策略的限制
如何解决因同源策略带来的跨域请求失败
一般我们都是采用CORS(跨域资源共享)解决此类问题的
跨域资源共享是一种基于HTTP header
的机制。这个机制的原理:
-
某一个ajax请求从一个网站发出,请求的server是一个和当前网站域名不一致的server,也就是说这是一个跨域请求。
-
请求刚开始被fire的时候,经过浏览器,浏览器检测到这是一个跨域请求,就会自动在这个request的header中加上:
Origin
字段,value就是当前网站protocol://host:port
比如https://developer.mozilla.org
这个字段是不能够被修改的 -
请求到达Server之后, 服务器可以检查这个origin,并决定是否给这个origin提供服务,提供怎样的服务。
-
如果服务器最终决定给这个请求提供服务,那么服务器就需要让浏览器知道,因此需要在response header中带上
Access-Control-Allow-Origin
value中就是服务器能够服务的域名
Access-Control-Allow-Origin: <origin> | *
注意这里的origin是能是一个域名不能是多个
- 浏览器接受到来自response,会首先check header中是否包含
Access-Control-Allow-Origin
,并查看当前的域是否在value中。如果是,浏览器会把response成功返回给网站,如果不是,那么就会报错。
ps: 当然还包含很多其他的header,一般Access-Controll-XXX-XXX
都是和跨域相关的header
当然以上只是CORS的一种场景,正常情况根据请求的不同,跨域机制也不同:
简单请求
通常请求类型是:
- GET
- HEAD
- POST
并且header中没有特殊的内容,只有常见的accept、content-type等
且content-type只能是application/x-www-form-urlencoded
text/plain
符合上述条件的请求一般就是简单请求,简单请求的跨域机制就符合上面的步骤,只要有origin
以及access-controllXX
注意如果请求的header中content-type是application/json
这类请求一定不是简单请求。
非简单请求(复杂请求)
较为复杂的不在上面简单请求类型的请求就是复杂请求
,浏览器在发出这类请求之前会发出一个预检请求
预检请求
是当你要发出一个跨域请求时,浏览器会自动率先发出一个OPTION类型的请求,带上Origin
, 询问服务器是否能够允许接下来的实际复杂请求,预检请求会在请求头部带上实际请求的相关信息:
Access-Control-Request-Method: POST
// 实际请求的method
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
// 实际请求中即将有的header
服务器会根据预检请求带来的实际请求的信息,决定是否允许接下来的实际请求,如果允许,会在response的header中携带
Access-Control-Allow-Methods: GET, POST, PUT
// 必须字段,标明当前服务器可以允许的来自此域的所有请求method
Access-Control-Allow-Headers: X-Custom-Header
// 标明当前服务器可以允许的来自此域的所有请求能携带的header字段
Access-Control-Allow-Credentials: true
// 后续会提到
Access-Control-Max-Age: 1728000
// 可选字段,标明本次预检请求的有效时间,在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。
浏览器会根据预检请求的response决定是否发送实际请求给服务器。
之后的实际请求会按照上面简单请求的流程,request会被浏览器携带origin
, response必须有Access-Controll-Allow-XXX
带credentials的请求
通常HTTP请求可以通过cookie
或者 Authorization header
给服务器发送身份凭证。
但是对于跨域的请求,在没有特殊设置的前提下,浏览器不会发送身份凭证信息给服务器,也就是说任何cookie或者authorization都不会自动的携带到某一个请求上。这是浏览器的同源策略之一。
那么如何将身份信息携带在请求中呢?
需要双重设置:
-
需要在XMLHttpRequest或者fetch上设置
request.withCredentials = true
, 浏览器才会在请求上携带身份信息 -
服务器接收到身份信息处理完毕之后,必须在response中携带
Access-Control-Allow-Credentials: true
否则浏览器不会把响应内容返回给请求的发送者。 -
对于这种携带身份信息的请求,它response的
Access-Control-Allow-Origin
的value不能是*
,只能是origin