Spring Boot2 跨域问题

Posted 福州-司马懿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Boot2 跨域问题相关的知识,希望对你有一定的参考价值。

跨域的定义

浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议,任何一者不同,都会被认为是跨域。

为什么会跨域

现在大部分开发都是前后端分离的。在前后端分离的模式下,前后端的域名(IP+端口)是不一致的,此时就会发生跨域问题。在请求的过程中,使用 get/post 去服务端取数据时,就会报错

跨域问题源于 javascript 的同源策略,即只有 协议+主机名+端口号(如果没有,默认80)相同,才允许相互访问。也就是说 JavaScript 只能访问和操作自己域下的资源,不能访问和操作其他域下的资源。跨域问题时针对 JavaScript 和 ajax 的,html本身没有跨域问题,比如 a标签、script标签、甚至form标签,可以直接跨域发送数据并接收数据。

禁止跨域的目的

跨域问题是浏览器对于ajax请求的一种安全限制:一个页面发起的ajax请求,只能是于当前页同域名的路径,这能有效的阻止跨站攻击。

跨域实例

举个例子,这是一个前后端分离的项目。后端用Spring Boot来写,端口号8080;前端使用Vue或React,然后部署到nginx的html目录下(端口号默认80)。这里虽然IP相同,都是本地IP,但端口号不同,因此也会被判定为跨域。

解决方案

禁止跨域虽然能阻止跨站攻击,但却给开发带来了不便。因为实际生产环境中,肯定会有很多台服务器进行交互,地址和端口都可能不同。

目前,常用的解决方案有3种:

  • Jsonp
    最早的解决方案,利用script标签可以跨域的原理实现的。缺点是需要服务端的支持,并且只能发起GET请求
  • nginx反向代理
    将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据后,再返回给客户端。此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器的地址,而隐藏了真实服务器的IP地址。优点是可以针对各种请求。缺点是需要在nginx进行额外配置,比较麻烦
  • CORS
    目前最常用,最简单的跨域请求方案。优点是可以在服务端控制是否允许跨域,支持各种请求,且可以自定义规则

CROS概念

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。
它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能(IE浏览器不能低于IE10)

Ajax 请求原理

浏览器会将 ajax 请求分为两类:简单请求、特殊请求

简单请求

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

  1. 请求方法是以下三种方法之一:
    • HEAD
    • GET
    • POST
  2. HTTP的头部信息不超过以下几种字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

当浏览器发现ajax的请求是简单请求时,会在请求头中携带一个Origin字段。该字段指明了请求属于哪个域(协议+域名+端口)。服务器会根据这个值决定是否允许其跨域

如果服务器允许跨域,需要在返回的响应头中携带下面的信息

Access-Control-Allow-Origin: http://www.example.com
Access-Control-Allow-Credentials: true
Content-Type: text/html; charset=utf-8

如果跨域请求想要操作cookie,需要满足3个条件:

  • 服务器的响应头中需要携带Access-Control-Allow-Credentials,并且值为true
  • 浏览器发起ajax需要指定withCredentials为true
  • 响应头中Access-Control-Allow-Origin一定不能为*,必须是指定的域名

特殊请求

不符合简单请求条件的,都会被浏览器判定为特殊请求
特殊请求在正式通信之前,会增加一次HTTP查询请求,称为“预检”请求(preflight)
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头部信息字段。只有得到肯定的答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错

一个“预检”请求的样板

OPTIONS /cors HTTP/1.1
Origin: http://www.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.example.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

与简单请求相比,除了Origin以外,多了2个头:

  • Access-Control-Request-Method: 接下来会用到的请求方式,比如PUT
  • Access-Control-Request-Headers:会额外用到的头信息

服务器收到预检请求,如果允许跨域,则会发出响应:

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://www.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 1728000
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

除了Access-Control-Allow-Origin和Access-Control-Allow-Credentials以外,这里又额外多出了3个头:

  • Access-Control-Allow-Methods: 允许访问的方式
  • Access-Control-Allow-Headers:允许携带的头
  • Access-Control-Max-Age: 本次许可的有效时长,单位秒。过期之前,ajax请求就无需再进行“预检”了

CROS实现

