收款定制开发Access to XMLHttpRequest at ‘http://xx‘ from origin ‘http://xx‘ has been blocked by CORS policy:

问题场景

错误信息

Access to XMLHttpRequest at 'http://localhost:9090' from origin 'http://localhost:9090' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
  • 1
  • 2
  • 3
  • 4

翻译:【在http://localhost:9090对http://localhost:9090收款定制开发进行请求时被CORS 策略关闭,收款定制开发服务器资源没有设置Access-Control-Allow-Origin消息头】

收款定制开发这就是常说的跨域问题。

原因分析

1. 收款定制开发什么是同源策略

1.1 含义

1995年,收款定制开发同源政策由 Netscape 收款定制开发公司引入浏览器。目前,收款定制开发所有浏览器都实行这个政策。

最初,收款定制开发它的含义是指,A收款定制开发网页设置的 Cookie,B收款定制开发网页不能打开,收款定制开发除非这两个网页"同源"。所谓"同源"指的是"三个相同"。

  • 协议相同
  • 域名相同
  • 端口相同

举例来说,http://www.example.com/dir/page.html这个网址,协议是http://,域名是www.example.com,端口是80(收款定制开发默认端口可以省略)。收款定制开发它的同源情况如下。

http://www.example.com/dir2/other.html:同源http://example.com/dir/other.html:不同源(域名不同)http://v2.www.example.com/dir/other.html:不同源(域名不同)http://www.example.com:81/dir/other.html:不同源(端口不同)
  • 1
  • 2
  • 3
  • 4

1.2 目的

收款定制开发同源政策的目的,收款定制开发是为了保证用户信息的安全,收款定制开发防止恶意的网站窃取数据。

收款定制开发设想这样一种情况:A收款定制开发网站是一家银行,收款定制开发用户登录以后,收款定制开发又去浏览其他网站。收款定制开发如果其他网站可以读取A网站的 Cookie,收款定制开发会发生什么?

很显然,如果 Cookie 包含隐私(收款定制开发比如存款总额),收款定制开发这些信息就会泄漏。收款定制开发更可怕的是,Cookie 收款定制开发往往用来保存用户的登录状态,收款定制开发如果用户没有退出登录,收款定制开发其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。

由此可见,"同源政策"是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。

1.3 限制范围

随着互联网的发展,"同源政策"越来越严格。目前,如果非同源,共有三种行为受到限制。

(1) Cookie、LocalStorage 和 IndexDB 无法读取。

(2) DOM 无法获得。

(3) AJAX 请求不能发送。

虽然这些限制是必要的,但是有时很不方便,合理的用途也受到影响。

2. 什么是跨域资源共享 CORS

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出请求,从而克服了AJAX只能同源使用的限制。

2.1 简介

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

整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与普通的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨域,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感知。因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨域通信。

2.2 两种请求

CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

只要同时满足以下两大条件,就属于简单请求。

(1)请求方法是以下三种方法之一。

HEADGETPOST
  • 1
  • 2
  • 3

(2)HTTP 的头信息不超出以下几种字段。

AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
  • 1
  • 2
  • 3
  • 4
  • 5

凡是不同时满足上面两个条件,就属于非简单请求。一句话,简单请求就是简单的 HTTP 方法与简单的 HTTP 头信息的结合。

这样划分的原因是,表单在历史上一直可以跨域发出请求。简单请求就是表单请求,浏览器沿袭了传统的处理方式,不把行为复杂化,否则开发者可能转而使用表单,规避 CORS 的限制。对于非简单请求,浏览器会采用新的处理方式。

2.3 简单请求

2.3.1 基本流程

对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是在头信息之中,增加一个Origin字段。

下面是一个例子,浏览器发现这次跨域 AJAX 请求是简单请求,就自动在头信息之中,添加一个Origin字段。

GET /cors HTTP/1.1Origin: http://api.bob.comHost: api.alice.comAccept-Language: en-USConnection: keep-aliveUser-Agent: Mozilla/5.0...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

