Skip to content

18. [课程]:跨源资源共享

关键词:CORS(跨源资源共享)。

本章的内容在某种程度上超出了本教程的范围。之所以将其纳入,是因为它介绍了 Web 编程和 JavaScript 编程。需要记住的是,本教程的目标之一是介绍 JEE 开发中经常使用的概念,即基于 Java 框架的 Web 开发。在此,我们将扩展产品和类别数据库研究中使用的 Web 服务器,使其能够接受跨域请求。

文档 [AngularJS / Spring 4 教程] 中,我们将开发一个客户端/服务器应用程序,其中客户端是一个 AngularJS 应用程序:

  • Angular 应用程序的 HTML/CSS/JS 页面来自服务器 [1];
  • 在 [2] 中,[dao] 服务向另一台服务器(服务器 [2])发起请求。然而,运行 Angular 应用程序的浏览器会禁止此操作,因为这存在安全漏洞。该应用程序只能向其来源服务器(即服务器 [1])发起查询;

实际上,说浏览器阻止 Angular 应用程序查询服务器 [2] 并不准确。它实际上是向服务器 [2] 发送请求,询问其是否允许非同源客户端进行查询。这种共享技术被称为 CORS(跨源资源共享)。服务器 [2] 通过发送特定的 HTTP 头部来授予权限。

我们将构建以下架构:

  • 在[1]中,一个Web应用程序提供HTML/JS页面;
  • 在[2]中,浏览器执行嵌入在HTML页面中的JavaScript,以查询安全Web服务[3];

18.1. 支持

  

本章的项目位于 [support / chap-18] 文件夹中。

18.2. 客户端项目

创建以下 Eclipse 项目:

  

18.3. Maven 配置

该项目是一个 Maven 项目,包含以下 [pom.xml] 文件:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.webjson</groupId>
    <artifactId>intro-server-webjson-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
 
    <name>intro-server-webjson-01</name>
    <description>démo spring mvc</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>
 
    <dependencies>
        <dependency>
            <groupId>istia.st.springdata</groupId>
            <artifactId>intro-spring-data-01</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
</project>
  • 第 11–15 行:这是一个 Spring Boot 项目;
  • 第 23–26 行:我们使用了 [spring-boot-starter-web] 依赖项,其中包含 Tomcat 服务器和 Spring MVC;

18.4. Spring 配置

  

用于配置 Spring 项目的 [WebConfig] 类如下:


