首页 > 技术文章 > cors跨域问题和一些零碎记录

zhangyjblogs 2021-02-22 21:29 原文

前言

最近做系统整合,涉及到统一认证和跨域问题,总结跨域如下,统一认证放另外博客

跨域总结

针对目前都是前后端分离项目,跨域问题很常见,跨域错误通常403如下

image-20210221180458705

针对跨域解决办法通常是创建filter CorsFilter,允许所有跨域,问题基本都能解决。

/**
 * 全局跨域配置
 */
@Configuration
public class GlobalCorsConfig {
    /**
     * 允许跨域调用的过滤器
     */
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        //允许所有域名进行跨域调用
        config.addAllowedOrigin("*");
        //允许跨越发送cookie
        config.setAllowCredentials(true);
        //放行全部原始头信息
        config.addAllowedHeader("*");
        //允许所有请求方法跨域调用
        config.addAllowedMethod("*");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

或者在NGINX上配置如下也可以解决

add_header Access-Control-Allow-Origin '*';
add_header Access-Control-Allow-Credentials 'true';
add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept";
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";

那么这样解决的背后原理是什么呢?

先说跨域的介绍和历史

跨域概念

CORS全称Cross-Origin Resource Sharing,意为跨域资源共享。当一个资源去访问另一个不同域名或者同域名不同端口的资源时,就会发出跨域请求。如果此时另一个资源不允许其进行跨域资源访问,那么访问的那个资源就会遇到跨域问题。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

跨域的历史

1995年由Netscape提出同源策略,浏览器在发送Ajax请求时,只接收同域服务器响应的数据资源;那么什么才算同域呢?很简单,协议、域名、端口全部相同才算同一域下,三个条件有一个不一致,都不算同域,既跨域;

因此,即使是我们自己的域名服务器,而二级域名或三级域名不一致,也会出现跨域,如:http://a.yhd.com与 http://b.yhd.com 之间需要数据交互,就跨域了;

这就让人很苦恼了,明明都是我自己的域名,我自己的服务器,还是受到了同源策略的保护无法进行数据交互;这不能算是设计缺陷,只能说当年考虑不周全,也有可能因为当年网络通信协议的不健全,原因不得而知;

解决跨域就是够使两个不同域下的数据进行顺利交互就可以了;各路解决方案中,最为典型的有两个:

JSONP和同域代理;

同域代理就是使用Ajax向同域下的后台发送请求,同时携带真实请求的地址及参数,后台接受请求后直接根据地址及参数转发请求,因为后台是可以直接模拟HTTP客户端发送请求的,所以没有跨域问题,而后台接受到响应数据后再原样返回给前端浏览器,从而实现跨域数据交互;

JSONP是利用了 script 标签的 src 属性来实现跨域数据交互的,因为浏览器解析HTML代码时,原生具有src属性的标签,浏览器都赋予其HTTP请求的能力,而且不受跨域限制,使用src发送HTTP请求,服务器直接返回一段JS代码的函数调用,将服务器数据放在函数实参中,前端提前写好响应的函数准备回调,接收数据,实现跨域数据交互;

JSONP和同域代理,本质上并没有解决Ajax跨域的问题,只是绕开这个问题而另辟蹊径实现的跨域数据交互,在数据交互层面上可以看做技术不成熟时的临时解决方案;但是JSONP 和同域代理 使用了很多年,当然跨域问题也存在了很多年,终于有人看不下去了,提出了浏览器与服务器跨域通信的安全性通信策略,它就是我们今天的主角 跨域资源共享(Cross-origin resource sharing),简称 CORS ; 有了它,我们可以安全放心的抛弃 JSONP 和 同域代理了,步入正统的跨域数据交互的殿堂;

跨域的解决方案:cors

CORS 的使用由一系列传输的HTTP头组成,这些HTTP头有两个作用,
1:用于阻止还是允许浏览器向其他域名发起请求;
2:用于接受还是拒绝其他域名返回的响应数据;
因此只要搞清楚什么样的头信息是控制浏览器发送还是不发送请求,什么样的头信息控制浏览器接受还是拒绝服务器的响应数据,这两点搞明白,CORS就算彻底搞清楚了;

跨域请求被分为了两种类型,一种是简单请求,一种是复杂请求 (需预检请求);简单请求与普通的ajax请求无异;但复杂请求,必须在正式发送请求前先发送一个OPTIONS方法的请求已得到服务器的同意,若没有得到服务器的同意,浏览器不会发送正式请求;因此下图跨域错误就是OPTIONS请求没有得到服务器的同意,浏览器不发送正式请求的错误。

image-20210221180458705

我们目前前后端交互content_type都是applicaion/json,这种属于复杂请求,因此我们忽略简单请求,只分析复杂请求。

例子如下:

前后端分离,前端运行在http://localhost:8090,访问后端接口http://localhost:8082/admin/login,因为端口不同,发生跨域,跨域会先发个OPTIONS请求判断服务器是否允许跨域,那么浏览器是如何知道服务器允许跨域呢?服务器会在response header内返回Access-Control-Allow-Origin,浏览器根据这个头判断接受服务器响应。

跨域必须会有http header的Origin字段,表示请求来源,比如下图是个预检(preFlight)请求,如下图

image-20210221204234122

服务端没有返回Access-Control-Allow-Origin,因此浏览器决绝发出跨域的正式请求,报错如下

image-20210221180458705

"预检"请求除了Origin字段,"预检"请求的头信息包括两个特殊字段。

(1)Access-Control-Request-Method

该字段是必须的,用来列出浏览器的CORS请求(正式请求)会用到哪些HTTP方法,上例是POST。

(2)Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是content_type。

在增加了CorsFilter后,请求截图如下:

image-20210221213240359

第一个/admin/login是预检请求OPTIONS,服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,做出回应,在response header内返回了Access-Control-Allow-Origin: http://localhost:8090,因此浏览器认为服务器允许跨域,因此跨域发送正式请求(Access-Control-Allow-Origin字段也可以设为星号,表示同意任意跨源请求。)。

第二个/admin/login是正式请求。

CORS中最常使用的request请求头为 Origin、Access-Control-Request-Headers、Access-Control-Request-Method;
CORS中最常使用的response响应头为 Access-Control-Allow-Origin、Access-Control-Allow-Headers、Access-Control-Expose-Headers;

request上送字段:

Origin:该字段必须,在request内上送,表示请求的来源,在跨域中,表示跨域请求的来源方

Access-Control-Request-Method:该字段必须,用来列出浏览器的CORS请求会用到哪些HTTP方法

Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是content_type

response响应字段:

Access-Control-Allow-Origin:该字段必需,请求的资源能共享给哪些域,它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求

Access-Control-Allow-Headers:该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。

Access-Control-Allow-Credentials:该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

Access-Control-Expose-Headers:该字段可选。CORS请求时,ajax请求XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。

Access-Control-Max-Age:该字段可选,用来指定本次预检请求的有效期,单位为秒。在此缓存期间,不用发出另一条预检请求。

CorsFilter简单分析

从上面分析,允许跨域,关键是response header内返回Access-Control-Allow-Origin,这个字段是在CorsFilter内,源码如下

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
			FilterChain filterChain) throws ServletException, IOException {

    if (CorsUtils.isCorsRequest(request)) {//request header有Origin则认为是跨域请求
        CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);
        if (corsConfiguration != null) {
            boolean isValid = this.processor.processRequest(corsConfiguration, request, response);//关键方法
            //CorsUtils.isPreFlightRequest(request)方法为true的情况:request header有Origin、Access-Control-Request-Method且http method是OPTIONS,则认为是预检
            if (!isValid || CorsUtils.isPreFlightRequest(request)) {
                return;//是预检方法,或者跨域不允许,直接返回
            }
        }
    }

    filterChain.doFilter(request, response);
}

