Skip to content

21. 跨域访问管理

21.1. 架构

接下来我们将探讨跨域请求的问题。在文档 [AngularJS / Spring 4 教程] 中,我们开发了一个客户端/服务器应用程序,其中客户端是一个 AngularJS 应用程序:

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

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

为演示可能出现的问题,我们将创建一个客户端/服务器应用程序,其中:

  • 服务器将是我们安全的 Web/JSON 服务器;
  • 客户端将是一个简单的 HTML 页面,其中包含用于向 Web/JSON 服务器发送请求的 JavaScript 代码;

我们将采用以下架构:

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

21.2. [spring-cors-server-jdbc-generic] 项目

21.2.1. 搭建开发环境

  
  • 下载上述项目。[spring-cors-*] 项目位于 [<examples>\spring-database-generic\spring-cors] 文件夹中;
  • 按下 Alt-F5 并重新构建所有 Maven 项目;

然后运行名为 [spring-cors-server-jdbc-generic] 的运行配置(此时 MySQL 数据库必须正在运行),这将启动一个监听 8081 端口的 Web 服务:

 

使用名为 [spring-jdbc-generic-04-fillDataBase] 的运行时配置填充 [dbproduitscategories] 数据库:

 

运行名为 [spring-cors-client-generic] 的运行时配置,该配置将在端口 8082 上启动第二个 Web 应用程序(运行在另一个 Tomcat 实例上):

 

