目录
如在浏览器控制台看到类似于下边的报错,则是出现了跨域请求问题
xxx has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the request resouce
在和前端打交道的时候,经常听到过跨域这个词,安全部门也用XSS这样看起来高大上的名词;
在后端开发中,并没有跨域这么麻烦的东西。所谓的跨域,是对于浏览器来说的,它限制了各种条条框框,以便能够安全的驯服javaScript这只野兽。
所以来吧,看看浏览器为了达到自己的目的,都干了些啥。
1 跨域和同源
所谓的同源,指的是两个URL的
协议(protocal)、域名(host)、端口(port)
都相同的情况,下面给出示例。
对于http://csdn.com/index.html来说,下面展示了4种不同的情况。
【1】https://csdn.com/index.html —- 协议不同(http与https),所以不同源。
【2】http://csdn.com:8080/index.html —- 端口不同(80与8080),所以不同源。
【3】http://baidu.com/index.html —- 域名/主机 不同,所以不同源。
【4】http://csdn.com/home.html —- 同源。
那么在不同源(跨域)的情况下,js的执行都有哪些限制呢?
【1】存储资源不共享,如Cookie、LocalStorage 和 IndexDB(浏览器数据库)等,都不能相互读取。
【2】跨域的情况下,DOM和Javascript对象都无法获取。
【3】更要命的是,Ajax请求无法发送,限制了前端程序员的发挥。
但随着业务的增长和域名的增加,跨域的需求是越来越多,浏览器的默认行为,成为了这个功能的拦路虎。所以我们需要寻找有效的方法,来突破这条加载自己脖子上的锁套。
下图是一个页面请求多个域名服务器获取资源的情况
2. CORS 跨域资源共享(解决跨域)
2.1 前端解决(不推荐)
前端工程师,发明了各种各样的请求方法。常见的有:
【1】
jsonp
使用javascript的代理模式,动态的创建script标签。比如常见的百度统计代码,虽然不同源,但是你仍然能把信息发送过去。jsonp只能支持GET请求,不支持POST请求。
【2】
document.domain + iframe
这个是利用iframe加载主域名相同的资源。
【3】
location.hash + iframe
依然是利用iframe等,使用的是全局对象,用起来很绕。
【4】
postMessage
Html5的新功能,专门用来解决跨域。我们只要发送端拥有某个窗口的有效js的句柄,就可以通过这套机制向该窗口发送任意长度的文本信息。但编程的时候,容易忘掉origin的判断,造成安全问题。
这些方法都需要写很多代码,还容易出错,调试起来也麻烦,所以现在用的最多的,是
CORS
。
这项技术是W3C的标准,前端代码几乎不需要做任何改动,浏览器可以自动完成。听起来非常的魔幻,但它其实是在HTTP协议上做文章的,要在Http的头里面,加入一些附加信息。只要服务器支持,就实现了跨域操作。所以通信的关键就有前端转移到了服务器的配置上。 目前,几乎所有的浏览器都支持。
2.2 Nginx 解决跨域
对Nginx来说,要解决跨域,就得加一些配置。
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Credentials' true;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
#
# Custom headers and headers various browsers *should* be OK with but aren't
#
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
#
# Tell client that this pre-flight info is valid for 20 days
#
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Credentials' true;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Credentials' true;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}
}
因为跨域,针对于正常的浏览器限制来说,相当于开了一条特许通道,所以它的配置非常的细腻。
Access-Control-Allow-Origin
(最重要),一般设置成 * 一了百了,但它可以指定具体的请求来源,也更加安全。所以,在dev环境调试时,为了方便开发,可以设置成 *,而线上最好设置成具体的domain。
Access-Control-Request-Method
,指定了跨域请求所允许的HTTP方法,我们这里是GET、POST、OPTIONS 等。
Access-Control-Allow-Headers
,表明服务器支持的所有头信息字段,用在预检请求中。值得注意的是,一些简单的头部信息,比如Content-Language、Content-Type等,不需要特别声明。如果你想偷懒,可以设置为 * 。
http的交互,是如何执行的呢?
假定当前浏览器访问的网址是http://csdn.com,当发起了一个指向http://baidu.com 的Ajax请求。正常情况下,这是不能通过的。于是浏览器在请求头中,自动添加了一行。
Origin: http://csdn.com
http://baidu.com 的服务器(nginx)进行了如下跨域配置,看到这个请求,一对比,可以啊兄弟,我允许你访问。
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
那请求就可以正常进行下去,否则会触发XHR的error。
上面介绍的,是简单请求的过程,如下图所示。(请求类型分为简单请求和复杂请求,建议访问https://www.test-cors.org/进行实际的测试来观测)
对于复杂请求来说,多了一步使用OPTIONS的预检操作,流程差不多。
2.3 tomcat 解决跨域
对于tomcat来说,配置一个filter就可以了。
<filter>
<filter-name>CorsFilter</filter-name>
<filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CorsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
2.4 SpringBoot 服务解决跨域
方式1:Controller类或方法上添加@CrossOrigin注解
方法上标注@CrossOrigin只针对当前方法支持跨域
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
类上标注@CrossOrigin 当前类中所有方法都支持跨域
@CrossOrigin(origins = "http://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
方式2:全局CORS配置
除了上述细粒度、基于注解的配置之外,你还可能需要定义一些全局CORS配置。这类似于使用筛选器,可以结合@CrossOrigin配置。
全局方式1: 采用过滤器(filter)
/**
* 跨域请求配置
*/
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 允许的请求头
corsConfiguration.addAllowedHeader("*");
// 允许的请求源 (如:http://localhost:8080)
corsConfiguration.addAllowedOrigin("*");
// 允许的请求方法 ==> GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
corsConfiguration.addAllowedMethod("*");
// URL 映射 (如: /admin/**)
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return new CorsWebFilter(urlBasedCorsConfigurationSource);
}
}
全局方式2: 继承WebMvcConfigurerAdapter或者实现WebMvcConfigurer接口
/**
* AJAX请求跨域
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
static final String ORIGINS[] = new String[]{"GET", "POST", "PUT", "DELETE"};
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods(ORIGINS)
.maxAge(3600);
}
}
注意:
- 微服务环境下,建议在gateway中添加配置类,解决整个系统的跨域问题。
- 如果在gateway中处理了跨域问题,不要再在各个微服务中处理跨域,否则会导致跨域请求失败。
3 总结
跨域问题,在前后分离的架构下,几乎100%都会遇到。跨域访问的限制,是浏览器做的文章,我们可以使用CORS来绕过去。既然是绕,那就不要一股脑的全部设置成 * ,虽然这样搞非常的让人省心。
有时候你确实会遇到连CORS都处理不了的跨域问题。在这种情况下,最好要求你的客户,升级一下支持的浏览器试试。毕竟有些特立独行的浏览器,是非常IE的。