Cors跨域(四):解决方案对决JSONP vs CORS

开发 前端
当被浏览器半信半疑的脚本运行在沙箱时,它们应该只被允许访问来自同一站点的资源,而不是那些来自其它站点可能怀有恶意的资源。但是呢,在现在的互联网场景中,跨域访问是一种必须,所以才有了解决跨域问题的方案。

[[407226]]

前言

你好,我是YourBatman。

挖掘机技术哪家强,山东技校找蓝翔;跨域问题怎么解,CORS还是JSONP?

关于浏览器跨域问题的解决方案,坊间一直“传闻”着两种解决方案:JSONP和CORS。由于文章的历史背景不同,作者偏好不一样,搞得好些同学迷惑得很,去谷歌里百度搜寻答案时经常就是这种赶脚。

作为一家负责任的“技校”(负责人的技术专栏),今天通过此文彻底给你解释清楚并给出确定的答案,助你快速选择正确的道路解决问题。

所属专栏

  • 点拨-Cors跨域

本文提纲

版本约定

  • JDK:8
  • Servlet:4.x
  • tomcat:9.x

正文

同源策略是浏览器最核心也最基本的安全功能。当被浏览器半信半疑的脚本运行在沙箱时,它们应该只被允许访问来自同一站点的资源,而不是那些来自其它站点可能怀有恶意的资源。但是呢,在现在的互联网场景中,跨域访问是一种必须,所以才有了解决跨域问题的方案。

两大方案:JSONP和CORS

对于跨域共享资源,一共有两大解决方案

  • JSONP:老一代浏览器解决方案
  • CORS:全新一套标准的解决方案

JSONP方案

和iPhone 7和iPhone 7P不一样,JSONP 不等于 JSON Plus,全称是JSON with Padding。JSON是一种基于文本的数据交换格式,而JSONP是一种使用模式,可以让网页从别的域访问资源,从而完成跨域资源共享。

本系列第一篇文章就说到:<script>标签的src是没有跨域这么一说的,可以基于这一点实现Get请求的跨域。

JSONP的实现跨域的基本原理是:利用script标签的src没有跨域限制 + 回调的方式来完成跨域访问。

代码实现示例

前端页面:托管在63342端口

  1. <!DOCTYPE html> 
  2. <html lang="en"
  3. <head> 
  4.     <meta charset="UTF-8"
  5.     <title>JSONP跨域请求</title> 
  6.     <!--导入Jquery--> 
  7.     <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script> 
  8. </head> 
  9. <body> 
  10. <script> 
  11.     // 请求完成后会回调此函数 
  12.     function jsonpCallback(result) { 
  13.         console.log("这是JSONP请求的响应结果:" + result); 
  14.     } 
  15. </script> 
  16. <!--注:这个script必须放在上面function的下面--> 
  17. <script type="text/javascript" src="http://localhost:8080/jsonp?callback=jsonpCallback"></script> 
  18. </body> 
  19. </html> 

说明:利用script的src发送http请求到服务端,因此此script标签务必放在function的下面(因为浏览器是从上至下渲染)

