遇到跨域怎么办

跨域在开发时经常遇到,因此总结一下

什么是跨域

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

简单请求和非简单请求

浏览器将 CORS 请求分为两类:

  • 简单请求(simple request)
  • 非简单请求(not-so-simple request)

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

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

  • HEAD
  • GET
  • POST

(2)HTTP 的 header 不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

https://www.ruanyifeng.com/blog/2016/04/cors.html
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS

搭建测试环境

先搭建一个环境再仔细讲解吧。总共有两个部分:

  • 基于 SpringBoot 的后端
  • 基于 jQuery 的前端

基于 SpringBoot 的后端

创建一个 controller

1
2
3
4
5
6
7
8
@RestController
public class TestController {

@RequestMapping("test")
public ApiResult test() {
return new ApiResult("ok");
}
}

返回数据包装类 ApiResult 的定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ApiResult {

private String data;

public ApiResult() {
}

public ApiResult(String data) {
this.data = data;
}

public String getData() {
return data;
}

public void setData(String data) {
this.data = data;
}
}

这样访问 http://localhost:8080/test 时就能得到以下 json 数据

1
2
3
{
"data": "ok"
}

基于 jQuery 的前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>前端测试环境</title>
<script src="jquery-3.4.1.min.js"></script>
</head>
<body>
<a href="#" onclick="test()">向后端发起请求</a>

<script>
function test() {
$.getJSON('http://localhost:8080/test').then(
function(result) {
console.log(result);
}
);
}
</script>
</body>
</html>

鼠标点击 <a> 标签,Chrome 浏览器的控制台就会报错

网页的域名是 http://127.0.0.1:8081/,向另外一个域名 http://localhost:8080/test 发起请求,因为域名不同,因此被浏览器的 CORS policy 给 block 了

产生跨域的原因

有 3 个原因:

  1. 浏览器的限制

浏览器会自身不允许跨域

  1. 协议、域名、端口任何一个不一样都会产生跨域

  2. 发起的请求是 XHR(XMLHttpRequest)请求

只有 XHR(XMLHttpRequest)请求才会发生跨域,如果是普通的请求则不会发生。比如这样

1
<img src="http://localhost:8080/test" />

如何解决

Filter

点击按钮,向 http://localhost:8080/test 发起请求。浏览器会校验请求返回时 HTTP 的 Response Header 里的跨域相关 header,比如 Access-Control-Allow-OriginAccess-Control-Allow-Method 等。所以,可以利用 Filter 给 HttpServletResponse 添加上相应的 header

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CorsFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
httpServletResponse.addHeader("Access-Control-Allow-Origin", "http://127.0.0.1:8081");
httpServletResponse.addHeader("Access-Control-Allow-Methods", "GET");
filterChain.doFilter(servletRequest, servletResponse);
}

@Override
public void destroy() {
}
}

然后配置一下

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class FilterConfig {

@Bean
public FilterRegistrationBean<CorsFilter> registerFilter() {
FilterRegistrationBean<CorsFilter> registration = new FilterRegistrationBean<>();
registration.addUrlPatterns("/*");
registration.setFilter(new CorsFilter());
return registration;
}
}

这时候再次发起请求,看一下 response header

这样浏览器就能允许 http://127.0.0.1:8081/ 的网页向 http://127.0.0.1:8081 发起 HTTP GET 请求了

filter 还可以改一下允许向任何域发起任何 http 方法的请求

1
2
httpServletResponse.addHeader("Access-Control-Allow-Origin", "*");
httpServletResponse.addHeader("Access-Control-Allow-Methods", "*");

简单请求和复杂请求

只靠 Access-Control-Allow-OriginAccess-Control-Allow-Method 解决跨域是不够的,因为跨域请求分为简单请求和复杂请求

什么是简单请求?

同时满足以下两个条件

  1. HTTP 方法是下面的其中一种
  • HEAD
  • GET
  • POST
  1. request header 中
  • 没有自定义 header
  • 并且 Content-Type 为以下几种中的一种:
    • text/plain
    • application/x-www-form-urlencoded
    • multipart/form-data

什么是复杂请求?

除了简单请求以外的就是复杂请求。比如常见的几种

  • PUT、DELETE 的 ajax 请求
  • 发送 json 格式数据的 ajax 请求
  • 带自定义 header 的 ajax 请求

预检请求 HTTP OPTIONS

如果是复杂请求,浏览器会发出一个 HTTP OPTIONS 请求,也叫预检请求,用来判断这个复杂请求是否会发生跨域

现在来测试一下。新增一个接收 json 格式数据的 controller 方法

1
2
3
4
5
@PostMapping("postJson")
public ApiResult postJson(@RequestBody User user) {
log.info("接收到的json数据是: {}", user);
return new ApiResult("hello " + user.getName());
}

带 Cookie 的跨域