//在关键方法内,如果允许跨域,handleInternal会给response header设置Access-Control-Allow-Origin,表示允许跨域

nginx设置允许跨域

通常允许跨域,可以简单粗暴使用nginx配置,而不使用CorsFilter,只需要设置如下即可

add_header Access-Control-Allow-Origin '*';
add_header Access-Control-Allow-Credentials 'true';
add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept";
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";

原因也是给response header设置Access-Control-Allow-Origin,这样浏览器就允许跨域了,通常许多项目也是这么简单粗暴的设置,或者粗暴的加上CorsFilter。

cors总结

跨域就是ajax请求访问不同域下的资源,由于同源策略控制,默认不允许跨域。跨域是浏览器和服务端共同实现的(http协议),浏览器都支持跨域,关键在服务器,服务器通过Access-Control-Allow-Origin来控制是否允许跨域。Access-Control-Allow-XXX头的作用是浏览器是否可以发起跨域,这个过程和前端代码无关。

一些其它零碎记录

AJAX请求

AJAX = Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)。

AJAX 不是新的编程语言,而是一种使用现有标准的新方法。

AJAX 最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容。

AJAX 不需要任何浏览器插件,但需要用户允许JavaScript在浏览器上执行。

后端如何判断是AJAX请求和json请求

//判断ajax请求
public static boolean isRequestAjax(HttpServletRequest request) {
    String ajaxHeader = request.getHeader("X-Requested-With");
    boolean isAjax = "XMLHttpRequest".equals(ajaxHeader);
    return isAjax;
}
//判断json请求
public static boolean isJsonRequest(HttpServletRequest request) {
    return request.getHeader("Accept") != null
        && request.getHeader("Accept").contains("application/json");
}

