「跨域 」及CORS

$ 什么是CORS

  CORS:Cross-Origin-Resource-Sharing:跨站点资源共享,是一种由服务端主导的,通过配置Access-Control-Allow-Origin等内容来解决跨域的方式,可以理解为服务器端允许配置表站点网站来请求数据。
  当然解决跨域的办法还有别的办法,如:

  • JSONP:通过动态生成<Script>标签,且在其src中嵌入真正的请求地址(仅支持get请求),在请求参数中定义好与服务端约定的回调函数名称这种方式来实现跨域。见后续
  • Nginx反向代理:目前最主流的解决跨域办法之一。在Nginx代理服务器上,在server块的localtion / 中配置 proxy_pass,实现请求的转发来实现跨域。见后续

$ 是谁在引起跨域?

  与老一辈的网页不同的是,为了提高开发效率、缩短开发周期和提高开发质量,现在大多数项目都采用的前后端分离的形式,这给跨域问题创造了天然的条件。

  跨域是浏览器的安全检测行为,本与服务器端无关。从浏览器发起一个请求开始,到服务器接收和响应请求这个过程中,真正跨域作用的地方在于浏览器接收到服务器端响应的数据后,通过检查是否满足跨域的条件要求而决定是否对数据进行拦截丢弃的过程。是不是感觉浏览器在“多管闲事”呢?但其实这种设计是出于对服务器端数据安全而做的考虑。

$ 发生跨域的三个必要条件

  也许你不假思索的就能回答出 不同协议不同域名不同端口。没有问题,但并不准确,我更倾向于把这三个叫 跨域的三要素,那什么是跨域形成的必要条件呢?

  1. 浏览器限制: 即浏览器对跨域行为进行检测和阻止
  2. 触发跨域的三要素之一: 即 协议,域名和端口三个条件满足其一
  3. 发起的是xhr请求: 即发起的是XMLHttpRequest类型的请求。

其实xhr请求才是设计者们设计跨域的最关键的条件因素。并且只有同时满足三个条件才能触发跨域问题。

$ 为什么 JSONP 可以解决跨域问题

  都知道jsonp能解决跨域问题,其实不太准确,因为它是绕过浏览器的安全限制策略。那他为什么能绕过浏览器的安全限制呢?

  JSONP(json with padding)方案原理就是通过动态创建script标签,利用标签内src属性发送同步请求,并利用回调函数的方式实现异步数据的回调从而完成与后台交互的功能。当然除了jsonp方案使用script标签发送请求的办法外,还能通过img标签的src属性也同样能发送请求。

  可以通过浏览器控制台Network选项查看发现,JSONP发出去的请求类型是scriptimg标签src属性发出去的请求类型是JSON,他们都不是 xhr, 因为没有形成跨域的第三个条件,因此不会触发浏览器跨域检查策略。这就是为什么JSONP 方案能处理解决跨域问题的原因。

$ 发生跨域了怎么办?

  前端开发者们最关心的还是如何处理跨域问题。当形成跨域条件后,我们又该如何处理?

1. 对于浏览器限制的问题

  我们可以在启动浏览器的时候添加启动参数,告知浏览器不需要检查安全。实现办法是使用命令行启动如谷歌浏览器:

open -a "Google Chrome" --args --disable-web-security  --user-data-dir

  但这种办法只能实现在测试环节而且并不理想,这是客户端行为,针对于一个实际应用,要广大用户去做这件事情简直无稽之谈。

2. JSONP处理方式

  上述已经提到为什么JSONP为什么能实现跨域,具体又该如何操作呢?JSONP是一种非官方的协议,虽说非官网但也是一种协议,他需要前后端共同遵守一个约定。假设前端有如下代码用来发送一个请求,为了方便我们使用jQuery来编写如下测试用例,我们标记dataTypejsonp来标识这是一个jsonp请求,jQuery会帮助我们事先动态创建script标签并设置为异步请求和发送请求等功能,并将script标签插入到html的头部上

