跨域问题总结

什么是跨域

为什么会跨域

跨域问题一直是前端的一大难题,从前端出道到至今,无论是自己还是身边的同事,以及网上前端朋友都被这个问题困扰着。

贴上标准的前端跨域报错:

Access to XMLHttpRequest at 'http://127.0.0.1:8080/cors_backend_war_exploded/test' from origin 'http://localhost:8081' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

说到跨域不得不谈的就是浏览器的同源策略,跨域也是因为浏览器这个机制引起的,这个机制的存在还是在于安全。

什么是源

Web内容的源由用于访问它的URL 的方案(协议),主机(域名)和端口定义。只有当方案,主机和端口都匹配时,两个对象具有相同的起源。

同源不同源一句话就可以判断:就是url中 scheme host port 都相同即为同源。

下面认识下url 结构中的这三个部分。

url结构

URL 代表着是统一资源定位符(Uniform Resource Locator)。URL 无非就是一个给定的独特资源在 Web 上的地址。

URL有如下结构组成:

  • Schme 或者 Protocol

  • Domain Name 也叫做host域名

  • port 端口号

  • Parameters参数

  • Anchor 锚点,一般用于定位位置

浏览器为什么需要同源策略

同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

前端解决方案

jsonp解决

我们发现Web页面上调用js文件时则不受是否跨域的影响,拥有”src”这个属性的标签都却拥有跨域的能力,比如<\a><\script><\img><\iframe>。那么跨域访问数据就有了一种可能,那就是在远程服务器上设法把数据装进js格式的文件里,供客户端调用和进一步处理。就好比使用一个<script>,让其src属性指向我们要访问的跨域资源,然后以接收js文件的形式接收数据。

JSONP的工作原理是通过动态向页面添加一个脚本标记,该标记的src属性指向远程JSONP API的URL。服务器返回的JSONP脚本被包装在一个函数调用中,当脚本下载完成后,该函数将自动执行。这就是为什么我们需要告诉JSONP要将脚本包装在哪个回调函数中的原因。这个函数在脚本下载后将被调用。JSONP的安全性问题主要在于脚本可能控制页面并对用户造成风险。因此,JSONP并不安全,只是没有被Web浏览器阻止而已。使用JSONP需要谨慎。需要注意的是jsonp只能用于get请求。

前端代码

ajax中设置参数dataType:"jsonp",表示这个请求是个jsonp请求。设置jsonpCallback:"callback",可以设置我们自定义返回调用的函数名。请求结束后,调用的函数其实就是我们的success,后端传入的参数也可以接受。

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
<%@page contentType="text/html;charset=UTF-8" %>
<html>
<head>
<script src="js/jquery.min.js"></script>
<script>
let f1 = () => {
console.log("12123");
$.ajax({
url:"http://127.0.0.1:8080/cors_backend_war_exploded/test",
type:"get",
dataType:"jsonp",
// jsonp:"callback",
jsonpCallback:"callback",
success(res){
console.log(res);
}
}
);
};
</script>
</head>
<body>
<h2>Hello World!</h2>
</body>
<a href="http://127.0.0.1:8080/cors_backend_war_exploded/test">跨域a标签请求</a>
<button id="cors-button"onclick="f1()">跨域ajax请求</button>
</html>

后端代码

在后端只需要注意把返回数据设置js文件,并且返回的是一个函数调用,函数名需要和前端设置的名称一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.zgy.cors.controller;


import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Controller
public class MyController {

@RequestMapping("/test")
public void testCors(HttpServletResponse response) throws IOException {
response.setContentType("text/javaScript;charset=UTF-8");
response.getWriter().print("callback(123222)");
}
}

后端解决方案

CORS的实现方式主要是通过HTTP头部来实现的,浏览器会在请求中添加一些自定义的HTTP头部,告诉服务器请求的来源、目标地址等信息。服务器在接收到请求后,会根据请求头中的信息来判断是否允许跨域请求,并在响应头中添加一些自定义的HTTP头部,告诉浏览器是否允许请求、允许哪些HTTP方法、允许哪些HTTP头部等信息。

在响应头中添加以下字段,可以解决跨域问题:

  • access-control-allow-origin : 该字段是必须的。它的值要么是请求时 Origin字段的值,要么是一个 *,表示接受任意域名的请求。
  • access-control-allow-credentials : 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为 true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为 true,如果服务器不要浏览器发送Cookie,删除该字段即可
  • Access-Control-Allow-Methods : 该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。

其实最重要的就是 access-control-allow-origin 字段,添加一个 * ,允许所有的域都能访问

@CrossOrigin注解

该注解可以作用在类以及方法层面上,当作用在类(Controller)层面上时,该类下的所有路径均允许跨域访问;当作用在方法层面上时,只有当前方法对应的路径允许跨域访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.zgy.cors.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Controller
@CrossOrigin
public class MyController {

// @ResponseBody
@RequestMapping("/test")
public void testCors(HttpServletResponse response) throws IOException {
// response.setContentType("text/javaScript;charset=UTF-8");
// response.getWriter().print("callback(123222)");
System.out.println("123123");
}
}

配置过滤器

使用过滤器我们可以对请求和响应头进行设置,做到跨域。

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
34
35
36
37
38
package com.zgy.cors.filter;

import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
String origin = request.getHeader("Origin");
if(origin!=null) {
response.setHeader("Access-Control-Allow-Origin", origin);
}
String headers = request.getHeader("Access-Control-Request-Headers");
if(headers!=null) {
response.setHeader("Access-Control-Allow-Headers", headers);
response.setHeader("Access-Control-Expose-Headers", headers);
}
response.setHeader("Access-Control-Allow-Methods", "*");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Credentials", "true");
chain.doFilter(request, response);
}

@Override
public void destroy() {
}
}

web.xml

1
2
3
4
5
6
7
8
<filter>
<filter-name>myFilter</filter-name>
<filter-class>com.zgy.cors.filter.MyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>myFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

配置WebMvcConfigurer

这个方法在非springboot环境下需要加@EnableWebMvc注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 允许跨域的路径
registry.addMapping("/**")
//允许的域名
.allowedOriginPatterns("*")
//允许cookie
.allowCredentials(true)
//允许请求方式
.allowedMethods("POST","PUT","DELETE","GET")
//允许的headers属性
.allowedHeaders("*")
//设置允许的时间
.maxAge(3600);
}
}

需要注意,如果配置了SpringSecurity,需要在SpringSecurity的配置类里面加入http.cors(),表示我们SpringSecurity允许跨域请求。