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 请求分为两类:简单请求、特殊请求
简单请求
只要同时满足以下两个条件,就属于简单请求
- 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
- 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-Origin | Y | 它的值要么是请求时Origin字段的具体值,要么是一个*,表示接受任意域名请求 |
Access-Control-Allow-Methods | Y | 它的值是逗号分割的一个具体的字符串或*,表明服务器支持的所有跨域请求的方法。这是为了避免多次“预检” |
Access-Control-Expose-Headers | N | CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma。如果想拿到其他字段,则必须在Access-Control-Expose-Headers里面指定 |
Access-Control-Allow-Credentials | N | 布尔值,表示是否允许发送Cookie。默认情况下,不发生Cookie,即false。对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json,这个值只能设置为true。如果服务器不要浏览器发送Cookie,删除该字段即可 |
Access-Control-Max-Age | N | 用来指定本次预检请求的有效期,单位为秒。在有效期间,不用发出另一条预检请求 |
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 跨域问题的主要内容,如果未能解决你的问题,请参考以下文章