qrcode

马上订阅,开启修仙之旅

Spring Boot 2.x (三): 跨域处理方案之 Cors

springboot2-cors-cover

一、什么是跨域

1.1 URI 文法

URI 文法由 URI 协议名(例如 “http”,“ftp”,“mailto” 或 “file”),一个冒号,和协议对应的内容所构成。特定的协议定义了协议内容的语法和语义,而所有的协议都必须遵循一定的 URI 文法通用规则,亦即为某些专门目的保留部分特殊字符。

下面展示了 URI 例子及它们的组成部分:

1
2
3
4
5
                     权限                 路径
┌───────────────┴───────────────┐┌───┴────┐
abc://username:password@example.com:123/path/data?key=value&key2=value2#fragid1
└┬┘ └───────┬───────┘ └────┬────┘ └┬┘ └─────────┬─────────┘ └──┬──┘
协议 用户信息 主机名 端口 查询参数 片段

URL 是一种 URI,它标识一个互联网资源,并指定对其进行操作或获取该资源的方法。

1.2 浏览器的同源策略

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。如果两个页面的协议,端口(如果有指定)和主机名都相同,则两个页面具有相同的源。只要协议,主机名,端口这三项组成部分中有一项不同,就可以认为是不同的域,不同的域之间互相访问资源,就被称之为跨域。

下表给出了相对 http://store.company.com/dir/page.html 同源检测的示例:

URL结果原因
http://store.company.com/dir2/other.html成功只有路径不同
http://store.company.com/dir/inner/another.html成功只有路径不同
https://store.company.com/secure.html失败不同协议 ( https和http )
http://store.company.com:81/dir/etc.html失败不同端口 ( http:// 80 是默认端口)
http://news.company.com/dir/other.html失败不同域名 ( news 和 store )

同源策略会限制以下几种行为:

  • Cookie、LocalStorage 和 IndexDB 无法读取;
  • DOM 和 JS 对象无法获得;
  • AJAX 请求不能发送,被浏览器拦截了。

在前后端分离的项目中,因为前端站点和后台站点一般是分开部署的,所以在实际开发过程中也会出现跨域问题。当然遇到问题最终还是要解决的,下面我们来看一下跨域问题的一些解决方案。

二、如何解决跨域

2.1 跨域解决方案

针对同源策略限制而引起的跨域问题,有以下 9 种解决方案:

  1. JSONP 跨域
  2. 跨域资源共享(CORS)
  3. Nginx 反向代理
  4. Node.js 中间件代理
  5. document.domain + iframe
  6. location.hash + iframe 跨域
  7. window.name + iframe 跨域
  8. postMessage 跨域
  9. WebSocket 协议跨域

接下来我们将着重介绍 CORS 解决方案,因为它是解决 AJAX 请求跨域问题的一剂“良药”,对其它方案感兴趣的同学请自行查阅相关资料。

2.2 CORS 简介

跨域资源共享(CORS)是一种机制,它使用额外的 HTTP 头来告诉浏览器让运行在一个域上的 Web 应用被允许访问来自不同源服务器上的指定的资源。CORS 需要浏览器和服务器同时支持。目前,所有主流的浏览器都支持该功能。整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。

对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求(预检请求),但用户不会有感觉。实现 CORS 通信的关键是后端,只要后端根据实际情况设置相应的响应头信息,就能解决 AJAX 请求跨域问题。

前面我们已经介绍跨域的概念和跨域问题的一些解决方案,现在我们进入本文的正题 —— Spring Boot CORS 跨域处理。

三、Spring Boot 环境搭建

本项目所使用的开发环境及主要框架版本:

  • java version “1.8.0_144”

  • spring boot 2.2.0.RELEASE

首先新建一个 Spring Boot 项目,然后在根目录下的 pom.xml 文件中引入以下依赖:

1
2
3
4
5
6
7
8
9
10
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

接着新建一个 index.html 页面并输入以下代码:

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
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Semlinker's Springboot2 Cors</title>
</head>
<body>
<h3>Semlinker's Springboot2 Cors</h3>
<div>
用户列表:
<p id="users"></p>
</div>
<script>
(function () {
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
document.querySelector("#users").innerHTML = xmlhttp.responseText;
}
};
xmlhttp.open("GET", "http://localhost:8081/users");
xmlhttp.send();
})();
</script>
</body>
</html>

以上代码比较简单,就是创建 XMLHttpRequest 对象,然后往 http://localhost:8081/users 地址发送 GET 请求。下面我们来创建一个 HomeController,用于处理 //users 请求,该控制器的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class HomeController {
private String[] users = {"Semlinker", "Lolo", "Kakuqo"};

@GetMapping("/users")
@ResponseBody
public String[] users() {
return users;
}

@GetMapping("/")
public String index() {
return "index";
}
}

万事俱备只欠东风,最后我们还需要配置一下 Idea 的 Run/Debug Configurations,下图的重点是通过 Program arguments 参数配置 CorsApp-8080 应用程序的端口,即 --server.port=8080。同理,我们通过设置不同的应用程序端口,就可以启动另一个新的应用程序,即 Cors-8081应用。

idea-cors-run-configurations

