专栏首页全栈修仙之路Spring Boot 2.x (三): 跨域处理方案之 Cor

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

一、什么是跨域

1.1 URI 文法

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

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

                     权限                 路径
      ┌───────────────┴───────────────┐┌───┴────┐
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 文件中引入以下依赖:

<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 页面并输入以下代码:

<!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 请求,该控制器的定义如下:

@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应用。

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

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

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

(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 来实现跨域,这个注解可以实现方法级别的细粒度的跨域控制。

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

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

@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 注解:

@Controller
public class HomeController {
    private String[] users = {"Semlinker", "Lolo", "Kakuqo"};

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

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

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

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

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

信息。

4.2 实现 WebMvcConfigurer 接口

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

// 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 过滤器,有了过滤器后,还需要对它进行注册,注册方式如下:

// 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

五、参考资源

  • Wikipedia - 统一资源标志符
  • MDN - Same-origin_policy
  • MrBird - Spring Boot 中处理跨域

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Angular 6 HttpClient 快速入门

    本教程将介绍如何在 Angular 6.x 中使用 HttpClient 发送 Http 请求,如 get、post、put 和 delete 请求。在 Ang...

    阿宝哥
  • Angular ViewChild和ViewChildren

    Angular 为我们提供 ViewChild 和 ViewChildren 装饰器来获取模板视图中匹配的元素。ViewChild 是属性装饰器,用来从模板视图...

    阿宝哥
  • Sequelize 快速入门

    Sequelize 是一个基于 Promise 的 Node.js ORM,目前支持 Postgres、MySQL、SQLite 和 Microsoft SQL...

    阿宝哥
  • 同源策略与CORS跨域

    PS:这篇文章是紧接着JSONP原理和Ajax学习与理解写的,有些内容是承接了上两篇文章. 这篇文章只算是我的个人学习笔记,内容没有经过精心排版,也没有认真校对...

    代码之风
  • 跨域问题

    测试URL为 http://localhost:80/home/allProductions 可以直接在浏览器console中执行

    微醺
  • 九种跨域方式实现原理(完整版)

    前后端数据交互经常会碰到请求跨域,什么是跨域,以及有哪几种跨域方式,这是本文要探讨的内容。

    Fundebug
  • 跨域

    前后端数据交互经常会碰到请求跨域,什么是跨域,以及有哪几种跨域方式,这是本文要探讨的内容。

    grain先森
  • 九种跨域方式实现原理(完整版)

    前后端数据交互经常会碰到请求跨域,什么是跨域,以及有哪几种跨域方式,这是本文要探讨的内容。

    江南一点雨
  • 初识nginx基础篇-日志管理和切割

    Nginx日志主要分为两种,访问日志和错误日志。两种日志可以在http和server模块中配置,nginx有一个非常灵活的日志记录模式。每个级别的...

    后端技术探索
  • 学会Spring Mvc 跨域你只需要看完这一篇

    现在普遍前后端分离,前端 http://ip:port/context 后端http://ip:anotherport/anothercontext ,然后你发...

    码农小胖哥

扫码关注云+社区

领取腾讯云代金券