// javascript
$.ajax({
  url: base + "/get1",
  dataType: "jsonp",
  jsonp: 'callback',   // 默认jsonp协议的约定就是callback作为回调函数,一般不修改
  cache: false,   // 默认为false,结果不能被缓存,它会在请求上添加随机数参数
  success: function(json) {
    result = json
  }
})

  因为需要客户端和服务器端共同遵守约定,因此服务器端也需要添加相应的代码来约定接受处理JOSNP请求,否则服务器端依旧会返回JSON对象结果从而导致浏览器接受到响应后解析到的数据与响应头格式不一致而抛出解析错误。换句话说就是服务端不知道该请求是jsonp请求。如下案例

// java
@ControllerAdvice
public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {
  
  public JsonpAdvice() {
    super("callback") // 约定 如果请求的参数中含有callback参数,就认为是jsonp的请求 
  }
}

  这样,服务器就能正确的识别jsonp请求,并返回js代码类型格式而不是json数据格式,从而实现执行所谓的回调函数。

  虽然JSONP能处理跨域问题,但其实它存在很多弊端,主要如下:

  1. 需要服务器端修改代码支持: 服务器需要遵守约定导致服务器端也需要编写代码支持
  2. 只支持GET请求: 即使设置了请求类型为POST也无效,只支持GET
  3. 发送的不是XHR请求: 正因为这点才支持了跨域,然而也丢失了xhr强大的功能

  因为jsonp并不能满足开发者们的开发需求,也不方便使用,因此现实中并不常用到。说句白话,学习jsonp跨域有时候确实能帮我们解决跨域问题,更多的还是用在处理面试上~

3. 解决请求式跨域【重点】

  为了更好的理解这个知识点,我们先回顾一下一个普通项目的交互关系

  客户端有各种各样的请求发送给中间服务器,中间服务器在接收到请求之后如果判断如果是静态资源(img,js插件等)则直接返回(绿线),如果是交互资源则转发至应用服务器上(蓝线)。

  请求式跨域是最为常有也最为有效的跨域解决办法,因为前后端分离的开发模式,使得客户端和服务器端通常都在不同服务器上,这种模式解决跨域主要有两种思路

  1. 被调用方解决:调用方在浏览器直接将请求发送至被调用方,被调用方处理完成后,在请求响应中添加基于http协议关于跨域请求的一些规定,就是在http响应头中添加Access-Control-Allow-Origin等一些配置允许跨域访问。
  2. 调用方解决:这是基于隐藏跨域的解决办法。调用方通过一个代理服务器转发请求到被调用方的中间服务器,浏览器看到请求都是来自同一个域,就不会报跨域问题了

  这两种方案虽然具有相同的效果,但思路是完全不一样的。第一种是基于解决跨域的思路,修改的是被调用方的HTTP服务器,我们在浏览器中能看到有调用方的url,也有被调用方的url;而第二种是基于隐藏跨域的思路,修改的是调用方的HTTP服务器,在浏览器中也就只能看到调用方的url。

1.被调用方解决

被调用方解决支持跨域办法: 最终的目的是在响应头增加字段
(1)在应用服务器端实现[重点] (2)在Ngnix上配置 (3) 在Apache上配置(4)Spring框架解决
本文只讲第一种 ,后三种有兴趣的同学可以搜索一下如何配置,一般都由服务端小伙伴完成。

  浏览器在执行跨域请求时,如果遇到是简单请求,则先执行后判断;如果是非简单请求,则先使用OPTION发起一个预检请求【preflight request】,从而获知服务器是否允许该跨域访问,如果允许,就在此发起带真实数据的请求,否则不发起。这就实现了对被调用方的数据安全保护,也是跨域问题设计所在的目的之一

预检命令在浏览器中的表现
跨域检查成功发起了两次请求