@Configuration
public class GlobalCorsConfig {
	@Bean
	public FilterRegistrationBean<CorsFilter> corsFilter() {
		FilterRegistrationBean<CorsFilter> corsFilterFilterRegistrationBean = new FilterRegistrationBean<>();
		//添加CORS配置信息
		CorsConfiguration corsConfiguration = new CorsConfiguration();
		//允许的域,不要写*,否则cookie就无法使用了
		corsConfiguration.addAllowedOrigin("*");
		//允许的头信息
		corsConfiguration.addAllowedHeader("*");
		//允许的请求方式
		corsConfiguration.setAllowedMethods(Arrays.asList("POST", "PUT", "GET", "OPTIONS", "DELETE"));
		//是否发送cookie信息
		corsConfiguration.setAllowCredentials(true);
		//预检请求的有效期,单位为秒
		corsConfiguration.setMaxAge(3600L);

		//添加映射路径,标识待拦截的请求
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		source.registerCorsConfiguration("/**", corsConfiguration);

		corsFilterFilterRegistrationBean.setFilter(new CorsFilter(source));
		corsFilterFilterRegistrationBean.setOrder(-1);
		return corsFilterFilterRegistrationBean;
	}
}

配置项解释

预检请求会向服务器确认跨域是否允许,服务返回的响应头里有对应字段 Access-Control-Allow-Origin 来给浏览器判断:如果允许,浏览器紧接着发送实际请求;不允许,报错并禁止客户端脚本读取响应相关的任何东西

字段是否必填含义
Access-Control-Allow-OriginY它的值要么是请求时Origin字段的具体值,要么是一个*,表示接受任意域名请求
Access-Control-Allow-MethodsY它的值是逗号分割的一个具体的字符串或*,表明服务器支持的所有跨域请求的方法。这是为了避免多次“预检”
Access-Control-Expose-HeadersNCORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma。如果想拿到其他字段,则必须在Access-Control-Expose-Headers里面指定
Access-Control-Allow-CredentialsN布尔值,表示是否允许发送Cookie。默认情况下,不发生Cookie,即false。对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json,这个值只能设置为true。如果服务器不要浏览器发送Cookie,删除该字段即可
Access-Control-Max-AgeN用来指定本次预检请求的有效期,单位为秒。在有效期间,不用发出另一条预检请求

CORS请求发送Cookie时,Access-Control-Allow-Origin只能是与请求网页一致的域名。同时,Cookie依然遵守同源策略,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie无法读取服务器域名下的Cookie

至于缓存(大小写不敏感)

  • HTTP/1.0的时候Pragma、Expires两个缓存的响应头
  • HTTP/1.1的时候添加一个新的响应头Cache-control

不要缓存

response.setDataHeader("Expires", -1);
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");

使用缓存

response.setDataHeader("Expires", System.currentTimeMillis() + 24*60*60*1000); //单位毫秒,优先级低
response.setHeader("Cache-Control", "max-age=24*60*60"); //单位秒,优先级更高

补充

网上有很多使用过滤器和拦截器的代码来实现跨域,经测试均无效

Spring-Boot 2.1.5.RELEASE + Edge 93.0.961.47 / Chrome 93.0.4577.63

过滤器

@Component
public class CorsFilter implements Filter {

	@Override
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
		System.out.println("doFilter " + req.getLocalAddr() + " => " + req.getRemoteAddr());
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		if(request.getHeader("Origin") != null) {
			response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
		} else {
			response.setHeader("Access-Control-Allow-Origin", "*");
		}
		response.setHeader("Access-Control-Allow-Credentials", "true");
		response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
		response.setHeader("Access-Control-Max-Age", "3600");
		response.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-Width, remember-me");
		chain.doFilter(request, response);
	}
}

拦截器

@Configuration
public class MyConfiguration implements WebMvcConfigurer {

	@Override
	public void addCorsMappings(CorsRegistry registry) {
		registry.addMapping("/**")
			.allowedOrigins("*")
			.allowCredentials(true)
			.allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS")
			.maxAge(3600);
	}
}

以上是关于Spring Boot2 跨域问题的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot2.1.3全局跨域

Spring Boot2.XX版本解决跨域问题

Spring Boot2.XX版本解决跨域问题

spring boot2X代码混淆

Spring Boot2 系列教程 | 使用 LomBok 提高开发效率

Spring Boot2 系列教程Spring Boot 整合 Freemarker