在服务器端判断request来自Ajax请求(异步)还是传统请求(同步):
两种请求在请求的Header不同,Ajax 异步请求比传统的同步请求多了一个头参数x-requested-with ,可以利用它,request.getHeader("x-requested-with"); 为 null,则为传统同步请求,为 XMLHttpRequest,则为 Ajax 异步请求。

AJAX请求和json请求区别

目前来说,前后端交互,数据类型都是application/json,数据类型是json也实际使用的是ajax请求,单纯使用ajax时候accept content_type是form表单。

ajax和axios请求json数据

目前前后端交互,通常使用的vue和react都是通过axios和后端发生交互,axios是通过promise实现对ajax技术的一种封装,就像jQuery实现ajax封装一样。
简单来说: ajax技术实现了网页的局部数据刷新,axios实现了对ajax的封装。axios是ajax 但ajax不止axios

跨域判断

//请求header有Origin则判断为跨域
public static boolean isCorsRequest(HttpServletRequest request) {
    return (request.getHeader("Origin") != null);
}

跨域例子如下图

image-20210219172159251

http header内的referer的作用

referer  是  HTTP  请求header 的一部分,当浏览器(或者模拟浏览器行为)向web 服务器发送请求的时候,头信息里有包含Referer。比如我在www.google.com 里有一个www.baidu.com 链接,那么点击这个www.baidu.com ,它的header 信息里就有:Referer=http://www.google.com 由此可以看出来吧。它就是表示一个来源

Referer的作用?
1.防盗链。
我在www.google.com里有一个www.baidu.com链接,那么点击这个www.baidu.com,它的header信息里就有:
Referer=http://www.google.com 
那么可以利用这个来防止盗链了,比如我只允许我自己的网站访问我自己的图片服务器,那我的域名是www.google.com,那么图片服务器每次取到Referer来判断一下是不是我自己的域名www.google.com,如果是就继续访问,不是就拦截。

这是不是就达到防盗链的效果了?

将这个http请求发给服务器后,如果服务器要求必须是某个地址或者某几个地址才能访问,而你发送的referer不符合他的要求,就会拦截或者跳转到他要求的地址,然后再通过这个地址进行访问。

2.防止恶意请求。
比如静态请求是*.html结尾的,动态请求是*.shtml,那么由此可以这么用,所有的*.shtml请求,必须 Referer  为我自己的网站。
Referer=http://www.google.com 
空Referer是怎么回事?什么情况下会出现Referer?
首先,我们对空 Referer  的定义为, Referer  头部的内容为空,或者,一个 HTTP  请求中根本不包含 Referer  头部。

那么什么时候 HTTP  请求会不包含 Referer  字段呢?根据Referer的定义,它的作用是指示一个请求是从哪里链接过来,那么当一个请求并不是由链接触发产生的,那么自然也就不需要指定这个请求的链接来源。

比如,直接在浏览器的地址栏中输入一个资源的URL地址,那么这种请求是不会包含 Referer  字段的,因为这是一个“凭空产生”的 HTTP  请求,并不是从一个地方链接过去的。

那么在防盗链设置中,允许空Referer和不允许空Referer有什么区别?
允许 Referer 为空,意味着你允许比如浏览器直接访问,就是空。

推荐阅读