【常见简单请求】主要有一下几种:1. GET / 2. HEAD / 3. POST且它的Content-Typetext/plainmultipart/form-dataapplication/x-www-form-urlencoded中的一种
【常见非简单请求】1. PUT / 2. DELETE / 3. OPTIONS /4. 发送json格式的ajax请求[常为post] / 5. 带自定义Header信息的ajax请求 / 6. CONNECT / 7. TRACE / 8. PATCH 等

  浏览器实现跨域判断的办法是: 当浏览器发现发起的是一个跨域的请求时,它会向请求头里增加一个Origin字段,当请求被响应时,浏览器会检查响应头里有没有设置允许跨域的信息,如果没有,它就会报错。
  同理,如果给请求增加头信息如contentType: application/json;charset=utf-8,那么contentType也是会被加入到请求头里作为跨域检查信息的。

因此,在应用服务器端的响应头需要添加允许跨域的设置,即如下:

// java
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  throws IOException, ServletException {

  HttpServletResponse res = (HttpServletResponse) response;

  // 允许跨域的域名,设置*表示允许除带Cookies信息的所有域名
  res.addHeader("Access-Control-Allow-Origin", "http://localhost:8081"); 
  // 允许跨域的方法,可设置*表示所有。GET/POST/OPTIONS等
  res.addHeader("Access-Control-Allow-Methods", "GET"); 
  // 假如给post请求头设置了contentType字段,则需要添加以下信息
  res.addHeader("Access-Control-Allow-Headers", "Content-Type");
  // 设置预检命令的缓存时效。单位是"秒"
  // 如果没有失效,则不会再次发起OPTION预检请求
  res.addHeader("Access-Control-Max-Age", "3600");
  // 还可以有其他配置...
  chain.doFilter(request, response);
}

这时,我们就可以在响应头Response Headers里观察到如下信息,这时跨域就被成功允许了。

以上方案已基本能实现在被调用方添加响应头字段来实现跨域的办法,但还有一种情况无法处理,那就是请求中带有Cookie的情况

  带有Cookie的请求还需要注意一下两点才能实现跨域

  1. Access-Control-Allow-Origin的值不能为'*'而是必须是全匹配,因此需要添上具体的域名
  2. 打开允许Cookie的设置,即Access-Control-Allow-Credentials: true

  但是这又带出了另一个问题,就是只能支持一个域名的跨域,怎么办?其实该变量可以通过调用方的请求头信息获取,解决办法如下:

// java
HttpServletRequest req = (HttpServletRequset) request;

String origin =  req.getHeader('Origin');

if (!org.springframework.util.StringUtils.isEmpty(origin)) {
  // 带cookie的时候origin必须是全匹配,不能使用 *
  res.addHeader("Access-Control-Allow-Orign", origin);
}

  对于需要增加请求头信息解决方案与此类似

2. 调用方解决跨域:反向代理

  当被调用方无法帮助解决处理跨域问题时,调用方也可以自己解决处理。其实现的办法就是利用反向代理

正向代理:利用代理客户端去请求服务器,从而隐藏了真实的客户端,服务器并不知道客户端是谁,这种代理方式称作正向代理,其代理的对象是客户端
反向代理: 反向代理隐藏了真正的服务端。举个例子,我们只知道敲下www.baidu.com时就能访问百度搜索页面,然而背后成千上万的服务器到底是哪一台正在为我们服务我们并不知道,这种隐藏了服务器端的代理方式称作反向代理,其代理的是服务器端。软件层面上常用ngnix来做反向代理服务器,他的性能很好,用来做负载均衡。

反向代理示意图

  为了实现反向代理,我们需要在ngnix中配置一个代理域名,或者称为一个网址a.com,就像百度成千上万的服务器使用用一个代理网址www.baidu.com一样。ngnix的配置信息如下

server {
  listen 80;
  server_name: a.com;
  // 真正服务器的地址
  location / {
    proxy_pass http://localhost:8081; 
  }
  // 代理ajax请求的url
  location /ajaxserver {
    proxy_pass http://localhost:8081/test/;
  }
}

$ 总结

  跨域是由浏览器安全限制造成
  解决跨域的办法有三种,一是jsonp绕过浏览器安全检测策略,二是从被调用方配置支持跨域的请求头信息,三是从调用方利用反向代理,在ngnix或apache中配置代理域名隐藏跨域。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容