【面试必会】烦不胜烦的跨域问题?再见
共 4860字,需浏览 10分钟
·
2020-10-18 21:02
最近接了个外包项目,我负责后端,想着省事直接用 Django,吭哧吭哧写了一天 API 后,拉了前端代码下来联调,果不其然遇到了跨域问题。然而这个跨域有点诡异,折腾了一天时间,顶不住睡一觉,第二天一起床反而解决了。这个故事告诉我们,做到十二点,就不做了,睡大觉!
谈到跨域,就不可避免的要讲讲CORS
,这些到底都是啥?它们有什么区别?请听我慢慢道来。
跨域资源共享
CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的 HTTP头组成,这些HTTP头决定浏览器是否阻止前端 JavaScript
代码获取跨域请求的响应。
简而言之,在前后端分离的开发模式下,我们的前端在向后台请求资源时,由于浏览器的同源策略,默认情况下我们是无法获得资源的,这个时候,就会产生我们常说的跨域问题
:
如上图所示,在浏览器的Console
控制台中,当我们发送的请求遇到跨域问题
时,就会出现上图的报错信息。
浏览器的同源策略
在了解跨域问题
的解决办法之前,我们先来看看浏览器的同源策略是怎么一回事:
如下图所示,Web document
我们可以理解为前端,所在域为domain-a.com
;当它请求同域名下的Web server
(即左上方的服务器)时,它们的请求是同源请求(总被允许的)
,而当它请求domain-b.com
下的Web server
时,它们的请求是跨域请求(受 CORS 控制)
。
也就是说,我们的跨域问题本质上不是一个问题,而是在跨域请求
中,我们没有用CORS
控制我们的服务器允许该请求,那么解决办法也就很简单了,我们需要修改后台的一些配置,使其允许来自前端的跨域请求
。
我们前面也提到,CORS 由一系列的传输 HTTP 头组成
,实际上,跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。
综合来看,实际上我们要修改的配置,就是返回给前端的response
上的头(headers)
。
回到故事的开始
稍微查阅资料,我们在response
的头中加入:Access-Control-Allow-Origin: *
;但事情并没有这么简单,在我的 Django
后台中,我尝试了自定义middleware
、使用第三方的django-cors-headers
、直接修改response
返回值,发现居然都不起作用。翻遍全网,来来去去都是这几个办法,迫于无奈,我打断点,Debug,甚至还瞎改了一通django-cors-headers
的源码,验证一些想法,最后发现的原因:前端的锅
。
注意下图中注释掉的部分,当我将其注释掉后,后端使用django-cors-headers
的情况下,跨域问题就解决了,那么为什么加上注释中的部分就会导致跨域问题呢?
这里我们不得不再深究一下跨域的相关知识,否则每次遇到,都只能找各种或断断续续、或版本不对的博客去看,问题没解决,时间倒是浪费了。
深究跨域相关知识
CORS
规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是GET
以外的 HTTP 请求,或者搭配某些 MIME 类型的POST
请求),浏览器必须首先使用OPTIONS
方法发起一个预检请求(preflight request
),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。
从上述内容中我们可以了解到,CORS
仅针对部分请求会先发起一个预检请求
(OPTIONS
),而对于其他的简单请求
则不需要这个过程。
我们先来看看哪些情况是符合简单请求
的:
简单请求
在本文指代:某些不会触发 CORS 预检请求
的请求。
使用列出的三种方法之一:GET、POST、HEAD
Fetch 规范定义了
对 CORS 安全的首部字段集合
,不得人为设置该集合之外的其他首部字段。Accept
Accept-Language
Content-Language
Content-Type
(注意,第三点为对该字段的限制)DPR
Downlink
Save-Data
Viewport-Width
Width
Content-Type
的值仅限三种之一text/plain
multipart/form-data
application/x-www-form-urlencoded
请求中的任意
XMLHttpRequestUpload
对象均没有注册任何事件监听器;XMLHttpRequestUpload
对象可以使用XMLHttpRequest.upload
属性访问。请求中没有使用
ReadableStream
对象。
需要满足上述五个条件,才符合简单请求的要求。我们如果能将预检请求降为简单请求,那么就能解决跨域问题。
如何解决呢?对于简单请求,我们只需要在返回的response
中加入Access-Control-Allow-Origin
控制头,把前端所在域加入或者直接使用*
,即可解决。
而回到故事中,前端发起的login
请求属于POST
请求,Content-Type
也是application/x-www-form-urlencoded
符合要求,问题就出在这个headers
上,这个自定义headers
使得第二点不满足,所以浏览器会先发起一个预检请求,不同于简单请求,包含预检请求的处理方式较为麻烦,直接在response
中加入控制头是不足以解决跨域问题的。
我们再来看看详细的预检请求
:
预检请求
与前述简单请求不同,需预检的请求
要求必须首先使用 OPTIONS
方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求
的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
不满足简单请求的就是预检请求,比如:PUT
、DELETE
请求等等。
如下图所示,这是一个预检请求
的例子,我们的POST
请求中包含了自定义的headers
:X_PINGOTHER: pingpong
,因此先发送了一个OPTIONS
请求:
在得到的response
中,我们可以看到:
Access-Control-Allow-Origin
中有Client
的来源origin
:http://foo.example
;
Access-Control-Allow-Methods
中有POST
;
Access-Control-Allow-Headers
中也有自定义的X-PINGOTHER
;
因此Client
得到允许,继续向Server
发送POST
请求,最终获得数据。
另外,首部字段 Access-Control-Max-Age
表明该响应的有效时间为 86400 秒,也就是 24 小时。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。
回到故事,在此时我进行了一个小实验,我把前端中的headers
的注释取消掉,
在后端的CORS
控制中,在Access-Control-Allow-Headers
中加入:
此时我们重新测试请求,发现解决了跨域问题。
总结
在这里,我们先总结一下如何解决跨域问题:
对于简单请求,直接操作对应的
response
的headers
,一般Access-Control-Allow-Origin
和Access-Control-Allow-Methods
就能够解决;对于预检请求,一般还需要Access-Control-Allow-Headers
。【如果需要传输cookie,那么会涉及:Access-Control-Allow-Credentials
】检查当前的请求是简单请求还是预检请求,如果不是简单请求,能否变为简单请求?
如果当前是预检请求,那么在
Chrome
中使用F12
->Network
中检查Request Headers
,检查是否有简单请求中不允许的头,记录下来。下图是我用另一个项目的前端来测试这个后端的跨域问题是否解决,在该前端项目中使用了一个第三方的
Encoding-Type
控制头:
我们在后端的CORS
控制中加入,成功解决。
在后端服务中,我们往往会使用一些现成的CORS
处理插件,比如Django
中的django-cors-headers
,那么只需按照其github
页面的配置方式,同时注意上述的自定义头的控制,那么处理跨域问题非常简单;如果不是使用现成的CORS
处理插件,我们就需要对OPTIONS
请求和各种请求的Response
进行处理,最好是统一处理,比如在Springboot
中可以在AOP
层统一处理;
在更轻量的情况下,我们也可以直接对单一请求的response
进行处理:
func login(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*") //允许访问所有域
w.Header().Add("Access-Control-Allow-Headers", "Content-Type") //header的类型
w.Header().Set("content-type", "application/json") //返回数据格式是json
resp := `{"code": "00",
"message": "SUCCESS",
"describe": "登录成功"
}`
type JsonResp struct {
Code int `json:"code"`
Message string `json:"message"`
Describe string `json:"describe"`
}
var smsresp JsonResp
temp := []byte(resp)
json.Unmarshal(temp, &smsresp)
fmt.Fprintf(w, string(temp))
}
上面是我之前用go
语言编写的一个login
API,我们在response
中加入Access-Control-Allow-Origin
用以解决跨域问题,非常直观。
文章整体目录
如何获取
很简单,在我的微信公众号 帅地玩编程 回复 程序员内功修炼 即可获取《程序员内功修炼》第一版和第二版的 PDF。
推荐,推荐一个 GitHub,这个 GitHub 整理了几百本常用技术PDF,绝大部分核心的技术书籍都可以在这里找到,GitHub地址:https://github.com/iamshuaidi/CS-Book(电脑打开体验更好),地址阅读原文直达