【面试必会】烦不胜烦的跨域问题?再见

苦逼的码农

共 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中加入控制头是不足以解决跨域问题的。

headers导致变为预检请求

我们再来看看详细的预检请求

预检请求

与前述简单请求不同,需预检的请求要求必须首先使用 OPTIONS方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

不满足简单请求的就是预检请求,比如:PUTDELETE请求等等。

如下图所示,这是一个预检请求的例子,我们的POST请求中包含了自定义的headersX_PINGOTHER: pingpong,因此先发送了一个OPTIONS请求:

在得到的response中,我们可以看到:

Access-Control-Allow-Origin中有Client的来源originhttp://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中加入:

CORS允许的控制头

此时我们重新测试请求,发现解决了跨域问题。

总结

在这里,我们先总结一下如何解决跨域问题:

  • 对于简单请求,直接操作对应的responseheaders,一般Access-Control-Allow-OriginAccess-Control-Allow-Methods就能够解决;对于预检请求,一般还需要Access-Control-Allow-Headers。【如果需要传输cookie,那么会涉及:Access-Control-Allow-Credentials

  • 检查当前的请求是简单请求还是预检请求,如果不是简单请求,能否变为简单请求?

  • 如果当前是预检请求,那么在Chrome中使用F12->Network中检查Request Headers,检查是否有简单请求中不允许的头,记录下来。

    下图是我用另一个项目的前端来测试这个后端的跨域问题是否解决,在该前端项目中使用了一个第三方的Encoding-Type控制头:

注意自定义headers

我们在后端的CORS控制中加入,成功解决。

允许该自定义headers

在后端服务中,我们往往会使用一些现成的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语言编写的一个loginAPI,我们在response中加入Access-Control-Allow-Origin用以解决跨域问题,非常直观。

读者福利
《程序员内功修炼》第二版强势来袭,汇总了高质量的算法、计算机基础文章并且每一篇文章,要嘛是漫画讲解,要嘛是对话讲解,一步步引导,要嘛是图形并茂,如果你想学习算法,学习计算机基础,那么我决定这份 PDF,一定会让你有所帮助。当然,如果一是一位有那么点迷茫的在校生,相信我的个人经历,可以给你打一份鸡血,让你更好着去寻找自己的目标。

文章整体目录

如何获取

很简单,在我的微信公众号 帅地玩编程 回复 程序员内功修炼 即可获取《程序员内功修炼》第一版和第二版的 PDF。

推荐,推荐一个 GitHub,这个 GitHub 整理了几百本常用技术PDF,绝大部分核心的技术书籍都可以在这里找到,GitHub地址:https://github.com/iamshuaidi/CS-Book(电脑打开体验更好),地址阅读原文直达


浏览 31
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报