服务端代码:托管在8080端口

  1. /** 
  2.  * 在此处添加备注信息 
  3.  * 
  4.  * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a> 
  5.  * @site https://yourbatman.cn 
  6.  * @date 2021/6/9 10:36 
  7.  * @since 0.0.1 
  8.  */ 
  9. @Slf4j 
  10. @WebServlet(urlPatterns = "/jsonp"
  11. public class JSONPServlet extends HttpServlet { 
  12.  
  13.     @Override 
  14.     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 
  15.         String callback = req.getParameter("callback"); 
  16.         resp.getWriter().write(callback + "('hello jsonp...')"); 
  17.     } 

说明:可以看到服务端的代码非常的清爽,不涉及到任何请求头/响应头

打开页面,发送JSONP请求,结果如下:

请求的响应体:

浏览器控制台输出:

完美。通过JSONP我们实现了访问不同域的资源,实现了跨域。

用jQuery的ajax发送异步JSONP请求

上例是使用<script>标签的src属性发送同步跨域请求,在实际开发中(特别是前后端分离)大多数情况下发送的均为Ajax异步请求,下面来试试。

说明:异步请求用原生XMLHttpRequest还是Ajax或者Promis方式发出,底层原理都归一是相同的

使用jQuery发送异步JSONP请求非常的简单,连<script>和函数都不用写:

  1. <!DOCTYPE html> 
  2. <html lang="en"
  3. <head> 
  4.     <meta charset="UTF-8"
  5.     <title>JSONP跨域请求</title> 
  6.     <!--导入Jquery--> 
  7.     <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script> 
  8. </head> 
  9. <body> 
  10. <!--<script>--> 
  11. <!--    // 请求完成后会回调此函数--> 
  12. <!--    function jsonpCallback(result) {--> 
  13. <!--        console.log("这是JSONP请求的响应结果:" + result);--> 
  14. <!--    }--> 
  15. <!--</script>--> 
  16. <!--&lt;!&ndash;注:这个script必须放在上面function的下面&ndash;&gt;--> 
  17. <!--<script type="text/javascript" src="http://localhost:8080/jsonp?callback=jsonpCallback"></script>--> 
  18. <script> 
  19.     $.ajax({ 
  20.         // type: "get", // 不用写方法,因为JSONP只支持GET请求 
  21.         url: "http://localhost:8080/jsonp", // 使用jQuery的Ajax后面是没有参数 
  22.         dataType: 'jsonp'
  23.         success: function (data) { 
  24.             console.log("这是JSONP请求的响应结果(jQuery Ajax):" + data); 
  25.         }, 
  26.     }); 
  27. </script> 
  28. </body> 
  29. </html> 

通过jQuery大大改善了js代码的书写方式,使得结构更加优雅、直观。这就是jQuery最厉害的语法糖能力~

说明:JsonP only works with type: GET。也就说type即使你写成post,jQuery也会给你转成get

服务端不变,发送异步请求,结果如下: 

关注点:

  1. Ajax的callback回调函数名是动态生成的,并且确保了唯一性
  2. 由于服务端并不关心回调的函数名名称,因此回调函数名的长短没有关系(浏览器自己能识别就成)

影响体如下:

浏览器控制台打印:

完美。对于有技术敏感性的你来讲,应该能发现底层原理依旧还是script的src,只是写法不一样,仅此而已。

优缺点

JSONP跨域方案作为一种“古老”方式,有如下优缺点:优点:

  • 对老浏览器(如IE8、7等)有非常好的兼容性
  • 书写起来比较简单,容易理解(毕竟没有那么多的请求头、响应头需要考虑嘛)

缺点:

  • 只能发送get请求,不支持POST、PUT等请求方式,这是硬伤
  • 安全度不高。因为JSONP是利用函数回调来由浏览器执行目标函数,这样宿主web其实是比较容易受到各类攻击的

总的来讲,随着Cors规范在2014年的正式确定,现代的浏览器100%支持Cors规范。由于浏览器的更新换代,JSONP的最大优势(兼容老浏览器)也就不复存在了,所以在实际开发中的使用建议是:不要使用JSONP,而应拥抱CORS。

CORS方案

由于JSONP方案存在一些不足(比如只支持Get请求就是硬伤),并不能很好的满足对跨域资源共享的需求,因此就出现了当下主流的跨域规范:CORS(Cross-origin resource sharing)

不同于JSONP的方案,CORS方案更强大实用,但稍微复杂那么一丢丢。其背后的基本思想是:使用自定义的HTTP头部和浏览器“沟通”,让浏览器和服务器相互“了解”对方,从而决定请求或响应成功与否。

说明:CORS 并不是为了解决服务端安全问题而出现,而是为了解决如何跨域调用资源。至于如何设计出安全的、开放的API,这就是安全范畴了(如可加上token验证、请求有效期、ip来源验证等手段)

CORS的WD(工作草案)从2009-03-17开始,2014-01-16进入REC(推荐标准)阶段,可谓正式毕业。起初CORS的推广的主要障碍是当时市面上的老浏览器并不支持它(比如当时市场占有率极大的IE 6、7、8这种老家伙),毕竟这个规范是新的只有升级的新浏览器才会支持到。

但历史的巨轮永远是滚滚向前,现在已经2021年了,现今市面上的浏览器对CORS规范的支持情况如下图所示(数据来源于:http://caniuse.com):

看到这张图,应该可以毫不客气的说:所有的浏览器(包括手机、PAD等浏览器)均已支持CORS规范

版本上拿Chrome浏览器举例:我现在使用的版本是91.0.xxxx.xxx,完美支持:

当下阶段,已完全无需考虑浏览器兼容问题,所以JSONP的优势也就不复存在,可以放心的、积极的拥抱CORS。既然要用CORS,作为程序员不能只停留在概念上层面,接下来就来聊点干的,看看从实操层面有哪些具体做法落地CORS呢?

CORS的核心要义是和服务端和浏览器进行沟通,服务端架构一般是分层的,理论上可以在任意层次完成沟通。从负责完成沟通的层次上来讲,一般分为这两大类:

  1. 代理服务器/网关负责
  2. Web应用自行负责

代理服务器/网关方

式众所周知,一般的架构不会是浏览器->后端服务点对点, 而是会设计(很多)中间层,比如代理服务器、网关等。就像这样:

既然如此,我们就多了一些手段来处理Cors。

从“距离”上看,我们可以在离浏览器最近的地方(流量入口处如Nginx,Gateway等)把Cors跨域问题搞定,这样后端Web Server就无需再操心了,可谓十分方便。

下面以Nginx为例,看看如何落地?

  1.  
  2. # Wide-open CORS config for nginx ### 没有保护的(潜台词:有安全风险的)NG Cors配置 
  3. location / { 
  4.   
  5.   ### 在Ng层就把Options请求全部拦截掉,不会下层到后面的web应用 
  6.      if ($request_method = 'OPTIONS') { 
  7.       
  8.         ### 使用*通配符表示允许所有的Origin源 
  9.         add_header 'Access-Control-Allow-Origin' '*'
  10.         # 
  11.         # Om nom nom cookies 
  12.         # 
  13.         add_header 'Access-Control-Allow-Credentials' 'true'
  14.         add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; ### 若有需要,可增加PUT、DELETE等请求 
  15.  
  16.         # 
  17.         # Custom headers and headers various browsers *should* be OK with but aren't 
  18.         # 
  19.         ### 允许自定义的请求头(根据需要,自行删减哈) 
  20.         add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'
  21.  
  22.         # 
  23.         # Tell client that this pre-flight info is valid for 20 days 
  24.         # 
  25.         ### 允许预检请求缓存20天之久(根据需要自行调整) 
  26.         add_header 'Access-Control-Max-Age' 1728000; 
  27.         add_header 'Content-Type' 'text/plain charset=UTF-8'
  28.         add_header 'Content-Length' 0; 
  29.         return 204; 
  30.      } 
  31.   
  32.   ### 因为上面OPTIONS只允许了GET/POST所以这里就只列出两,根据需要自行增减哦 ### 
  33.      if ($request_method = 'POST') { 
  34.         add_header 'Access-Control-Allow-Origin' '*'
  35.         add_header 'Access-Control-Allow-Credentials' 'true'
  36.         add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'
  37.         add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'
  38.      } 
  39.      if ($request_method = 'GET') { 
  40.         add_header 'Access-Control-Allow-Origin' '*'
  41.         add_header 'Access-Control-Allow-Credentials' 'true'
  42.         add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'
  43.         add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'
  44.      } 

这是一段比较“著名”的、通用的Nginx解决Cors问题的配置。这段配置基本能够解决绝大多数的跨域请求case,但也正是因为它的通用性,带有如下不足:

  1. Access-Control-Allow-Origin为通配符*,表示所有的Origin都能访问本站资源,安全性低
  2. Access-Control-Allow-Origin响应头只允许有一个(有多个就会报错),而把它写进了NG,导致后端Web应用无法对它进行精细化控制了
  3. Access-Control-Allow-Credentials的值恒定设置为true。在本系列第二篇文章提到:当需要跨域请求携带cookie等验证信息时,Access-Control-Allow-Origin头的值是不允许为*的,而NG这一层对此又限制了

总而言之言而总之,在离浏览器最近的地方处理Cors有优有劣。优点是通用性很好、“体验”也最好(web server无需感知),但也应当知晓它的劣势,如安全性低、个性化性差(因为无法感知到业务需求嘛)。万物具有两面性,请勿一刀切,要因地制宜呀。

一般来讲纯前端静态资源的跨域资源共享可用Ng形式统一处理,但对于服务端(后端)Web应用的API接口资源管理,由于场景较为复杂,对安全性要求颇高,因此还是交给给应用自行管理更为合适

Gateway网关方式

网关也可认为是一种代理服务器,属于中间层中的一层。不过相较于Nginx来讲,它的可编程性更强一些,因此很多时候将Cors逻辑放到网关层具有更大的灵活性(特别是内网网关),起到一个折中的效果。

Web应用方式

Web应用是离浏览器“最远”的地方,在这里解决Cors对应用侵入性最大。但是呢,由于能感知到业务(如知道有哪些接口、哪些功能)的存在,所以就能做到精细化控制,安全性最高,个性化最强,因此具体落地处理方式也有多种。

1. 硬编码方式

顾名思义,就是在实际处理请求的代码前/中/后通过硬编码的方式解决。本系列前面文章给出的代码示例,为了便于理解均是这种硬编码方式。

  1. /** 
  2.  * 在此处添加备注信息 
  3.  * 
  4.  * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a> 
  5.  * @site https://yourbatman.cn 
  6.  * @date 2021/6/9 10:36 
  7.  * @since 0.0.1 
  8.  */ 
  9. @Slf4j 
  10. @WebServlet(urlPatterns = "/cors"
  11. public class CorsServlet extends HttpServlet { 
  12.  
  13.     @Override 
  14.     protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 
  15.         super.doOptions(req, resp); 
  16.         setCrosHeader(resp); 
  17.     } 
  18.  
  19.     @Override 
  20.     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 
  21.         String requestURI = req.getRequestURI(); 
  22.         String method = req.getMethod(); 
  23.         String originHeader = req.getHeader("Origin"); 
  24.         log.info("收到请求:{},方法:{}, Origin头:{}", requestURI, method, originHeader); 
  25.  
  26.         resp.getWriter().write("hello cors..."); 
  27.         setCrosHeader(resp); 
  28.     } 
  29.  
  30.     private void setCrosHeader(HttpServletResponse resp) { 
  31.         resp.setHeader("Access-Control-Allow-Origin""http://localhost:63342"); 
  32.         resp.setHeader("Access-Control-Expose-Headers""token,secret"); 
  33.         resp.setHeader("Access-Control-Allow-Headers""token,secret"); // 一般来讲,让此头的值是上面那个的【子集】(或相同) 
  34.     } 

优点:个性化极强,可以针对接口级别给出不同的CORS逻辑,精细化控制缺点:侵入性从应用级别上升到了业务代码级别,显得十分臃肿,粒度太细后期维护成本高

2. 自定义Filter/Interceptor

既然是Filter那便属于“批处理”方案:对整个应用做Cors的统一逻辑处理

  1. /** 
  2.  * 在此处添加备注信息 
  3.  * 
  4.  * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a> 
  5.  * @site https://yourbatman.cn 
  6.  * @date 2021/6/14 09:50 
  7.  * @since 0.0.1 
  8.  */ 
  9. public class CORSFilter implements Filter { 
  10.  
  11.     @Override 
  12.     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 
  13.         HttpServletResponse resp = (HttpServletResponse) response; 
  14.         resp.addHeader("Access-Control-Allow-Credentials""true"); 
  15.         resp.addHeader("Access-Control-Allow-Origin""*"); 
  16.         resp.addHeader("Access-Control-Allow-Methods""GET, POST, DELETE, PUT"); 
  17.         resp.addHeader("Access-Control-Allow-Headers""Content-Type,X-CAF-Authorization-Token,sessionToken,X-TOKEN"); 
  18.         if (((HttpServletRequest) request).getMethod().equals("OPTIONS")) { 
  19.             resp.getWriter().println("ok"); 
  20.             return
  21.         } 
  22.         chain.doFilter(request, resp); 
  23.     } 
  24.      

优点:应用级别的统一处理,对业务代码无侵入性。应用内集中化处理Cors逻辑,维护方便缺点:无法做到接口级别的粒度,对于某些特殊要求的细粒度控制自然就无能为力

说到底,上例中的自定义Filter的方式仍属于硬编码方式(将影响Cors的相关头信息写死的),不够灵活。其实可以再优化一下,让其更富弹性。为此,早在N年之前就有eBay开源的过滤器方案:cors-filter.java 供以参考。

  1. <dependency> 
  2.  <groupId>org.ebaysf.web</groupId> 
  3.  <artifactId>cors-filter</artifactId> 
  4.  <version>1.0.1</version> 
  5. </dependency> 

它可以让允许的origins、methods、headers等都支持可配置化,更富弹性。

3. Spring Framework方式

调研一下:现在做Java(Web)开发,应该没有不使用Spring Framework的吧?

Spring自4.2版本(2015-06)开始,就提供了对Cors的全面支持,大大简化应用级Cors问题的处理。其中面向开发者提供了两个用于优雅处理Cors问题的组件:

  • @CrossOrigin:借助此注解可以通过声明式方式,对类级别、甚至接口级别进行跨域的资源控制
  • CorsFilter:Spring也提供了用于“全局处理”的过滤器,兼具了普适性和灵活性

WebMvcConfigurer:这是一种配置方式,严格来讲不算一种解决方案而是一种落地方式而已

由于Java开发者一直和Spring打交道,因此深入理解此场景下的解决方案,打通其执行原理方可使用起来得心应手,所以这也是本系列关心的重中之重。关于此part本系列下文会单独成篇解读,包括使用姿势到设计思想、源码分析.....

4. Spring Boot方式

如你所知,Spring Boot是构建在Spring Framework之上的。在Cors这块Spring Boot并未对其做增强or扩展,因此使用姿势上同Spring Framework。

这是不是再一次验证了那句话:在Spring Boot上能走多远由你对Spring Framework的了解深度而决定

Cors安全漏洞

浏览器的同源策略(SOP)是一个安全基石。SOP是一个很好的策略,但是随着Web应用的发展,网站由于自身业务的需求,需要实现一些跨域的功能,能够让不同域的页面之间能够相互访问各自页面的内容,这就导致SOP策略不是那么的凑效了。

Cors作为当下解决浏览器跨域问题的标准方案,如若使用不当是会带来安全漏洞,造成隐患的。其中最常见的便是:Access-Control-Allow-Origin: *到底。殊不知,*用于表示允许任意域访问,这种配置一般只用于共享公开资源。如果用于非公共资源的话,那就相当于击穿了浏览器的同源策略,给所有Origin授权。

其实这和授权授信有点像,当授权范围越大,方便的是操作/管理上,但这就容易被利用而被攻击。因此在允许的情况下,能粒度小点就尽量精细化控制(特别是敏感资源、接口),毕竟安全无小事。

Access-Control-Allow-Origin既然不建议配置为*,那么如何允许多域名呢?本系列上篇文章有详细分析,请参考:Access-Control-Allow-Origin

安全性这个东西是相对的,没有绝对的安全,也做不到绝对的安全。我们能做的,就是尽量去解决已知的安全性问题,不要让“入侵”来得很容易即可。

JSONP与CORS对比

JSONP与CORS的使用目的相同,并且都需要服务端和客户端同时支持,虽然功能上讲CORS更为强大,但......下面进行对比下

1.JSONP的最主要优势是对(老)浏览器的支持很好,而CORS由于出现较晚(2014年确定)这方面稍差~

  • 不过,还是那句话:现在都2021年了,在浏览器支持方面可以几乎不用再作考虑

2.JSONP 只能 用于Get请求,而CORS能用于所有的Http Method。这一点上JSONP被完虐

3.JSONP的错误处理机制不完善(其实是没有),当发生错误时开发者无法进行处理。而CORS可以通过onerror监听到错误事件,从而就可以看到错误详情方便排查问题

4.JSONP只会发送一次请求,而CORS的非简单请求会发送两次(大部分情况下的请求都会属于非简单请求)

  • 还不懂什么是简单请求和非简单请求,看本系列第一篇:Cors跨域(一):深入理解跨域请求概念及其根因

5.安全问题上,二者也有较大差异:

  • JSONP不是跨域的规范,它存在明显的安全漏洞。表现在:callback参数注入(这是由于这些元素都是裸露的),以及资源授权方面无法限制(也就说他能接受所有Origin的请求从而也不太安全)
  • CORS是跨域的规范,并且能够对资源授权方面做控制。Access-Control-Allow-Origin响应头就是最重要的一个响应头,当然喽若你把它恒定设为*,那它的安全性就大大退化

总的来讲,CORS相较于JSONP 优势明显 ,在实际生产使用上,忘了JSONP吧

总结

JSONP作为解决跨域问题的曾经的唯一方案,立下汗马功劳,现在是该退役了。但我们有理由记得它,毕竟英雄迟暮也希望不被遗忘(扯淡了,主要是这个名词在很多新/老文章中还经常被提起,注意分辨不要被弄迷糊啦)。

总而言之,作为新时代的开发人员,心里可认为跨域问题的解决方案只有一种:那便是Cors。下一篇将是“激动人心”的内容:讲述Cors在Spring环境中的实施,见识下那有多优雅吧

本文转载自微信公众号「BAT的乌托邦」,可以通过以下二维码关注。转载本文请联系BAT的乌托邦公众号。

责任编辑:姜华 来源: BAT的乌托邦
相关推荐

2014-08-19 10:36:02

AngularCORS

2019-04-10 10:32:16

CORSNginx反向代理

2020-08-13 07:04:45

跨域CORS浏览器

2022-03-21 07:35:34

处理方式跨域

2023-12-20 14:42:59

2021-06-15 07:32:59

Cookie和Sess实现跨域

2022-04-29 09:11:14

CORS浏览器

2020-08-31 19:20:33

浏览器CORS跨域

2013-11-27 10:23:23

2021-06-10 18:11:02

Cors跨域Web开发Cors

2019-03-13 14:15:25

CORS跨域资源前端

2021-06-17 07:15:36

Cors跨域多域名

2018-01-26 08:39:03

2018-12-12 15:50:13

2023-05-06 15:32:04

2020-12-18 09:36:01

JSONP跨域面试官

2019-11-11 17:34:16

前端开发技术

2024-01-31 07:55:52

2012-03-27 15:23:15

JSONPAJAX

2009-05-21 14:47:38

WEB开发JsonAjax
点赞
收藏

51CTO技术栈公众号