使用浏览器访问 URL [http://localhost:8082/client.html]:

  • 在 [1] 中,我们请求所有分类的简短版本;
  • 在 [2] 中,获取服务器的 JSON 响应;

21.2.2. 客户端项目 [spring-cors-client-generic]

  

通过 [application.properties] 文件,我们可以设置客户端 Web 应用程序的端口。其内容如下:


server.port=8082

因此:

  • 客户端是一个可通过 URL [http://localhost:8082] 访问的 Web 应用程序;
  • 服务器是一个可通过 URL [http://localhost:8081] 访问的 Web 应用程序;

由于客户端并非通过与服务器相同的端口进行访问,因此会引发跨域请求的问题。事实上,[http://localhost:8081] 和 [http://localhost:8082] 属于两个不同的域名。

21.2.3. Maven 配置

该项目是一个 Maven 项目,其 [pom.xml] 文件如下:


<?xml version="1.0" encoding="UTF-8"?>
<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>dvp.spring.database</groupId>
    <artifactId>spring-cors-client-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <name>spring-cors-client-generic</name>
    <description>Client cors for webjson server</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
 
    <!-- plugins -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
</project>
  • 第 14–19 行:这是一个 Spring Boot 项目;
  • 第 27–30 行:我们使用了 [spring-boot-starter-web] 依赖项,其中包含 Tomcat 服务器和 Spring MVC;

21.2.4. jQuery 和 JavaScript 基础

该 Web 应用程序提供以下单页:

 

它包含在浏览器中运行的 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="/js/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]。您可以在网址 [http://jquery.com/download/] 上找到 jQuery 的最新版本:

Image

我们将下载的文件放置在 [static/js] 文件夹中:

  

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

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

JS
结果
$("#element1")
: 返回所有 id
[element1],因此通常包含
包含 0 或 1 个元素,因为 HTML 页面中
两个完全相同的 ID
$("#element1").text("blah")
:将文本 [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 服务器进行通信。所有操作都在浏览器内部完成。现在,让我们查看该页面的源代码:


<!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="/js/jquery-1.11.1.min.js"></script>
</head>
<body>
  <h3>Rudiments de JQuery</h3>
  <div id="element1">
    Elément 1
  </div>
</body>
</html>

这是原始文本。它并未反映第 10–12 行对该元素所做的更改。在调试 JavaScript 时,务必牢记这一点。在这种情况下,通常无需查看显示页面的源代码。

21.2.5. 应用程序的 JavaScript 代码

让我们回到客户端应用程序页面的 HTML 代码,该页面将查询 Web 服务 / JSON:

 

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Spring MVC</title>
<script type="text/javascript" src="/js/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/client.js"></script>
</head>
<body>
    <h2>Client du service web / jSON</h2>
    <form id="formulaire">
        <!--  method HTTP -->
        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 : <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="submit" value="Valider" onclick="javascript:requestServer(); return false;"></input>
    </form>
    <hr />
    <h2>Réponse du serveur</h2>
    <div id="response"></div>
</body>
</html>
  • 第 6 行:我们导入 jQuery 库;
  • 第 7 行:我们导入即将编写的代码;
  • 第 11、15、17、21 行:请注意页面组件的 [id] 标识符。JavaScript 通过这些标识符引用这些组件;

[client.js] 的代码如下:


// global data
var url;
var posted;
var response;
var method;
 
function requestServer() {
    // retrieve information from the form
    var urlValue = url.val();
    var postedValue = posted.val();
    method = document.forms[0].elements['method'].value;
    // 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 YWRtaW46YWRtaW4='
        },
        url : 'http://localhost:8081' + url,
        type : 'GET',
        dataType : 'tex/plain',
        beforeSend : function() {
        },
        success : function(data) {
            // text result
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // system error
            response.text(jqXHR.responseText);
        }
    })
}
 
function doPost(url, posted) {
    // make a manual Ajax call
    $.ajax({
        headers : {
            'Authorization' : 'Basic YWRtaW46YWRtaW4='
        },
        url : 'http://localhost:8081    ' + url,
        type : 'POST',
        contentType : 'application/json',
        data : posted,
        dataType : 'tex/plain',
        beforeSend : function() {
        },
        success : function(data) {
            // text result
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // system error
            response.text(jqXHR.responseText);
        }
    })
}
 
// document loading
$(document).ready(function() {
    // retrieve page component references
    url = $("#url");
    posted = $("#posted");
    response = $("#response");
});
  • 第 71–75 行:文档在浏览器中加载完成后执行的 JavaScript 代码;
  • 第 73–75 行:获取 HTML 文档中三个元素的引用;
  • 第 2–5 行:在 JavaScript 文件中定义的所有函数中均可访问的全局变量;
  • 第 9 行:获取用户输入的 URL;
  • 第 10 行:获取用户要提交的值;
  • 第 11 行:获取用于向第 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'] 中的一个;
  • 第 13–18 行:根据要使用的 HTTP 方法,将执行 [doGet] 或 [doPost] 方法;
  • jQuery 的 [$.ajax] 方法用于发起 HTTP 请求;
  • 第 23–25 行:我们正在与一个要求 HTTP 头部 [Authorization: Basic code] 的服务器通信。我们为用户 [admin / admin] 创建此头部,该用户是唯一有权查询服务器的用户;
  • 第 26 行:用户将输入 [/getAllLongCategories, /saveCategories, ...] 格式的 URL。因此这些 URL 必须完整填写;
  • 第 27 行:要使用的 HTTP 方法;
  • 第 28 行:服务器返回 JSON。我们将响应类型指定为 [text/plain],以便按原样显示;
  • 第 33 行:显示服务器的文本响应;
  • 第 39 行:以文本格式显示任何错误信息;
  • 第 44 行:[doPost] 方法接收第二个参数,即待提交的值;
  • 第 52 行:用于指示提交的值将以 JSON 字符串的形式呈现;

21.2.6. 客户端执行

客户端应用程序是一个由以下可执行 [Client] 类启动的控制台应用程序:

  

package spring.cors.client;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 
@EnableAutoConfiguration
public class Client {
 
    public static void main(String[] args) {
        SpringApplication.run(Client.class, args);
    }
}
  • 第 6 行:[@EnableAutoConfiguration] 注解是 [Spring Boot] 项目的注解(第 4 行)。Spring Boot 会检查项目类路径中存在的归档文件。在此情况下,这些文件将是 [pom.xml] 文件中单个依赖项提供的所有 Maven 依赖项:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
</dependencies>

该依赖包含大量库,其中最重要的是 Spring MVC 和 Tomcat 服务器。由于这些依赖关系,Spring Boot 将使用默认值配置一个在 Tomcat 上运行的 Spring MVC 项目。 随后,Tomcat 服务器将被配置为在 8080 端口上运行。若需覆盖 Spring Boot 选定的默认值,可使用位于类路径根目录下的 [application.properties] 文件([src/main/resources] 目录下的所有内容均位于类路径根目录):

  

我们指定 Tomcat 服务器应运行在 8082 端口,具体如下:


server.port=8082

可在以下网址(2015年6月)[http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html] 查阅 [application.properties] 中可用的参数列表;

返回 [Client.java] 中的代码:

  • 第 10 行:[SpringApplication.run] 方法将把 [client.html] 页面部署到项目类路径中的 Tomcat 服务器上;

21.2.7. URL [/getAllShortCategories]

我们启动:

  • 在端口 8081 上运行安全的 Web/JSON 服务器(配置 [spring-security-server-jdbc-generic]);
  • 在端口 8082 上启动该服务器的客户端(配置 [spring-cors-client-generic]);

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

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

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

  • 在 [1] 中,我们位于 [网络] 标签页;
  • 在 [2] 中,我们可以看到发出的 HTTP 请求不是 [GET] 而是 [OPTIONS]。对于跨域请求,浏览器会通过发送 HTTP [OPTIONS] 请求向服务器查询,以确保满足特定条件。在此情况下,相关请求即 [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。

21.2.8. 一个新的 Web 服务 / JSON

我们正在创建一个新的 Maven 项目 [spring-cors-server-jdbc-generic]:

 

新 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>dvp.spring.database</groupId>
    <artifactId>spring-cors-server-jdbc-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>
 
    <name>spring-cors-server-jdbc-generic</name>
    <description>démo spring cors</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>
 
    <!-- plugins -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
    <dependencies>
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-security-server-jdbc-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>
  • 第 30–32 行:我们通过访问 Web 服务器上的安全 JSON 存档,检索迄今为止所有已完成的工作数据;

最终,依赖关系如下:

  

配置类 [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;

@Configuration
@ComponentScan(basePackages = { "spring.cors.server.service" })
@Import({ spring.security.config.AppConfig.class })
public class AppConfig {
 
    // cross-domain queries
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }
...
}
  • 第 12 行:该类是一个 Spring 配置类;
  • 第 9 行:其他 Spring 组件位于 [spring.cors.server.service] 包中;
  • 第 14 行:我们从 [spring-security-server-jdbc-generic] 项目导入 Bean;
  • 第 18–21 行:我们创建了一个名为 [isCorsEnabled] 的 Spring 组件,用于指示是否接受服务器域名外的客户端;

21.2.9. 控制器

该新 Web 服务包含四个控制器:

  
  • [CorsCategorieController] 负责处理分类相关的 URL。它仅管理来自 Web 客户端的 CORS 头部。除此之外,它将工作委托给 [spring-webjson-server-jdbc-generic] 依赖项中的 [CategorieController];
  • [CorsProductController] 和 [CorsAuthenticateController] 同样通过将工作委托给 [spring-webjson-server-jdbc-generic] 依赖项中的 [ProductController] 以及 [spring-security-server-jdbc-generic] 依赖项中的 [AuthenticateController] 来实现;
  • [CorsController] 用于提取前三个控制器共有的逻辑;

21.2.9.1. [CorsController]

[CorsController] 类的定义如下:


package spring.cors.server.service;
 
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class CorsController {
 
    @Autowired
    private boolean isCorsEnabled;
 
    // sending options to the customer
    public void sendOptions(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");
    }
}
  • 第 8 行:[CorsController] 类是一个 Spring 控制器;
  • 第 11–12 行:注入 [isCorsEnabled] Bean,该 Bean 用于指示是否处理 CORS 头部;
  • 第 15–26 行:[sendOptions] 方法处理对发送 CORS 头信息的客户端的响应;
  • 第 17-19 行:如果应用程序配置为接受跨域请求,且发送方已发送 [Origin] HTTP 头,且该源以 [http://localhost] 开头,则接受跨域请求;否则,则拒绝;
  • 第 21 行:如果客户端位于 [http://localhost:port] 域中,则发送 HTTP 头:

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

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

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

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

21.2.9.2. 控制器 [CorsCategorieController]


package spring.cors.server.service;
 
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.beans.factory.annotation.Autowired;
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.RestController;
 
import spring.jdbc.entities.Categorie;
import spring.webjson.server.entities.CoreCategorie;
import spring.webjson.server.service.CategorieController;
import spring.webjson.server.service.Response;
 
@RestController
public class CorsCategorieController extends CorsController {
 
    @Autowired
    private CategorieController categorieController;
 
    @RequestMapping(value = "/cors-getAllShortCategories", method = RequestMethod.OPTIONS)
    public void corsGetAllShortCategories(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse response) {
        sendOptions(origin, response);
    }
 
    @RequestMapping(value = "/cors-getAllShortCategories", method = RequestMethod.GET)
    public Response<List<Categorie>> getAllShortCategories(
            @RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
        // original method
        return categorieController.getAllShortCategories();
    }
 
...
}
  • 第 19 行:[@RestController] 注解使该类既是 Spring 组件,也是 MVC 控制器,能够向客户端发送自己的响应;
  • 第 20 行:[CorsCategorieController] 类继承了我们刚才看到的 [CorsController] 类;
  • 第 22–23 行:从 [spring-webjson-server-jdbc-generic] 依赖中注入 [CategorieController] 控制器;
  • 第 25–29 行:处理通过 HTTP [OPTIONS] 方法请求 URL [/cors-getAllShortCategories] 的情况。根据约定,我们规定希望调用安全 Web 服务 [/U] URL 的 Web 客户端必须实际调用 [/cors-U] URL。因此,部署后的 Web 服务将包含两种类型的 URL:
    • [/U]:用于非 Web 客户端;
    • [/cors-U]:用于 Web 客户端;
  • 第 25 行:[/cors-getAllShortCategories] 方法接受以下参数:
    • 对象 [@RequestHeader(value = "Origin", required = false)],用于从请求中获取 HTTP [Origin] 头部。该头部由请求源发送:
Origin:http://localhost:8082

我们指定 [Origin] HTTP 头为可选 [required = false]。在此情况下,如果该头缺失,[String origin] 参数将取空值。若设置为 [required = true](即默认值),则头缺失时会抛出异常。我们希望避免这种情况;

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

这两个参数由 Spring 注入;

  • 第 28 行:我们将请求的处理委托给父类 [CorsController] 的 [sendOptions] 方法;
  • 第 31–36 行:当通过 GET 请求访问 URL [/cors-getAllShortCategories] 时,由 [getAllShortCategories] 方法进行处理;
  • 第 35 行:将工作委托给 [spring-webjson-server-jdbc-generic] 依赖项中的 [CategorieController.getAllShortCategories] 方法

现在我们可以进行进一步测试了。我们发布新版 Web 服务后发现,问题依然存在。没有任何变化。如果我们在上面的第 28 行添加控制台输出,该输出将永远不会显示,这表明第 25 行的 [corsGetAllShortCategories] 方法从未被调用。

经过一番研究,我们发现 Spring MVC 会通过默认处理机制自行处理 [OPTIONS] HTTP 请求。因此,响应始终由 Spring 发出,而第 25 行中的 [corsGetAllShortCategories] 方法从未被调用。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;
 
@Configuration
@ComponentScan(basePackages = { "spring.cors.server.service" })
@Import({ spring.security.config.AppConfig.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);
    }
}
 
  • 第 23-24 行:我们注入了 [DispatcherServlet dispatcherServlet] 组件,该组件在 [spring-webjson-server-jdbc-generic] 依赖中已定义;
  • 第 26-30 行:[@PostConstruct] 注解确保 [init] 方法将在 [AppConfig] 类实例化完成且 Spring 完成注入操作后执行;
  • 第 29 行:我们配置该 Servlet,使其将 [OPTIONS] HTTP 请求转发至应用程序;

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

  • 在 [1] 中,我们可以看到有两个针对 URL [http://localhost:8080/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]。

我们修改处理 URL [/cors-getAllShortCategories] 的 GET 请求的方法:


    @RequestMapping(value = "/cors-getAllShortCategories", method = RequestMethod.GET)
    public Response<List<Categorie>> getAllShortCategories(
            @RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
        // headers CORS
        sendOptions(origin, response);
        // original method
        return categorieController.getAllShortCategories();
}
  • 第 5 行:与 HTTP [OPTIONS] 请求类似,服务器也会为 HTTP [GET] 请求发送 CORS HTTP 头部;

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

 

我们已成功获取所有分类的简短版本。

21.2.9.3. [GET] URL

在 [CorsCategorieController、CorsProduitController、CorsAuthenticateController] 控制器中,处理请求的 [GET] URL 的操作代码遵循了之前处理 URL [/cors-getAllShortArticles] 的操作模式。 读者可查阅本文档附带的示例代码进行验证。以下是 [CorsProduitController] 控制器中针对 [/cors-getAllLongProduits] URL 的示例:


package spring.cors.server.service;
 
import java.util.List;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.beans.factory.annotation.Autowired;
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.RestController;
 
import spring.jdbc.entities.Produit;
import spring.webjson.server.entities.CoreProduit;
import spring.webjson.server.service.ProduitController;
import spring.webjson.server.service.Response;
 
@RestController
public class CorsProduitController extends CorsController {
 
    @Autowired
    private ProduitController produitController;
 
@RequestMapping(value = "/cors-getAllLongProduits", method = RequestMethod.GET)
    public Response<List<Produit>> getAllLongProduits(@RequestHeader(value = "Origin", required = false) String origin,HttpServletResponse response) {
        // headers CORS
        sendOptions(origin, response);
        // original method
        return produitController.getAllLongProduits();
 
    }
 
    @RequestMapping(value = "/cors-getAllLongProduits", method = RequestMethod.OPTIONS)
    public void corsGetAllLongProduits(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse response) {
        sendOptions(origin, response);
    }
...
}
 

21.2.9.4. [POST] URL

让我们考虑以下场景:

  • 我们向 URL [2] 发送一个 POST [1] 请求;
  • 在 [3] 中,是提交的值。这是没有产品的分类的 JSON 字符串;
  • 最终,我们希望创建一个名为 [category[2]] 的分类;

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

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

我们将 [CorsController.sendOptions] 方法修改如下:


    public void sendOptions(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");
    }
}
  • 第 9 行:我们添加了 HTTP 标头 [Content-Type](不区分大小写);
  • 第 11 行:我们添加了 HTTP 方法 [POST];

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


    @RequestMapping(value = "/cors-saveCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Response<List<CoreCategorie>> saveCategories(HttpServletRequest request,
            @RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
        // headers CORS
        sendOptions(origin, response);
        // original method
        return categorieController.saveCategories(request);
    }
 
    @RequestMapping(value = "/cors-saveCategories", method = RequestMethod.OPTIONS)
    public void corsSaveCategories(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse response) {
        sendOptions(origin, response);
}

完成这些修改后,结果如下:

 

类别 [categorie[2]] 已成功添加到数据库中。数据库管理系统 (DBMS) 为其分配了主键 226。可通过 GET 方法 [/cors-getAllShortCategories] 进行验证:

 

21.2.10. 结论

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


package spring.cors.server.config;
 
...
 
@Configuration
@ComponentScan(basePackages = { "spring.cors.server.service" })
@Import({ spring.security.config.AppConfig.class })
public class AppConfig {
 
    // cross-domain queries
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }
...
}

21.3. Eclipse 项目 [spring-cors-server-jpa-generic]

CORS Web 服务现将由 [spring-cors-server-jpa-generic] 项目实现,该项目基于 [spring-security-server-jpa-generic] 项目,后者使用 Spring Data JPA 管理数据库访问:

[spring-cors-server-jpa-generic] 项目是通过克隆之前研究的 [spring-cors-server-jdbc-generic] 项目创建的。

  

接下来,需要进行两处修改。第一处是在 [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>dvp.spring.database</groupId>
    <artifactId>spring-cors-server-jpa-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>
 
    <name>spring-cors-server-jpa-generic</name>
    <description>démo spring cors</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>
 
    <!-- plugins -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
    <dependencies>
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-security-server-jpa-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>
  • 第 30–32 行:对安全 Web 服务 [spring-security-server-jpa-generic] 的依赖;

最终,项目依赖项如下:

  

注意:按下 Alt-F5,然后重新生成所有项目

第二项更改是更新那些报告错误的类中的导入语句 [Alt-Shift-O]。

就这样。我们使用运行时配置 [spring-cors-server-jpa-generic-hibernate-eclipselink] 启动 CORS Web 服务:

然后启动通用客户端:

然后使用浏览器,通过 GET 请求访问 URL [1]。在 [2] 中,我们可以看到返回的类别简短版本中包含 [entityType] 字段,而之前的 JDBC 版本中该字段是缺失的。

接下来我们将介绍另外两种 CORS 架构:

  • CORS / JPA EclipseLink / DB2 架构;
  • CORS / JPA EclipseLink / DB2 架构;

我们将实现以下架构:

加载以下项目:

  

注意:按下 Alt-F5 并重新生成所有 Maven 项目。

启动 DB2 数据库,并验证 [dbproduitscategories] 数据库是否存在。如果不存在,请创建它(参见第 12.1.2 节)。

我们使用 [spring-security-create-users-hibernate-eclipselink] 运行时配置在 [dbproduitscategories] 数据库中创建用户:

Image

然后使用名为 [spring-cors-server-jpa-generic-hibernate-eclipselink] 的运行时配置启动 CORS Web 服务,并使用其名为 [spring-cors-client-generic] 的客户端:

使用运行时配置 [spring-jdbc-generic-04-fillDataBase] 将数据填入 [dbproduitscategories] 数据库:

 

最后,在浏览器中访问以下 URL:

 

21.5. CORS / JPA OpenJPA / Firebird 架构

接下来我们将实现以下架构:

加载以下项目:

  

注意:按下 Alt-F5 并重新生成所有 Maven 项目。

启动 Firebird 数据库管理系统,并验证 [dbproduitscategories] 数据库是否存在。如果不存在,请创建它(参见第 14.1.2 节)。

我们使用 [spring-security-create-users-openjpa] 运行时配置在 [dbproduitscategories] 数据库中创建用户:

Image

然后使用名为 [spring-cors-server-jpa-generic-openjpa] 的运行时配置启动 CORS Web 服务:

使用 [spring-cors-client-generic] 配置启动 CORS 客户端:

 

使用 [spring-jdbc-generic-04-fillDataBase] 运行时配置向 [dbproduitscategories] 数据库中插入数据:

 

最后,在浏览器中访问以下 URL: