Skip to content

21. 跨域访问管理

21.1. 架构

现在我们将探讨跨域请求的问题。在文档 [Tutoriel 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-*] 项目位于 [<exemples>\spring-database-generic\spring-cors] 文件夹中;
  • 按 Alt-F5 并重新生成所有 Maven 项目;

随后运行名为 [spring-cors-server-jdbc-generic] 的运行配置(需先启动 SGBD 和 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 /> <!-- 从存储库中查找父级 -->
    </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>

    <!-- 插件 -->
    <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]。可在 jQuery 的最新版本中找到它,即 URL [http://jquery.com/download/]:

Image

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

  

完成上述操作后,使用Chrome浏览器请求静态视图[jQuery.html]:

在 Google Chrome 中,执行 [Ctrl-Maj-I] 以调出开发者工具 [3]。通过 [Console] 和 [4] 标签页,可以运行 JavaScript 代码。 下文将提供需输入的 JavaScript 命令并附上说明。

JS
résultat
$("#element1")
:返回所有具有 id
[element1],因此通常包含
包含0或1个元素,因为
HTML。
$("#element1").text("blabla")
:将文本 [blabla] 应用于集合中的所有元素
。这将导致
页面显示的内容
$("#element1").hide()
隐藏了集合中的元素。文本
[blabla] 不再显示。
$("#element1")
:重新显示集合。这让我们
可以看到,ID 为 [element1] 的元素
具有 CSS style='display : none;' 属性,这
该元素被隐藏。
$("#element1").show()
:显示集合中的元素。文本
[blabla] 再次显示。正是
CSS style='display : block;' 确保了这种
显示效果。
$("#element1").attr('style','color: red')
:为所有元素设置一个属性
集合。此处的属性为 [style],其值为
[color: red]。文本 [blabla] 变为红色。
Tableau
Dictionnaire

需要注意的是,在整个操作过程中,浏览器的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. 应用程序的代码 jS

让我们回到客户端应用程序页面的代码 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">
        <!-- 方法 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 />
        <!-- 提交的值 -->
        <br /> Chaîne jSON à poster : <input type="text" id="posted" size="50" />
        <!-- 确认按钮 -->
        <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] 如下:


// 全局数据
var url;
var posted;
var response;
var method;

function requestServer() {
    // 获取表单信息
    var urlValue = url.val();
    var postedValue = posted.val();
    method = document.forms[0].elements['method'].value;
    // 手动发起Ajax请求
    if (method === "get") {
        doGet(urlValue);
    } else {
        doPost(urlValue, postedValue);
    }
}

function doGet(url) {
    // 手动发起Ajax请求
    $.ajax({
        headers : {
            'Authorization' : 'Basic YWRtaW46YWRtaW4='
        },
        url : 'http://localhost:8081' + url,
        type : 'GET',
        dataType : 'tex/plain',
        beforeSend : function() {
        },
        success : function(data) {
            // 文本结果
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // 系统错误
            response.text(jqXHR.responseText);
        }
    })
}

function doPost(url, posted) {
    // 手动发起Ajax请求
    $.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) {
            // 文本结果
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // 系统错误
            response.text(jqXHR.responseText);
        }
    })
}

// 在加载文档时
$(document).ready(function() {
    // 正在获取页面组件的引用
    url = $("#url");
    posted = $("#posted");
    response = $("#response");
});
  • 第71-75行:jS代码在文档于浏览器中加载完毕后执行;
  • 第 73-75 行:获取文档 HTML 中三个区域的引用;
  • 第2-5行:在文件jS中定义的所有函数中均有效的全局变量;
  • 第 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行:用户将输入URL类型的[/getAllLongCategories, /saveCategories, ...]。因此需要补充这些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 选择的默认值,可以使用位于 Classpath 根目录下的 [application.properties] 文件([src / main / resources] 中的所有内容都位于 Classpath 根目录下):

  

我们通过以下方式指定 Tomcat 服务器应在 8082 端口上运行:


server.port=8082

可用参数列表可在 [application.properties] 至 URL(2015年6月)及 [http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html] 中找到;

返回 [Client.java] 的代码:

  • 第10行:[SpringApplication.run]方法将把[client.html]页面部署到项目Classpath中的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] 时,当前所在标签页为 [Network];
  • 在 [2] 标签页中,可以看到已发出的请求 HTTP 并非 [GET],而是 [OPTIONS]。 对于跨域请求,浏览器会通过向服务器发送请求 HTTP [OPTIONS],来验证是否满足若干条件。 在此情况下,请求即为[5-6]标记所指向的那些;
  • 在 [5] 中,浏览器询问是否可以通过 GET 访问目标 URL。 请求 [Access-Control-Request-Method] 要求响应包含标头 HTTP [Access-Control-Allow-Methods],表明所请求的方法被接受;
  • 在 [6] 中,浏览器发送了 HTTP [Origin: http://localhost:8081] 标头。 该标头要求在 HTTP [Access-Control-Allow-Origin] 标头中返回响应,表明已接受指定的源;
  • 在 [7] 中,浏览器询问是否接受 HTTP、[accept] 和 [authorization] 这三个标头。 请求 [Access-Control-Request-Headers] 等待包含 HTTP 和 [Access-Control-Allow-Headers] 标头的响应,表明所请求的标头已被接受;
  • 出现 [3] 错误。点击图标后,出现 [4] 错误;
  • 在 [4] 中,消息提示服务器未发送 HTTP [Access-Control-Allow-Origin] 头部,该头部用于指示是否接受请求来源;
  • 在 [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>

    <!-- 插件 -->
    <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 {

    // 跨域请求
    @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];
  • [CorsProduitController] 和 [CorsAuthenticateController] 同样将工作委托给依赖项 [spring-webjson-server-jdbc-generic] 中的控制器 [ProduitController] 以及依赖项 [AuthenticateController] 中的控制器[spring-security-server-jdbc-generic] 依赖项中的控制器;
  • [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;

    // 向客户端发送选项
    public void sendOptions(String origin, HttpServletResponse response) {
        // 是否允许跨源请求?
        if (!isCorsEnabled || origin == null || !origin.startsWith("http://localhost")) {
            return;
        }
        // 设置 CORS 头部
        response.addHeader("Access-Control-Allow-Origin", origin);
        // 允许某些标头
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
        // 允许 GET
        response.addHeader("Access-Control-Allow-Methods", "GET");
    }
}
  • 第 8 行:类 [CorsController] 是一个 Spring 控制器;
  • 第11-12行:注入Bean [isCorsEnabled],该Bean用于指示是否需要处理CORS头部;
  • 第15-26行:方法[sendOptions]负责响应发送CORS头部的客户端;
  • 第17-19行:如果应用程序配置为接受跨域请求,且请求发起方发送了HTTP和[Origin]这两个头部,并且该源以[http://localhost]开头, 则接受跨域请求,否则拒绝;
  • 第 21 行:如果客户端位于 [http://localhost:port] 域中,则发送 HTTP 标头:
Access-Control-Allow-Origin:  http://localhost:端口

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

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

针对请求头 HTTP [Access-Control-Request-X],服务器以请求头 HTTP [Access-Control-Allow-X] 进行响应,其中指明了允许的内容。 第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) {
        // 源方法
        return categorieController.getAllShortCategories();
    }

...
}
  • 第19行:注解[@RestController]使该类同时成为Spring组件和控制器MVC,该控制器会自行向客户端发送响应;
  • 第 20 行:类 [CorsCategorieController] 继承了我们刚刚看到的类 [CorsController];
  • 第22-23行:将控制器[CategorieController categorieController]注入依赖项[spring-webjson-server-jdbc-generic];
  • 第 25-29 行:处理 URL 和 [/cors-getAllShortCategories],当它们被 HTTP 和 [OPTIONS] 命令调用时。 根据约定,我们决定:希望调用安全Web服务中URL [/U]的Web客户端,实际上应调用URL [/cors-U]。 因此,部署的 Web 服务将包含两种类型的 URL:
    • [/U]:用于非Web客户端;
    • [/cors-U]:用于 Web 客户端;
  • 第25行:[/cors-getAllShortCategories]方法支持以下参数:
    • 对象 [@RequestHeader(value = "Origin", required = false)] 将从请求中获取标头 HTTP [Origin]。该标头由请求发起者发送:
Origin:http://localhost:8082

说明 HTTP [Origin] 头部是可选的 [required = false]。 在此情况下,若该标头缺失,参数 [String origin] 的值将为 null。若使用默认值 [required = true],当标头缺失时将抛出异常。我们希望避免这种情况;

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

这两个参数由 Spring 注入;

  • 第28行:将请求的处理委托给父类[CorsController]中的方法[sendOptions];
  • 第31-36行:当通过GET调用时,[getAllShortCategories]方法处理URL和[/cors-getAllShortCategories];
  • 第 35 行:任务被委托给依赖项 [spring-webjson-server-jdbc-generic] 中的方法 [CategorieController.getAllShortCategories]

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

经过一番调查,我们发现Spring MVC会自行处理HTTP和[OPTIONS]命令,并采用默认处理机制。 因此,响应始终由 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 {

    // 跨域请求
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }
    
    @Autowired
    private DispatcherServlet dispatcherServlet;
    
    @PostConstruct
    public void init(){
        // 应用程序自行处理请求QZXW2HTMLCSFRUUAZQX[OPTIONS]
        dispatcherServlet.setDispatchOptionsRequest(true);
    }
}

  • 第 23-24 行:注入在依赖项 [spring-webjson-server-jdbc-generic] 中定义的组件 [DispatcherServlet dispatcherServlet];
  • 第 26-30 行:注解 [@PostConstruct] 确保在实例化类 [AppConfig] 并完成 Spring 的注入操作后,将执行方法 [init];
  • 第 29 行:要求该 Servlet 将命令 HTTP 和 [OPTIONS] 转发给应用程序;

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

  • 在 [1] 中,我们看到有两个请求 HTTP 分别指向 URL 和 [http://localhost:8080/getAllCategories];
  • 在 [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]。

我们将处理 GET 的方法从 URL 修改为 [/cors-getAllShortCategories]:


    @RequestMapping(value = "/cors-getAllShortCategories", method = RequestMethod.GET)
    public Response<List<Categorie>> getAllShortCategories(
            @RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
        // 标头CORS
        sendOptions(origin, response);
        // 源方法
        return categorieController.getAllShortCategories();
}
  • 第5行:与处理HTTP和[OPTIONS]请求时相同, 服务器将针对请求 HTTP [GET] 发送请求头 HTTP CORS;

修改后,结果如下:

 

我们确实获得了所有类别的简短版本。

21.2.9.3. URL [GET]

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


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) {
        // CORS 头信息
        sendOptions(origin, response);
        // 原始方法
        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. URL [POST]

让我们来看以下情况:

  • 将 POST [1] 映射到 URL [2];
  • 在 [3] 中,该值已被发布。这是某个无商品分类的字符串 jSON;
  • 最终,我们试图创建一个名为 [categorie[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) {
        // 允许正文吗?
        if (!isCorsEnabled || origin == null || !origin.startsWith("http://localhost")) {
            return;
        }
        // 设置标头 CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // 允许某些标头
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
        // 允许 GET 和 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) {
        // CORS 标头
        sendOptions(origin, response);
        // 来源方法
        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]] 已成功添加到数据库中。 SGBD 为其分配了主键 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 {

    // 跨域请求
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }
...
}

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

Web 服务 CORS 现将由项目 [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>

    <!-- 插件 -->
    <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-Maj-O]。

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

然后启动通用客户端:

并通过浏览器请求 URL、[1] 以及 GET。 在 [2] 中,可以看到返回的分类简短版本包含字段 [entityType],而之前的版本 JDBC 中并不包含该字段。

接下来我们将处理另外两个架构 CORS:

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

我们将实现以下架构:

加载以下项目:

  

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

运行 SGBD 和 DB2,并确认数据库 [dbproduitscategories] 是否存在。如果不存在,请创建它(参见第 12.1.2 节)。

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

Image

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

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

 

最后,在浏览器中请求以下 URL:

 

21.5. CORS / JPA OpenJPA / Firebird

现在我们将实现以下架构:

加载以下项目:

  

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

启动 SGBD Firebird 并确认 [dbproduitscategories] 数据库确实存在。如果不存在,请创建它(参见第 14.1.2 节)。

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

Image

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

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

 

使用运行配置 [spring-jdbc-generic-04-fillDataBase] 将值填入表 [dbproduitscategories]:

 

最后,在浏览器中请求以下 URL: