一文弄懂 CORS 跨域(前端+后端代码实例讲解)

常常被问到一些问题,好比写 Java 服务端的同窗的来问:我服务端明明正确返回了,测试环境 debug 能看到,为何前端就是拿不到数据? 而后写前端的同窗会问:为何我明明设置了 withCredentials=true,服务端同窗仍是拿不到 cookie?javascript

因此决定从新捋一捋使用 CORS 解决跨域的问题,先后端要怎么作?为何这么作?html

为何会有跨域的问题?

为了保证用户信息的安全,全部的浏览器都遵循同源策略,那什么状况下算同源呢?同源策略又是什么呢?前端

记住:协议、域名、端口号彻底相同时,才是同源java

能够参考 Web安全 - 浏览器的同源策略git

在同源策略下,会有如下限制:github

  • 没法获取非同源的 Cookie、LocalStorage、SessionStorage 等
  • 没法获取非同源的 dom
  • 没法向非同源的服务器发送 ajax 请求

可是咱们又常常会遇到先后端分离,不在同一个域名下,须要ajax请求数据的状况。那咱们就要规避这种限制。web

能够在网上搜到不少解决跨域的方法,有些方法比较古老了,如今项目中用的比较多的是 jsonp 和 CORS(跨域资源共享),这篇主要讲 CORS 的原理和具体实践。ajax

CORS 跨域原理

CORS 跨域的原理其实是浏览器与服务器经过一些 HTTP 协议头来作一些约定和限制。能够查看 HTTP-访问控制(CORS)数据库

与跨域相关的协议头json

请求头 说明
Origin 代表预检请求或实际请求的源站 URI,不论是否跨域ORIGIN 字段老是被发送
Access-Control-Request-Method 将实际请求所使用的 HTTP 方法告诉服务器
Access-Control-Request-Headers 将实际请求所携带的首部字段告诉服务器
响应头 说明
Access-Control-Allow-Origin 指定容许访问该资源的外域 URI,对于携带身份凭证的请求不可以使用通配符*
Access-Control-Expose-Headers 指定 XMLHttpRequest的getResponseHeader 能够访问的响应头
Access-Control-Max-Age 指定 preflight 请求的结果可以被缓存多久
Access-Control-Allow-Credentials 是否容许浏览器读取 response 的内容;
当用在 preflight 预检请求的响应中时,指定实际的请求是否可以使用 credentials
Access-Control-Allow-Methods 指明实际请求所容许使用的 HTTP 方法
Access-Control-Allow-Headers 指明实际请求中容许携带的首部字段

代码实例

这里写了个 demo,一步步来分析。目录以下:

.
├── README.md
├── client
│   ├── index.html
│   └── request.js
└── server
    ├── pom.xml
    ├── server-web
    │   ├── pom.xml
    │   ├── server-web.iml
    │   └── src
    │       └── main
    │           ├── java
    │           │   └── com
    │           │       └── example
    │           │           └── cors
    │           │               ├── constant
    │           │               │   └── Constants.java
    │           │               ├── controller
    │           │               │   └── CorsController.java
    │           │               └── filter
    │           │                   └── CrossDomainFilter.java
    │           ├── resources
    │           │   └── config
    │           │       └── applicationContext-core.xml
    │           └── webapp
    │               ├── WEB-INF
    │               │   ├── dispatcher-servlet.xml
    │               │   └── web.xml
    │               └── index.jsp
    └── server.iml
复制代码

  • Client:前端,简单的ajax请求

在client文件夹下,启动静态服务器,前端页面经过http://localhost:8000/index.html访问:

anywhere -h localhost -p 8000
复制代码

  • Server: java项目,SpringMVC

在 IntelliJ IDEA 中本地启动 tomcat,设置host: http://localhost:8080/,服务端数据经过http://localhost:8080/server/cors请求。

这里前端和后端由于端口号不一样,存在跨域限制,下面经过 CORS 来解决由于跨域没法经过ajax请求数据的问题。


没有容许跨域的状况

这种状况就是前端什么都不作,服务端也什么都不作。

Client: 请求成功后,将数据显示在页面上

new Request().send('http://localhost:8080/server/cors',{
	success: function(data){
		document.write(data)
	}
});
复制代码

Server:

@Controller
@RequestMapping("/server")
public class CorsController {
    @RequestMapping(value="/cors", method= RequestMethod.GET)
    @ResponseBody
    public String ajaxCors(HttpServletRequest request) throws Exception{
        return "SUCCESS";
    }
}
复制代码

在浏览器地址栏输入http://localhost:8080/server/cors直接请求服务端,能够看到返回结果: ‘SUCCESS’

server_1.png

在浏览器地址栏输入http://localhost:8000/index.html,从不一样域的网页中向 Server 发送 ajax 请求。能够看到几个方面:

从 network 能够看到,请求返回正常。

client_1_network.png

但 Response 中没有内容,显示 Failed to load response data

client_network_response.png

而且控制台报错:

client_1_console_error.png

总结:

一、浏览器请求是发出去了的,服务端也会正确返回,可是咱们拿不到response的内容

二、浏览器控制台会报错提示能够怎么作,并且提示的很明白: xhr不能请求http://localhost:8080/server/cors,请求资源的响应头中没有设置Access-Control-Allow-Origin,Origin:http://localhost:8000是不容许跨域请求的。

那下一步,咱们要在服务端响应跨域请求时,设置响应头: Access-Control-Allow-Origin

设置 Access-Control-Allow-Origin 容许跨域

先说明为何要设置 Access-Control-Allow-Origin,能够把 Access-Control-Allow-Origin 看成一个指令,服务端设置 Access-Control-Allow-Origin 就是告诉浏览器容许向服务端请求资源的域名,浏览器经过 Response 中的 Access-Control-Allow-Origin 就能够知道能不能把数据吐出来。

官方解释是这样的: Access-Control-Allow-Origin 响应头指定了该响应的资源是否被容许与给定的 origin 共享。

Access-Control-Allow-Origin能够设置的值有:

Access-Control-Allow-Origin: *
Access-Control-Allow-Origin:
复制代码

那在java服务端给响应头设置 Access-Control-Allow-Origin 能够这么作:

一、添加一个过滤器

public class CrossDomainFilter implements Filter{
    public void init(FilterConfig filterConfig) throws ServletException {}
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse resp = (HttpServletResponse)servletResponse;
        resp.setHeader("Access-Control-Allow-Origin", "http://localhost:8000");
        filterChain.doFilter(servletRequest,servletResponse);
    }
    public void destroy() {}
}
复制代码

二、而后在web.xml文件中添加过滤器配置:

crossDomainFilter
    com.example.cors.filter.CrossDomainFilter
    crossDomainFilter
    /*
复制代码

三、而后从新启动tomcat,client从新发送请求http://localhost:8000/index.html

client_2_access_success.png

client_2_access_response.png

能够看到咱们可以拿到返回结果了,响应头中有咱们在服务端设置的Access-Control-Allow-Origin: http://localhost:8000,这个应该跟请求头中的origin一致,或者设置Access-Control-Allow-Origin:*也是能够的,这就容许任何网站来访问资源了(前提是不带凭证信息,这个后面讲)

以上就是容许一个简单的跨域请求的作法,只须要服务端设置响应头Access-Control-Allow-Origin。

简单请求与预检请求

上面讲述了一个简单请求经过在服务端设置响应头 Access-Control-Allow-Origin 就能够完成跨域请求。

那怎样的请求算是一个简单请求?与简单请求相对应的是什么样的请求呢?解决跨域的方式又有什么不同呢?

符合如下条件的可视为简单请求:

一、使用下列 HTTP 方法之一

- GET
- HEAD
- POST,而且Content-Type的值在下列之一:
  - text/plain
  - multipart/form-data
  - application/x-www-form-urlencoded
复制代码

二、而且请求头中只有下面这些

- Accept
- Accept-Language
- Content-Language
- Content-Type (须要注意额外的限制)
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
复制代码

不知足上述要求的在发送正式请求前都要先发送一个预检请求,预检请求以 OPTIONS 方法发送,浏览器经过请求方法和请求头可以判断是否发送预检请求。

好比 Client 发送以下请求:

new Request().send('http://localhost:8080/server/options',{
	method: 'POST',
	header: {
		'Content-Type': 'application/json'  //告诉服务器实际发送的数据类型
	},
	success: function(data){
		document.write(data)
	}
});
复制代码

Server 端处理请求的 controller:

@Controller
@RequestMapping("/server")
public class CorsController {
    @RequestMapping(value="/options", method= RequestMethod.POST)
    @ResponseBody
    public String options(HttpServletRequest request) throws Exception{
        return "SUCCESS";
    }
}
复制代码

由于请求时,请求头中塞入了 header,'Content-Type': 'application/json'。根据前面讲述的能够知道,浏览器会以 OPTIONS 方法发出一个预检请求,浏览器会在请求头中加入:

Access-Control-Request-Headers:content-type
Access-Control-Request-Method:POST
复制代码

这个预检请求的做用在这里就是告诉服务器:我会在后面请求的请求头中以 POST 方法发送数据类型是application/json 的请求,询问服务器是否容许。

client_3_options_error.png

在这里服务器尚未作任何容许这种请求的设置,因此浏览器控制台报错:

client_3_options_console_error.png

也清楚的说明了出错的缘由: 服务端在预检请求的响应中没有告诉浏览器容许协议头 Content-Type,即服务端须要设置响应头 Access-Control-Allow-Headers,容许浏览器发送带 Content-Type 的请求。

Server端过滤器中添加Access-Control-Allow-Headers:

public class CrossDomainFilter implements Filter{
    public void init(FilterConfig filterConfig) throws ServletException {}
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse resp = (HttpServletResponse)servletResponse;
        resp.setHeader("Access-Control-Allow-Origin", "http://localhost:8000");
        resp.setHeader("Access-Control-Allow-Headers", "Content-Type");
        filterChain.doFilter(servletRequest,servletResponse);
    }
    public void destroy() {}
}
复制代码

能够看到请求成功

client_3_options_console_success.png

再来看请求的具体信息,第一次以 OPTIONS 方法发送预检请求,浏览器设置请求头:

Access-Control-Request-Headers:content-type //请求中加入的请求头
Access-Control-Request-Method:POST  //跨域请求的方法
复制代码

服务端设置响应头:

Access-Control-Allow-Headers:Content-Type   //容许的header
Access-Control-Allow-Origin:http://localhost:8000 //容许跨域的源
复制代码

client_3_options_success.png

也能够设置 Access-Control-Allow-Methods 来限制客户端的的请求方法。

这样预检请求成功了,浏览器会发出第二个请求,这是真正请求数据的请求:

client_3_options_2_post.png

能够看到 POST 请求成功了,第二次请求头中没有设置 Access-Control-Request-Headers 和 Access-Control-Request-Method。

可是这里有个问题,须要预检请求时,浏览器会发出两次请求,一次 OPTIONS,一次 POST。两次都返回了数据。这样服务端若是逻辑复杂一些,好比去数据库查找数据,从 web 层、 service 到数据库这段逻辑就会走两遍,浏览器会两次拿到相同的数据,因此服务端的 filter 能够改一下,若是是 OPTIONS 请求,在设置完跨域请求响应头后就不走后面的逻辑直接返回。

public class CrossDomainFilter implements Filter{
    public void init(FilterConfig filterConfig) throws ServletException {}
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse resp = (HttpServletResponse)servletResponse;
        resp.setHeader("Access-Control-Allow-Origin", "http://localhost:8000");
        resp.setHeader("Access-Control-Allow-Headers", "Content-Type");   
        //OPTION请求就直接返回
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        if (req.getMethod().equals("OPTIONS")) {
            resp.setStatus(200);
            resp.flushBuffer();
        }else {
            filterChain.doFilter(servletRequest,servletResponse);
        }
    }
    public void destroy() {}
}
复制代码

总结:

一、 对于 POST 请求设置响应头Content-Type为某些值、自定义请求头等状况,浏览器会先以OPTIONS方法发送一个预检请求,并设置相应的请求头。

二、 服务端仍是正常返回,但若是预检请求响应头中不设置相应的响应头,预检请求不经过,不会再发出第二次请求来获取数据。

三、 服务端设置相应的响应头,浏览器会发出第二个请求,并将服务端返回的数据吐出,咱们能够得到response的内容

带凭证信息的请求

还有一种状况咱们常常遇到。浏览器在发送请求时须要给服务端发送 cookie,服务端根据 cookie 中的信息作一些身份验证等。

默认状况下,浏览器向不一样域的发送 ajax 请求,不会携带发送 cookie 信息。

Client:

var containerElem = document.getElementById('container')
new Request().send('http://localhost:8080/server/testCookie',{
	success: function(data){
		containerElem.innerHTML = data
	}
});
复制代码

Server:

@RequestMapping(value="/testCookie", method= RequestMethod.GET)
@ResponseBody
public String testCookie(HttpServletRequest request,HttpServletResponse response) throws Exception{
    String str = "SUCCESS";
    Cookie[] cookies = request.getCookies();
    String school = getSchool(cookies);
    if(school == null || school.length() == 0){
        addCookie(response);
    }
    return str + buildText(cookies);
}
复制代码

服务端收到请求,判断 cookie 中有没有 school,没有就添加 cookie.

client_4_cookie_none.png

能够看到响应头中有 Set-Cookie,再次请求时,若是是同源请求,浏览器会将 Set-Cookie 中的值放在请求头中,可是对于跨域请求,默认是不发送这个 Cookie 的。

若是要让浏览器发送 cookie,须要在 Client 设置 XMLHttpRequest 的 withCredentials 属性为 true。

Client:

var containerElem = document.getElementById('container')
new Request().send('http://localhost:8080/server/testCookie',{
	withCredentials: true,
	success: function(data){
		containerElem.innerHTML = data
	}
});
复制代码

如今浏览器在请求头中加入了 cookie 信息

client_4_cookie_error.png

可是服务端返回的数据没有在页面中展现,而且报错:

client_4_credentials_error.png

报错信息很明白: 当请求中包含凭证信息时,须要设置响应头 Access-Control-Allow-Credentials,是否带凭证信息是由 XMLHttpRequest的withCredentials 属性控制的。
**
因此咱们在 Server 端 filter 中加入这个响应头:

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse resp = (HttpServletResponse)servletResponse;
        resp.setHeader("Access-Control-Allow-Origin", "http://localhost:8000");
        resp.setHeader("Access-Control-Allow-Credentials","true");
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        if (req.getMethod().equals("OPTIONS")) {
            resp.setStatus(200);
            resp.flushBuffer();
        }else {
            filterChain.doFilter(servletRequest,servletResponse);
        }
    }
复制代码

如今浏览器知道响应头中 Access-Control-Allow-Credentials 为 true,就会把数据给吐出来了,咱们可以从response 中拿到内容了。

client_4_cookie_success_console.png

client_4_cookie_success.png

那若是附带凭证信息而且有预检请求呢?若是有预检请求,并附带凭证信息( XMLHttpRequest 的withCredentials 设置为 true), 服务端须要设置 Access-Control-Allow-Credentials: true,不然浏览器不会发出第二次请求,并报错。

client_4_cookie_preflight_error.png

总结:

一、跨域请求时,浏览器默认不会发送cookie,须要设置XMLHttpRequest的withCredentials属性为true

二、 浏览器设置XMLHttpRequest的withCredentials属性为true,代表要向服务端发送凭证信息(这里是cookie)。那么服务端就须要在响应头中添加Access-Control-Allow-Credentials为true。不然浏览器上有两种状况:

  • 若是是简单请求,服务端结果吐出了,浏览器拿到了但就是不给吐出来,并报错。
  • 若是是预检请求,一样咱们拿不到返回结果,并报错提示预检请求不经过,不会再发第二次请求。

其余

cookie 的同源策略

另外就是设置了 XMLHttpRequest 的 withCredentials 属性为 true,浏览器发出去了,服务端仍是拿不到 cookie的问题。

cookie 也遵循同源策略的,在设置 cookie 的时候能够发现除了键值对,还能够设置 cookie 的这些值:

cookie属性值 说明
path 可访问 cookie 的路径,默认为当前文档位置的路径
domain 可访问 cookie 的域名,默认为当前文档位置的路径的域名部分
max-age 多久后失效,秒为单位时间。
负数:session 内有效;0:删除 cookie;正数:有效期为建立时刻 + max-age
expires cookie 失效日期.若是没有定义,cookie 会在对话结束时过时,即会话 cookie
secure cookie 只经过 https 协议传输

若是获取不到 cookie,能够检查下 cookie 的 domain 和 path.

IE 上跨域访问没有权限

在跨域发送 ajax 请求时提示没有权限。 由于IE浏览器默认对跨域访问有限制。须要在浏览器设置中去除限制。
方法: 设置 > Internet 选项 > 安全 > 自定义级别 > 在设置中找到其余 - 在【其余】中将【经过域访问数据源】启用。

Demo 源码

CORS Demo 源码

参考

相关文章
相关标签/搜索