上面的头信息中,Origin字段用来说明,本次请求来自哪个域(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的 HTTP 回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为 HTTP 回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://api.bob.comAccess-Control-Allow-Credentials: trueAccess-Control-Expose-Headers: FooBarContent-Type: text/html; charset=utf-8
  • 1
  • 2
  • 3
  • 4

上面的头信息之中,有三个与 CORS 请求相关的字段,都以Access-Control-开头。

(1)Access-Control-Allow-Origin

该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

(2)Access-Control-Allow-Credentials

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

(3)Access-Control-Expose-Headers

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

2.3.2 withCredentials 属性

上面说到,CORS 请求默认不包含 Cookie 信息(以及 HTTP 认证信息等)。如果需要包含 Cookie 信息,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。

Access-Control-Allow-Credentials: true
  • 1

另一方面,开发者必须在 AJAX 请求中打开withCredentials属性。

var xhr = new XMLHttpRequest();xhr.withCredentials = true;
  • 1
  • 2

否则,即使服务器同意发送 Cookie,浏览器也不会发送。或者,服务器要求设置 Cookie,浏览器也不会处理。

但是,如果省略withCredentials设置,有的浏览器还是会一起发送 Cookie。这时,可以显式关闭withCredentials。

xhr.withCredentials = false;
  • 1

需要注意的是,如果要发送 Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie 依然遵循同源政策,只有用服务器域名设置的 Cookie 才会上传,其他域名的 Cookie 并不会上传,且(跨域)原网页代码中的document.cookie也无法读取服务器域名下的 Cookie。

2.4 非简单请求

2.4.1 预检请求

非简单请求是那种对服务器提出特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。

非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为“预检”请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。这是为了防止这些新增的请求,对传统的没有 CORS 支持的服务器形成压力,给服务器一个提前拒绝的机会,这样可以防止服务器大量收到DELETE和PUT请求,这些传统的表单不可能跨域发出的请求。

下面是一段浏览器的 JavaScript 脚本。

var url = 'http://api.alice.com/cors';var xhr = new XMLHttpRequest();xhr.open('PUT', url, true);xhr.setRequestHeader('X-Custom-Header', 'value');xhr.send();
  • 1
  • 2
  • 3
  • 4
  • 5

上面代码中,HTTP 请求的方法是PUT,并且发送一个自定义头信息X-Custom-。

浏览器发现,这是一个非简单请求,就自动发出一个“预检”请求,要求服务器确认可以这样请求。下面是这个“预检”请求的 HTTP 头信息。

OPTIONS /cors HTTP/1.1Origin: http://api.bob.comAccess-Control-Request-Method: PUTAccess-Control-Request-Headers: X-Custom-HeaderHost: api.alice.comAccept-Language: en-USConnection: keep-aliveUser-Agent: Mozilla/5.0...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

“预检”请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

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

(1)Access-Control-Request-Method

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

(2)Access-Control-Request-Headers

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

2.4.2 预检请求的回应

服务器收到“预检”请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

HTTP/1.1 200 OKDate: Mon, 01 Dec 2008 01:15:39 GMTServer: Apache/2.0.61 (Unix)Access-Control-Allow-Origin: http://api.bob.comAccess-Control-Allow-Methods: GET, POST, PUTAccess-Control-Allow-Headers: X-Custom-HeaderContent-Type: text/html; charset=utf-8Content-Encoding: gzipContent-Length: 0Keep-Alive: timeout=2, max=100Connection: Keep-AliveContent-Type: text/plain
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

上面的 HTTP 回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。

Access-Control-Allow-Origin: *
  • 1

如果服务器否定了“预检”请求,会返回一个正常的 HTTP 回应,但是没有任何 CORS 相关的头信息字段,或者明确表示请求不符合条件。

OPTIONS http://api.bob.com HTTP/1.1Status: 200Access-Control-Allow-Origin: https://notyourdomain.comAccess-Control-Allow-Method: POST
  • 1
  • 2
  • 3
  • 4

上面的服务器回应,Access-Control-Allow-Origin字段明确不包括发出请求的http://api.bob.com。

这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。

XMLHttpRequest cannot load http://api.alice.com.Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
  • 1
  • 2

服务器回应的其他 CORS 相关字段如下。

Access-Control-Allow-Methods: GET, POST, PUTAccess-Control-Allow-Headers: X-Custom-HeaderAccess-Control-Allow-Credentials: trueAccess-Control-Max-Age: 1728000
  • 1
  • 2
  • 3
  • 4

(1)Access-Control-Allow-Methods

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

(2)Access-Control-Allow-Headers

如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在“预检”中请求的字段。

(3)Access-Control-Allow-Credentials

该字段与简单请求时的含义相同。

(4)Access-Control-Max-Age

该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

2.4.3 浏览器的正常请求和回应

一旦服务器通过了“预检”请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

下面是“预检”请求之后,浏览器的正常 CORS 请求。

PUT /cors HTTP/1.1Origin: http://api.bob.comHost: api.alice.comX-Custom-Header: valueAccept-Language: en-USConnection: keep-aliveUser-Agent: Mozilla/5.0...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

上面头信息的Origin字段是浏览器自动添加的。

下面是服务器正常的回应。

Access-Control-Allow-Origin: http://api.bob.comContent-Type: text/html; charset=utf-8
  • 1
  • 2

上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。

解决方案

基于Spring Boot项目

方式1 后台服务配置CORS支持

CORS允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

基于HttpServletResponse返回数据

在后台应用接口,使用HttpServletResponse返回数据,并添加Access-Control-Allow-Origin响应头。

        String result = JSON.toJSONString(user);        response.setCharacterEncoding("UTF-8");        response.setContentType("text/html;charset=UTF-8");        response.setHeader("Access-Control-Allow-Origin","*");        response.getWriter().write(result);
  • 1
  • 2
  • 3
  • 4
  • 5

也可以使用@CrossOrigin注解,配置在在Controller类或者接口方法上。

@CrossOrigin(origins = {"*"})
  • 1

也可以在Spring MVC配置类WebMvcConfigurer中添加CorsMapping,还有其他方式,道理都一样。

 @Override    public void addCorsMappings(CorsRegistry registry) {        registry.addMapping("/**")                .allowedOrigins("*")                .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")                .maxAge(3600)                .allowCredentials(true);    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

方式2 jsoup

原理

JSONP的最基本的原理是:动态添加一个<script>标签,而script标签的src属性是没有跨域的限制的。这样说来,这种跨域方式其实与ajax XmlHttpRequest协议无关了。

如果设为dataType: ‘jsonp’,这个$.ajax方法就和ajax XmlHttpRequest没什么关系了,取而代之的则是JSONP协议。JSONP是一个非官方的协议,它允许在服务器端集成Script tags返回至客户端,通过javascript callback的形式实现跨域访问。

JSONP即JSON with Padding。由于同源策略的限制,XmlHttpRequest只允许请求当前源(域名、协议、端口)的资源。如果要进行跨域请求, 我们可以通过使用html的script标记来进行跨域请求,并在响应中返回要执行的script代码,其中可以直接使用JSON传递javascript对象。 这种跨域的通讯方式称为JSONP。

优缺点

优点:它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制;它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持;并且在请求完毕后可以通过调用callback的方式回传结果。

缺点:它只支持GET请求而不支持POST等其它类型的HTTP请求;它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题。

实际使用测试后,发现,这种方式前后端都需要修改,目前并不是一个很好的解决方案。。。。

改造步骤

  1. 改造页面,添加Jsonp请求
<script src="https://s3.pstatp.com/cdn/expire-1-M/jquery/3.3.1/jquery.min.js"></script>    <script type="text/javascript">        function sub() {            $.ajax({                url: "http://localhost:9111/v1/test",                type: "GET",                dataType: "jsonp",                success: function (data) {                    var result = JSON.stringify(data);                    alert("success" + result)                },                error: function (data) {                    var result = JSON.stringify(data);                    alert("error" + result)                }            });        }    </script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  1. 改造后端,返回指定格式的数据
    @GetMapping("/test")    // @CrossOrigin(origins = {"*"})    @ResponseBody    public void test(HttpServletResponse response, HttpServletRequest request) throws IOException {        System.out.println("=============" + this.user.getUserId());        String result = JSON.toJSONString(user);        response.setCharacterEncoding("UTF-8");        response.setContentType("text/html;charset=UTF-8");        String callback = request.getParameter("callback");        result = callback + "(" + result + ")";        response.getWriter().write(result);    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

流程分析

发送请求时,会自动传递一个随机生成的callback回调参数。

后台返回的数据是一个字符串,会使用回调参数名,加上在小括号包裹数据返回。

Jsonp接受到数据后,会处理小括号内的数据,并传递给回调函数success。

方式3 Nginx代理

在之前两种方式,都需要修改代码,如果我们的前端应用采用Nginx作为容器时,则可以直接采用代理转发就能解决跨域了。

搭建案例

  1. 首先在下载一个Nginx

  2. 解压,进入主目录,并修改index.html为以下内容,直接发送一个跨域请求。

<!DOCTYPE html><html><head><title>Welcome to nginx!</title>    <script src="https://s3.pstatp.com/cdn/expire-1-M/jquery/3.3.1/jquery.min.js"></script>    <script type="text/javascript">        function callback(data) {            var result = JSON.stringify(data);            alert("回调函数" + result)        }        function sub() {            $.ajax({                url: "http://localhost:9111/v1/test",                type: "GET",                dataType: "json",                success: function (data) {                    var result = JSON.stringify(data);                    alert("success" + result)                },                error: function (data) {                    var result = JSON.stringify(data);                    alert("error" + result)                }            });        }    </script></head><body><button onclick="sub() " type="button">测试跨域</button></body></html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  1. 启动nginx,点击按钮,发现跨域异常。

配置代理

  1. 修改请求路径,直接访问本身nginx中的路径。
function sub() {            $.ajax({                url: "/v1/test",                type: "GET",                dataType: "json",                success: function (data) {                    var result = JSON.stringify(data);                    alert("success" + result)                },                error: function (data) {                    var result = JSON.stringify(data);                    alert("error" + result)                }            });        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  1. 配置代理,将请求代理到跨域服务器。
location ^~ /v1/ {   proxy_pass   http://localhost:9111;}
  • 1
  • 2
  • 3
  1. 刷新nginx,测试,发现解决跨域问题。

参考文档

http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html
http://www.ruanyifeng.com/blog/2016/04/cors.html
https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy

网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发