package spring.cors.client.config;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
 
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
 
    // -------------------------------- layer configuration [web]
    @Autowired
    private ApplicationContext context;
 
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
        return servlet;
    }
 
    @Bean
    public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new ServletRegistrationBean(dispatcherServlet, "/*");
    }
 
    @Bean
    public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
        return new TomcatEmbeddedServletContainerFactory("", 8081);
    }
 
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/*.html").addResourceLocations("classpath:/static/");
        registry.addResourceHandler("/*.js").addResourceLocations("classpath:/static/js/");
    }
}
  • 第 15 行:该类配置了一个 Spring MVC 项目;
  • 第 16 行:该类继承了 [WebMvcConfigurerAdapter] 类,以重写其中的一些方法;
  • 第 18–36 行:我们之前已经遇到过这些 Bean,例如在第 13.5.3.1 节中。请注意,第 35 行中,Web 服务将在 8081 端口上运行;
  • 第 38–42 行:[addResourceHandlers] 方法允许您定义静态资源,即第 23 行中 [DispatcherServlet] 未处理的资源;
  • 第 40 行:任何针对后缀为 .html 的资源的请求,都将返回请求中指定的文件,该文件位于项目类路径的 [static] 文件夹中;
  • 第 41 行:对任何后缀为 .js 的资源的请求,将返回请求中指定的 JavaScript 文件,该文件位于项目类路径的 [static/js] 文件夹中;
  

18.5. jQuery 和 JavaScript 基础

客户端的 HTML 页面如下所示:

 

该页面将包含在浏览器中运行的 JavaScript(JS)代码。我们将介绍一些 JavaScript 基础知识,以便更好地理解这段代码。客户端将使用 jQuery 库 [https://jquery.com/] 发起 HTTP 请求,该库提供了许多简化 JavaScript 开发的函数。我们创建一个静态 HTML 文件 [jQuery.html] 并将其放置在 [static] 文件夹中:

 

该文件的内容如下:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>JQuery-01</title>
<script type="text/javascript" src="/jquery-2.1.3.min.js"></script>
</head>
<body>
    <h3>Rudiments de JQuery</h3>
    <div id="element1">Elément 1</div>
</body>
</html>
  • 第 6 行:导入 jQuery;
  • 第10–12行:一个ID为[element1]的页面元素。我们将对该元素进行操作。

我们需要下载文件 [jquery-2.1.3.min.js]。jQuery 的最新版本可在以下网址 [http://jquery.com/download/] 获取:

Image

将下载的文件放置在 [static/js] 文件夹中,并更新 HTML 文件的第 6 行以匹配已安装的版本。

完成上述操作后,在 Chrome 中打开静态视图 [jQuery.html] [1-2]:

在 Google Chrome 中,按 [Ctrl-Shift-I] 打开开发者工具 [3]。[控制台] 选项卡 [4] 允许您运行 JavaScript 代码。下面,我们将提供需要输入的 JavaScript 命令并解释其作用。

JS
结果
$("#element1")
: 返回所有 ID 为 [element1] 的元素集合,
因此通常包含 0 或 1 个元素
因为一个 HTML 页面上不能存在两个相同的 ID
$("#element1").text("blabla")
: 为集合中的所有元素
。这会更改
页面显示的内容
$("#element1").hide()
隐藏集合中的元素。
文本 [blabla] 不再显示。
$("#element1")
: 再次显示集合。这
让我们看到 ID 为 [element1] 的元素具有
具有 CSS 属性 style='display: none;',这
导致该元素被隐藏。
$("#element1").show()
:显示集合中的元素。文本
[blabla] 再次出现。正是
style='display: block;' 确保了此
显示效果。
$("#element1").attr('style','color: red')
:为集合中的所有元素设置一个属性
集合中的所有元素上设置一个属性。此处的属性是 [style],其值为
[color: red]。文本 [blabla] 将变为红色。
数组
字典

请注意,在所有这些操作过程中,浏览器的 URL 都没有发生变化。没有与 Web 服务器进行通信。所有操作都在浏览器内部完成。现在,让我们查看该页面的源代码:

这是初始文本。它并未反映我们在第 10 至 12 行对该元素所做的修改。在调试 JavaScript 时,这一点非常重要。因此,通常没有必要查看显示页面的源代码。

18.6. 应用程序的 JavaScript 代码

让我们回到将查询 Web 服务 / jSON 的客户端应用程序页面:

  
 

本页面的 HTML 代码如下:


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Spring MVC</title>
<script type="text/javascript" src="/jquery-2.1.3.min.js"></script>
<script type="text/javascript" src="/client.js"></script>
</head>
<body>
    <h2>Client du service web / jSON</h2>
    <form id="formulaire">
        <!--  identifier -->
        Identifiant :
        <!--  -->
        <input type="text" id="identifiant" name="identifiant" value="" />
        <!--  password -->
        <br /> <br /> Mot de passe :
        <!--  -->
        <input type="text" id="password" name="password" value="" />
        <!--  method HTTP -->
        <br /> <br /> Méthode HTTP :
        <!--  -->
        <input type="radio" id="get" name="method" value="get"
            checked="checked" />GET
        <!--  -->
        <input type="radio" id="post" name="method" value="post" />POST
        <!--  URL -->
        <br /> <br />URL cible (commençant par /): <input type="text"
            id="url" size="30"><br />
        <!-- posted value -->
        <br /> Chaîne jSON à poster : <input type="text" id="posted"
            size="50" />
        <!-- validation button -->
        <br /> <br /> <input type="button" value="Valider"
            onclick="javascript:requestServer()"></input>
    </form>
    <hr />
    <h2>Réponse du serveur</h2>
    <div id="response"></div>
</body>
</html>
  • 第 6 行:我们导入 jQuery 库;
  • 第 7 行:我们导入即将编写的代码;
  • 第 15、19、26、29、31 行:请注意页面组件的 [id] 标识符。JavaScript 通过这些标识符引用这些组件;

[client.js] 的代码如下:


// global data
var url;
var posted;
var response;
var method;
var baseUrl = 'http://localhost:8080';
var identifiant;
var password;
var authorizationHeader;
 
function requestServer() {
    // information retrieval
    var urlValue = url.val();
    var postedValue = posted.val();
    var identifiantValue = identifiant.val();
    var passwordValue = password.val();
    var method = document.forms[0].elements['method'].value;
    authorizationCode = btoa(identifiantValue + ':' + passwordValue);
    // delete the previous answer
    response.text("");
    // make a manual Ajax call
    if (method === "get") {
        doGet(urlValue);
    } else {
        doPost(urlValue, postedValue);
    }
}
 
function doGet(url) {
    // make a manual Ajax call
    $.ajax({
        headers : {
            'Authorization':'Basic '+authorizationCode
        },
        url : baseUrl + url,
        type : 'GET',
        dataType : 'text',
        beforeSend : function() {
        },
        success : function(data) {
            // text result
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // system error
            response.text(JSON.stringify(jqXHR.statusCode()));
        }
    })
}
 
function doPost(url, posted) {
    // make a manual Ajax call
    $.ajax({
        headers : {
            'Authorization':'Basic '+authorizationCode
        },
        url : baseUrl + url,
        type : 'POST',
        contentType : 'application/json; charset=UTF-8',
        data : posted,
        dataType : 'text',
        beforeSend : function() {
        },
        success : function(data) {
            // text result
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // system error
            response.text(JSON.stringify(jqXHR.statusCode()));
        }
    })
}
 
// document loading
$(document).ready(function() {
    // retrieve page component references
    identifiant = $("#identifiant");
    password = $("#password");
    url = $("#url");
    posted = $("#posted");
    response = $("#response");
});
  • 第 80–87 行:文档在浏览器中加载完成后执行的 JavaScript 代码;
  • 第 81–86 行:通过 [id] 标识符获取 HTML 文档中各个元素的引用;
  • 第 2–9 行:在 JavaScript 文件中定义的所有函数中均可访问的全局变量;
  • 第 13 行:获取用户输入的 URL;
  • 第 14 行:获取用户要提交的值(如果是 GET 操作则为空);
  • 第 15 行:获取用户输入的用户名;
  • 第 16 行:获取用户的密码;
  • 第 17 行:获取在请求第 9 行中的 URL 时要使用的方法 [get] 或 [post]:
    • [document] 指由浏览器加载的文档,即 DOM(文档对象模型),
    • [document.forms[0]] 指文档中的第一个表单;一个文档可能包含多个表单。此处仅有一个,
    • [document.forms[0].elements['method']] 指具有 [name='method'] 属性的表单元素。共有两个:

<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<input type="radio" id="post" name="method" value="post" />POST
  • (续)
    • [document.forms[0].elements['method'].value] 是将提交给具有 [name='method'] 属性的组件的值。我们知道,提交的值即为所选单选按钮的 [value] 属性的值。因此,此处该值将是字符串 ['get', 'post'] 中的一个;
  • 第 18 行:我们对字符串 `username:password` 进行 Base74 编码。该编码后的字符串将用于 HTTP [Authorization] 头部,我们将通过该头部向服务器发送请求以进行身份验证;
  • 第 22–26 行:根据要使用的 HTTP 方法,我们调用 [doGet] 或 [doPost] 方法;
  • jQuery方法 [$.ajax] 用于发起HTTP请求;
  • 第 32–34 行:我们与一个需要 [Authorization: Basic code] HTTP 头信息的服务器进行通信;
  • 第 35 行:用户将输入形式为 [/cors-getAllCategories,/cors-addProduits, ...] 的 URL。因此,这些 URL 必须补充第 6 行中的服务器 URL;
  • 第 36 行:要使用的 HTTP 方法;
  • 第 37 行:服务器返回 JSON。我们将结果类型指定为 [text],以便按原样显示接收到的内容;
  • 第 42 行:显示服务器的文本响应;
  • 第 48-49 行:显示任何错误信息;
  • 第 53 行:[doPost] 方法接收第二个参数,即待提交的值;
  • 第 61 行:用于指示提交的值将以 JSON 字符串的形式呈现;

18.7. 客户端执行

客户端应用程序是一个由以下 [Boot] 可执行类启动的 Spring Boot 应用程序:

  

package spring.cors.client.boot;
 
import org.springframework.boot.SpringApplication;
 
import spring.cors.client.config.WebConfig;
 
public class Boot {
 
    public static void main(String[] args) {
        SpringApplication.run(WebConfig.class, args);
    }
}
  • 第 10 行:[SpringApplication.run] 方法使用 [WebConfig] 配置文件。页面 [client.html] 将部署到项目类路径中的 Tomcat 服务器上;

18.8. URL [/getAllCategories]

我们启动:

  • 在 8080 端口上启动 Web/JSON 服务器;
  • 此服务器在 8081 端口的客户端;

然后我们请求 URL [http://localhost:8081/client.html] [1]:

  • 在 [2] 中,我们对 URL [http://localhost:8080/getAllCategories] 执行 GET 请求;

我们没有收到服务器的响应。当我们查看 Chrome 开发者控制台(Ctrl-Shift-I)时,看到一条错误:

  • 在 [1] 中,我们位于 [网络] 标签页;
  • 在[2]中,我们可以看到发出的 HTTP 请求不是 [GET] 而是 [OPTIONS]。对于跨域请求,浏览器会通过发送 [OPTIONS] HTTP 请求向服务器进行验证,以确保满足特定条件。在此示例中,相关请求由标记 [5-6] 所示;
  • 在 [5] 中,浏览器询问目标 URL 是否可通过 GET 方法访问。[Access-Control-Request-Method] 请求头要求服务器返回包含 [Access-Control-Allow-Methods] HTTP 头部的响应,以表明所请求的方法被接受;
  • 在 [6] 中,浏览器发送 HTTP 标头 [Origin: http://localhost:8081]。该标头要求响应包含 [Access-Control-Allow-Origin] HTTP 标头,以表明接受指定的源;
  • 在 [7] 中,浏览器询问是否接受 [Accept] 和 [Authorization] 这两个 HTTP 头。请求头 [Access-Control-Request-Headers] 期望响应包含 [Access-Control-Allow-Headers] HTTP 头,以表明接受所请求的头部;
  • 在 [3] 中发生错误。点击图标会导致错误 [4];
  • 在 [4] 中,提示信息表明服务器未发送 [Access-Control-Allow-Origin] HTTP 头,该头用于指定是否接受请求源;
  • 在 [8] 中,我们可以看到服务器确实未发送此标头。因此,浏览器拒绝执行最初请求的 HTTP GET 请求;

我们需要修改 Web 服务器 / JSON。

18.9. 新的 Web 服务 / json

我们创建一个新的 Maven 项目 [intro-spring-cors-server-jpa]:

18.9.1. Maven 配置

新 Web 服务的 Maven 配置如下:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.cors</groupId>
    <artifactId>spring-cors-server-jpa</artifactId>
    <version>0.0.1-SNAPSHOT</version>
 
    <name>spring-cors-server-jpa</name>
    <description>démo spring cors</description>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>
 
    <dependencies>
        <dependency>
            <groupId>istia.st.spring.security</groupId>
            <artifactId>intro-spring-security-server-01</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
 
    <!-- plugins -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
</project>
  • 第 23–27 行:我们通过访问安全 Web 服务器上的 /json 存档,检索迄今为止所有已完成的工作数据;

18.9.2. Spring 配置

配置类 [AppConfig] 如下所示:

  

package spring.cors.server.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
 
import spring.security.config.SecurityConfig;
 
@Configuration
@ComponentScan(basePackages = { "spring.cors.server.service" })
@Import({ SecurityConfig.class })
public class AppConfig {
 
    // cross-domain queries
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }
}
  • 第 10 行:该类是一个 Spring 配置类;
  • 第 11 行:其他 Spring 组件可在 [spring.cors.server.service] 包中找到;
  • 第 16–19 行:我们创建了一个名为 [isCorsEnabled] 的 Spring 组件,用于指示是否接受服务器域名外的客户端;

18.9.3. [AbstractCorsController] 类

[AbstractCorsController] 类,它将成为本应用程序中所有控制器的主类:

 

其代码如下:


package spring.cors.server.service;
 
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.beans.factory.annotation.Autowired;
 
public abstract class AbstractCorsController {
 
    @Autowired
    private boolean isCorsEnabled;
 
    // sending options to the customer
    public void setHeaders(String origin, HttpServletResponse response) {
        // Cors allowed ?
        if (!isCorsEnabled || origin == null || !origin.startsWith("http://localhost")) {
            return;
        }
        // set header CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // certain headers are allowed
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
        // we authorize GET
        response.addHeader("Access-Control-Allow-Methods", "GET");
    }
}
  • 第 7 行:[CorsController] 类是抽象类,因为它设计为被继承,而非被实例化;
  • 第 13–24 行:[setHeaders] 方法将跨域请求所需的 HTTP 头部添加到发送给客户端的 [HttpServletResponse response](第 13 行)中;
  • 第 33 行:[setHeaders] 方法接受以下参数:
    • 跨域请求中 [Origin] HTTP 头中的字符串 [origin]:
Origin:http://localhost:8081

在此,第 13 行中的 [origin] 参数的值应为 [http://localhost:8081]。如果请求不包含 [Origin] HTTP 头,我们将确保 [origin==null];

  • (待续)
    • 将返回给发起请求的客户端的 [HttpServletResponse response] 对象;

这两个参数由 Spring 注入;

  • 第 15–175 行:如果应用程序配置为接受跨域请求,且发送方已发送 [Origin] HTTP 头,且该源以 [http://localhost] 开头,则接受跨域请求;否则,则拒绝;
  • 第 19 行:如果客户端位于 [http://localhost:port] 域中,则发送 HTTP 头:

Access-Control-Allow-Origin:  http://localhost:port

这意味着服务器接受该客户端的源;

  • 第 21 行:我们在 [OPTIONS] HTTP 请求中指定了两个特定的 HTTP 头部:
Access-Control-Request-Method: GET
Access-Control-Request-Headers: accept, authorization

针对 [Access-Control-Request-X] HTTP 头,服务器会返回一个 [Access-Control-Allow-X] HTTP 头来指定允许的操作。第 20–23 行只是重复了客户端的请求,以表明该请求已被接受;

18.9.4. 控制器 [MyControllerWithHttpOptions]

为避免修改第13.5.3节中讨论的不安全Web/JSON服务器[intro-server-webjson-01],我们将创建一个新的控制器。不安全服务器处理URL [/url],而新控制器将处理URL [/cors-url],该URL将接受跨源请求。

[MyControllerWithHttpOptions] 类是负责处理 [OPTIONS] 类型 HTTP 请求的控制器:

 

package spring.cors.server.service;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
import com.fasterxml.jackson.core.JsonProcessingException;
 
@Controller
public class MyControllerWithHttpOptions extends AbstractCorsController {
 
    @RequestMapping(value = "/cors-getAllCategories", method = RequestMethod.OPTIONS)
    public void getAllCategories(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse httpServletResponse){
        // headers CORS
        setHeaders(origin, httpServletResponse);
    }
...
  • 第 14 行:该类是一个 Spring MVC 控制器;
  • 第 15 行:[MyControllerWithHttpOptions] 类继承了我们刚才介绍的 [AbstractCorsController] 类;
  • 第 17–18 行:[getAllCategories] 方法(第 18 行)处理通过 HTTP [OPTIONS] 方法请求的 URL ["/cors-getAllCategories"];
  • 第 18 行:[getAllCategories] 方法接受两个参数:
    • [@RequestHeader(value = "Origin", required = false) String origin],用于在存在时获取 HTTP 标头 [Origin:http://localhost:8081] 的值。 在此示例中,[String origin] 参数将接收值 [http://localhost:8081]。该标头并非必需 [required = false]。当其不存在时,[String origin] 参数的值为 null
    • [HttpServletResponse httpServletResponse]:将发送给客户端的响应;
  • 第 21 行:我们发送支持跨源请求的 HTTP 头部。`setHeaders` 方法定义在父类 `AbstractCorsController` 中;

此操作适用于第 13.5.3 节中讨论的未受保护的 web/JSON 服务器 [intro-server-webjson-01] 所暴露的所有 URL。当该服务暴露 URL [/url] 时,上文的 [MyControllerWithHttpOptions] 类会暴露 URL [/cors-url]。

18.9.5. [MyControllerWithCors] 控制器

 

[MyControllerWithCors] 类是负责处理 [GET] 和 [POST] HTTP 请求的控制器:


package spring.cors.server.service;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
 
import com.fasterxml.jackson.core.JsonProcessingException;
 
import spring.webjson.service.MyController;
 
@Controller
public class MyControllerWithCors extends AbstractCorsController {
 
    // spring dependencies
    @Autowired
    private MyController myController;
 
...
    @RequestMapping(value = "/cors-getAllCategories", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllCategories(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse httpServletResponse) throws JsonProcessingException {
        // answer
        return myController.getAllCategories();
    }
...
  • 第 17 行:[MyControllerWithCors] 类是一个 Spring MVC 控制器
  • 第 18 行:它继承了 [AbstractCorsController] 类;
  • 第 21–22 行:从第 13.5.3 节中讨论的未受保护的 Web 服务器 / JSON [intro-server-webjson-01] 注入 [MyController] 控制器;
  • 第 25–27 行:[getAllCategories] 方法处理通过 HTTP [GET] 方法请求的 URL [/cors-getAllCategories](第 28 行);
  • 第 26 行:[getAllCategories] 方法的结果将发送给客户端。该结果是一个 JSON 流(第 27 行的 [produces] 属性以及第 25 行结果的 [String] 类型);
  • 第 27 行:该方法接收的参数与我们刚才分析的 [MyControllerWithHttpOptions] 控制器中的 [getAllCategories] 方法相同;
  • 第 30 行:调用 [myController.getAllCategories()] 方法以发送响应;

最终,是未受保护服务器的 [myController.getAllCategories()] 方法发送了响应。我们只是为其响应添加了跨域请求所需的头部信息。

对于第13.5.3节中讨论的、由不安全的Web/JSON服务器[intro-server-webjson-01]公开的所有URL,均采用此处理方式。当该服务公开URL [/url] 时,上文的 [MyControllerWithCors] 类便会公开URL [/cors-url]。

跨域请求的流程如下:

  • 客户端的 JavaScript 代码使用 HTTP GET 或 POST 请求访问 URL [/cors-url];
  • 执行此代码的浏览器会拦截该请求,并首先使用 HTTP OPTIONS 请求访问 URL [/cors-url],以验证目标 Web 服务是否接受跨源请求;
  • [MyControllerWithHttpOptions] 控制器中的某个方法会发送浏览器所期望的跨域标头;
  • 随后,浏览器使用 HTTP GET 或 POST 请求访问初始 URL [/cors-url];
  • 随后 [MyControllerWithCors] 控制器中的某个方法进行响应;

18.9.6. 测试

[intro-spring-cors-server-jpa] 项目的启动类如下:

  

package spring.cors.server.boot;
 
import org.springframework.boot.SpringApplication;
 
import spring.cors.server.config.AppConfig;
 
public class Boot {
 
    public static void main(String[] args) {
        SpringApplication.run(AppConfig.class, args);
    }
}
  • 第 10 行:使用 Spring 配置 [AppConfig] 执行静态方法 [SpringApplication.run]。根据此配置,项目归档中嵌入的 Tomcat 服务器将被启动,并在其上部署 Web 应用程序 [intro-spring-cors-server-jpa]。 项目归档中包含的未受保护服务器 Web 应用程序 [intro-server-webjson-01] 也部署在该服务器上。由于项目 [intro-spring-security-server-01] 同样包含在归档中,最终将暴露两种类型的 URL:
    • 受保护 Web 服务的 URL:/url;
    • 接受跨域请求的 Web 服务 URL:/cors-url;

现在我们可以进行进一步测试了。我们启动新版本的 Web 服务,发现问题依然存在。没有任何变化。如果我们在下面的第 7 行添加一个控制台输出语句,它将永远不会显示,这表明 [MyControllerWithHttpOptions] 类的 [getAllCategories] 方法从未被调用;


@Controller
public class MyControllerWithHttpOptions extends AbstractCorsController {
 
    @RequestMapping(value = "/cors-getAllCategories", method = RequestMethod.OPTIONS)
    public void getAllCategories(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse httpServletResponse){
        System.out.println(un_texte) ;
        // headers CORS
        setHeaders(origin, httpServletResponse);
    }
 

经过一番研究,我们发现默认情况下,Spring MVC 会自行处理 HTTP [OPTIONS] 请求。因此,响应始终由 Spring 发出,绝不会由上文第 5 行中的 [getAllCategories] 方法处理。Spring MVC 的这一默认行为是可以更改的。我们修改现有的 [AppConfig] 类:

  

package spring.cors.server.config;
 
import javax.annotation.PostConstruct;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.DispatcherServlet;
 
import spring.security.config.SecurityConfig;
 
@Configuration
@ComponentScan(basePackages = { "spring.cors.server.service" })
@Import({ SecurityConfig.class })
public class AppConfig {
 
    // cross-domain queries
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }
 
    @Autowired
    private DispatcherServlet dispatcherServlet;
 
    @PostConstruct
    public void init() {
        // the application processes requests itself HTTP [OPTIONS]
        dispatcherServlet.setDispatchOptionsRequest(true);
    }
}
  • 第 25-26 行:注入 [dispatcherServlet] Bean,该 Bean 负责处理客户端请求。该 Bean 在第 13.5.3 节中讨论的未受保护的 Web/JSON 服务器 [intro-server-webjson-01] 的配置中已定义;
  • 第 28-29 行:一旦 [AppConfig] 类被实例化且 Spring 注入完成,[init] 方法(第 29 行)就会被执行。因此,当它执行时,第 26 行的字段已经初始化完毕;
  • 第 31 行:我们配置 [dispatcherServlet] Bean,使其允许 Web 应用程序自行处理 [OPTIONS] HTTP 请求;

我们使用此新配置重新运行测试。结果如下:

  • 在[1]中,我们可以看到有两个发往 URL [http://localhost:8080/cors-getAllCategories] 的 HTTP 请求;
  • 在 [2] 中,是 [OPTIONS] 请求;
  • 在 [3] 中,服务器响应中包含我们刚刚配置的三个 HTTP 头部;

现在让我们来分析第二个请求:

  • 在 [1] 中,正在分析的请求;
  • 在 [2] 中,这是 GET 请求。得益于第一个 [OPTIONS] 请求,浏览器已获取所需信息。现在,它正在执行最初请求的 [GET] 请求;
  • 在 [3] 中,是服务器的响应;
  • 在 [4] 中,服务器发送 JSON;
  • 在 [5] 中,发生了错误;
  • 在 [6] 中,显示错误信息;

这里的情况更难解释。服务器的响应 [3] 属于正常情况 [HTTP/1.1 200 OK]。 因此,我们本应已获取到所请求的文档。可能的情况是,服务器确实发送了文档 ,但浏览器阻止了其使用,因为浏览器要求对于 GET 请求,响应中也必须包含 HTTP 头部 [Access-Control-Allow-Origin:http://localhost:8081]。

接下来,我们将修改控制器 [MyControllerWithCors],使其也发送跨源请求所需的标头:


    @RequestMapping(value = "/cors-getAllCategories", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllCategories(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse httpServletResponse) throws JsonProcessingException {
        // headers CORS
        setHeaders(origin, httpServletResponse);
        // answer
        return myController.getAllCategories();
}
  • 第 6 行:响应中包含了跨域请求所需的头部字段;

进行此更改后,结果如下:

我们已成功获取了分类列表。

18.10. 其他 [GET] URL

在 [MyControllerWithCors, MyControllerWithHttpOptions] 控制器中,处理请求的 [GET] URL 的操作代码遵循了之前处理 [/cors-getAllCategories] URL 的操作模式。读者可以查阅本文档提供的示例代码进行验证。以下是 [/cors-getAllProducts] URL 的一个示例:

在 [MyControllerWithHttpOptions] 中


    @RequestMapping(value = "/cors-getAllProduits", method = RequestMethod.OPTIONS)
    public void getAllProduits(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse httpServletResponse) {
        // headers CORS
        setHeaders(origin, httpServletResponse);
}

在 [MyControllerWithCors] 中


    @RequestMapping(value = "/cors-getAllProduits", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllProduits(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse httpServletResponse) throws JsonProcessingException {
        // headers CORS
        setHeaders(origin, httpServletResponse);
        // answer
        return myController.getAllProduits();
}

得到的结果如下:

18.11. [POST] URL

让我们来分析以下场景:

  • 我们向 URL [2] 发送一个 POST [1] 请求;
  • 在 [3] 中,即提交的值。这是一个 JSON 字符串;
  • 总体而言,我们试图创建一个名为 [category2] 的分类;

目前我们并未修改任何代码。所得结果如下:

  • 在[1]中,与[GET]请求类似,浏览器会发出一个[OPTIONS]请求;
  • 在[2]中,它请求[POST]请求的访问授权。此前,它是[GET];
  • 在[3]中,它请求发送HTTP头部[accept, authorization, content-type]的授权。此前,我们仅有前两个头部;
  • 在 [4] 中,Web 服务未授予所有请求的权限,从而导致错误 [5];

我们将 [AbstractController.sendHeaders] 方法修改如下:


package spring.cors.server.service;
 
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.beans.factory.annotation.Autowired;
 
public abstract class AbstractCorsController {
 
    @Autowired
    private boolean isCorsEnabled;
 
    // sending options to the customer
    public void setHeaders(String origin, HttpServletResponse response) {
        // Cors allowed ?
        if (!isCorsEnabled || origin == null || !origin.startsWith("http://localhost")) {
            return;
        }
        // set header CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // certain headers are allowed
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
        // we authorize GET and POST
        response.addHeader("Access-Control-Allow-Methods", "GET, POST");
    }
}
  • 第 21 行:我们添加了 HTTP 标头 [Content-Type](不区分大小写);
  • 第 23 行:我们添加了 HTTP 方法 [POST];

这意味着 [POST] 方法与 [GET] 请求的处理方式相同。以下是 URL [/cors-addArticles] 的示例:

在 [MyControllerWithCors] 中


    @RequestMapping(value = "/cors-addCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String addCategories(HttpServletRequest request,
            @RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse httpServletResponse)
                    throws JsonProcessingException {
        // headers CORS
        setHeaders(origin, httpServletResponse);
        // answer
        return myController.addCategories(request);
}

位于 [MyControllerWithHttpOptions] 中


    @RequestMapping(value = "/cors-addCategories", method = RequestMethod.OPTIONS)
    public void addCategories(HttpServletRequest request,
            @RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse httpServletResponse)
                    throws JsonProcessingException {
        // headers CORS
        setHeaders(origin, httpServletResponse);
}

结果如下:

 

类别 [categorie2] 已成功添加到数据库中。数据库管理系统为其分配了主键 1729。

18.12. [AuthenticateCorsController]

  

[AuthenticateCorsController] 控制器的作用是提供 URL [/cors-authenticate],该 URL 允许您通过跨域请求调用现有的 URL [/authenticate]。其代码如下:


package spring.cors.server.service;
 
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
 
import com.fasterxml.jackson.core.JsonProcessingException;
 
import spring.security.service.AuthenticateController;
 
@Controller
public class AuthenticateCorsController extends AbstractCorsController {
    @Autowired
    private AuthenticateController authenticateController;
 
    @RequestMapping(value = "/cors-authenticate", method = RequestMethod.GET)
    @ResponseBody
    public String authenticate(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse response) throws JsonProcessingException {
        // headers CORS
        setHeaders(origin, response);
        // original method
        return authenticateController.authenticate();
    }
 
    @RequestMapping(value = "/cors-authenticate", method = RequestMethod.OPTIONS)
    public void corsAuthenticate(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse response) {
        // headers CORS
        setHeaders(origin, response);
    }
 
}

以下是两个示例:

  • 响应内容通过以下 JavaScript 代码显示:

function doGet(url) {
    // make a manual Ajax call
    $.ajax({
        headers : {
            'Authorization':'Basic '+authorizationCode
        },
        url : baseUrl + url,
        type : 'GET',
        dataType : 'text',
        beforeSend : function() {
        },
        success : function(data) {
            // text result
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // system error
            response.text(JSON.stringify(jqXHR.statusCode()));
        }
    })
}
  • 响应 [1] 由 [success] 函数的第 14 行显示;
  • 响应 [2] 由 [error] 函数的第 20 行显示。[JSON.stringify] 函数将 [jqXHR.statusCode()] 对象转换为 JSON 字符串,该对象封装了发生的错误。此对象提供的信息很少。可以使用 [jqXHR] 对象的其他方法来获取信息,例如服务器返回的 HTTP 头部;

18.13. 结论

我们的应用程序现已支持跨域请求。可通过 [AppConfig] 类中的配置启用或禁用此功能:


@ComponentScan(basePackages = { "spring.cors.server.service" })
@Import({ SecurityConfig.class })
public class AppConfig {
 
    // cross-domain queries
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }
 
    @Autowired
    private DispatcherServlet dispatcherServlet;
 
    @PostConstruct
    public void init() {
        // the application processes requests itself HTTP [OPTIONS]
        dispatcherServlet.setDispatchOptionsRequest(true);
    }
}