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

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

代理服务器/网关方式


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


image.png

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


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

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


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


这是一段比较“著名”的、通用的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. 硬编码方式


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


/**
 * 在此处添加备注信息
 *
 * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a>
 * @site https://yourbatman.cn
 * @date 2021/6/9 10:36
 * @since 0.0.1
 */
@Slf4j
@WebServlet(urlPatterns = "/cors")
public class CorsServlet extends HttpServlet {
    @Override
    protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doOptions(req, resp);
        setCrosHeader(resp);
    }
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String requestURI = req.getRequestURI();
        String method = req.getMethod();
        String originHeader = req.getHeader("Origin");
        log.info("收到请求:{},方法:{}, Origin头:{}", requestURI, method, originHeader);
        resp.getWriter().write("hello cors...");
        setCrosHeader(resp);
    }
    private void setCrosHeader(HttpServletResponse resp) {
        resp.setHeader("Access-Control-Allow-Origin", "http://localhost:63342");
        resp.setHeader("Access-Control-Expose-Headers", "token,secret");
        resp.setHeader("Access-Control-Allow-Headers", "token,secret"); // 一般来讲,让此头的值是上面那个的【子集】(或相同)
    }
}



优点:个性化极强,可以针对接口级别给出不同的CORS逻辑,精细化控制

缺点:侵入性从应用级别上升到了业务代码级别,显得十分臃肿,粒度太细后期维护成本高

2. 自定义Filter/Interceptor


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

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


优点:应用级别的统一处理,对业务代码无侵入性。应用内集中化处理Cors逻辑,维护方便

缺点:无法做到接口级别的粒度,对于某些特殊要求的细粒度控制自然就无能为力


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


<dependency>
  <groupId>org.ebaysf.web</groupId>
  <artifactId>cors-filter</artifactId>
  <version>1.0.1</version>
</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年确定)这面稍差一些~

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


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


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


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

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


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

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

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


✍总结



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


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

相关文章
|
8天前
|
开发框架 中间件 Java
如何处理跨域资源共享(CORS)的 OPTIONS 请求?
处理 CORS 的 OPTIONS 请求的关键是正确设置响应头,以告知浏览器是否允许跨域请求以及允许的具体条件。根据所使用的服务器端技术和框架,可以选择相应的方法来实现对 OPTIONS 请求的处理,从而确保跨域资源共享的正常进行。
|
8天前
|
JavaScript 前端开发 API
跨域资源共享(CORS)的工作原理是什么?
跨域资源共享(CORS)通过浏览器和服务器之间的这种交互机制,在保证安全性的前提下,实现了跨域资源的访问,使得不同源的网页能够合法地获取和共享服务器端的资源,为现代Web应用的开发提供了更大的灵活性和扩展性。
|
22天前
|
JSON 前端开发 安全
CORS 是什么?它是如何解决跨域问题的?
【10月更文挑战第20天】CORS 是一种通过服务器端配置和浏览器端协商来解决跨域问题的机制。它为跨域资源共享提供了一种规范和有效的方法,使得前端开发人员能够更加方便地进行跨域数据交互。
|
1月前
|
缓存 前端开发 应用服务中间件
CORS跨域+Nginx配置、Apache配置
CORS跨域+Nginx配置、Apache配置
131 7
|
2月前
|
JSON 安全 前端开发
浅析CORS跨域漏洞与JSONP劫持
浅析CORS跨域漏洞与JSONP劫持
81 3
|
5月前
|
前端开发 安全 JavaScript
Spring Boot2 系列教程(十四)CORS 解决跨域问题
Spring Boot2 系列教程(十四)CORS 解决跨域问题
|
2月前
|
安全
CORS 跨域资源共享的实现原理
CORS 跨域资源共享的实现原理
|
3月前
|
Web App开发 JSON 数据格式
【Azure Developer】浏览器查看本地数据文件时遇见跨域问题(CORS)
【Azure Developer】浏览器查看本地数据文件时遇见跨域问题(CORS)
【Azure Developer】浏览器查看本地数据文件时遇见跨域问题(CORS)
|
3月前
|
API
【Azure Function】Function本地调试时遇见跨域问题(blocked by CORS policy)
【Azure Function】Function本地调试时遇见跨域问题(blocked by CORS policy)
【Azure Function】Function本地调试时遇见跨域问题(blocked by CORS policy)
|
3月前
|
安全 前端开发 Java
Web端系统开发解决跨域问题——以Java SpringBoot框架配置Cors为例
在Web安全上下文中,源(Origin)是指一个URL的协议、域名和端口号的组合。这三个部分共同定义了资源的来源,浏览器会根据这些信息来判断两个资源是否属于同一源。例如,https://www.example.com:443和http://www.example.com虽然域名相同,但由于协议和端口号不同,它们被视为不同的源。同源(Same-Origin)是指两个URL的协议、域名和端口号完全相同。只有当这些条件都满足时,浏览器才认为这两个资源来自同一源,从而允许它们之间的交互操作。
Web端系统开发解决跨域问题——以Java SpringBoot框架配置Cors为例