在配置完成后,分别启动 CorsApp-8080 和 CorsApp-8081 两个应用程序,待两个应用启动完成后,访问 http://localhost:8080/ 地址,此时你会发现页面上并没有显示任何用户。而访问 http://localhost:8081/ 地址时,你确可以看到以下内容:

1
2
3
Semlinker's Springboot2 Cors
用户列表:
["Semlinker","Lolo","Kakuqo"]

接着我们再次访问 http://localhost:8080/ 地址,然后打开控制台,这时你会看到以下错误信息:

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

http://localhost:8080' has been blocked by CORS policy 这行消息告诉了我们具体原因,很明显是由于端口不同(8080 -> 8081)违反了同源策略,浏览器出于安全考虑限制了跨域请求。现在我们也遇到跨域问题,下面我们就来学习一下在 Spring Boot 中如何利用 Cors 来解决上述的 AJAX 请求跨域问题。

四、Spring Boot Cors 跨域解决方案

4.1 CrossOrigin 注解

在 Spring Boot 中为我们提供了一个注解 @CrossOrigin 来实现跨域,这个注解可以实现方法级别的细粒度的跨域控制。

1
2
3
4
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {}

由上可知我们可以在类或者方法上使用该注解,如果在类上添加该注解,该类下的所有接口都允许跨域访问,如果在方法上添加注解,那么仅限于添加注解的方法可以访问。在该注解中包含以下属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {
@AliasFor("origins")
String[] value() default {};

@AliasFor("value")
String[] origins() default {};

String[] allowedHeaders() default {};

String[] exposedHeaders() default {};

RequestMethod[] methods() default {};

String allowCredentials() default "";

long maxAge() default -1;
}

CrossOrigin 注解每个属性的详细含义如下所示:

属性含义
value指定所支持域的集合, 表示所有域都支持,默认值为 。这些值对应于 HTTP 请求头中的 Access-Control-Allow-Origin
origins@AliasFor(“value”),与 value 属性一样
allowedHeaders允许请求头中的 headers,在预检请求 Access-Control-Allow-Headers 响应头中展示
exposedHeaders响应头中允许访问的 headers,在实际请求的 Access-Control-Expose-Headers 响应头中展示
methods支持的 HTTP 请求方法列表,默认和 Controller 中的方法上标注的一致。
allowCredentials表示浏览器在跨域请求中是否携带凭证,比如 cookies。在预检请求的 Access-Control-Allow-Credentials 响应头中展示
maxAge预检请求响应的最大缓存时间,单位为秒。在预检请求的 Access-Control-Max-Age 响应头中展示

介绍完 @CrossOrigin 注解的相关知识,我们来修改一下 HomeController 控制器,在 users 方法上添加 @CrossOrigin 注解:

1
2
3
4
5
6
7
8
9
10
11
@Controller
public class HomeController {
private String[] users = {"Semlinker", "Lolo", "Kakuqo"};

@GetMapping("/users")
@ResponseBody
@CrossOrigin
public String[] users() {
return users;
}
}

完成修改之后,重新启动一下项目,然后继续访问 http://localhost:8080/ 地址,如果一切顺利的话,在页面就可以看到期望的内容:

1
2
3
Semlinker's Springboot2 Cors
用户列表:
["Semlinker","Lolo","Kakuqo"]

现在通过浏览器的开发者工具,查看 http://localhost:8081/users 的 HTTP 请求报文:

cross-origin-demo

从图中可知,当 users 方法添加了 @CrossOrigin 注解之后,响应头返回了 Access-Control-Allow-Origin:*

信息。

4.2 实现 WebMvcConfigurer 接口

除了使用 @CrossOrigin 注解外,我们还可以通过实现 WebMvcConfigurer 接口来实现统一的跨域配置。首先在当前项目中新建一个 config 包,接着创建一个 CorsConfiguration 配置类,该类需要实现 WebMvcConfigurer 接口,然后覆写 addCorsMappings 方法,最后利用 CorsRegistry 对象进行跨域配置,具体实现如下所示:

1
2
3
4
5
6
7
8
9
10
// com/semlinker/config/CorsConfig.java
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET");
}
}

配置完成之后,为了排除 @CrossOrigin 注解的影响,我们需要先移除 HomeController 类的 users 方法上的 @CrossOrigin 注解,重启项目再次访问 http://localhost:8080/ 地址,发现效果一样。

4.3 过滤器

过滤器是向 Web 应用程序的请求和响应,添加相关功能的 Web 服务组件。过滤器会拦截用户发送至 Web 资源服务器的请求,处理后将请求信息传递给 Web 资源服务器。Web 资源服务器的响应也会经过过滤器处理后,再返回给用户。因此我们就可以利用过滤器的特性来统一添加跨域响应头。

这里我们可以直接利用 org.springframework.web.filter 包下的 CorsFilter 过滤器而不用自己实现 Cors 过滤器,有了过滤器后,还需要对它进行注册,注册方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// com/semlinker/config/CorsConfig.java
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
}

项目地址:https://github.com/semlinker/springstack/tree/master/springboot2-cors

五、参考资源


欢迎小伙伴们订阅前端全栈修仙之路,及时阅读 Angular、TypeScript、Node.js/Java和Spring技术栈最新文章。

qrcode