Skip to content

18. [Cours]: Gerenciamento de acesso entre domínios

Palavras-chave: CORS (Cross-Origin Resource Sharing).

Este capítulo está um pouco à margem do TD. Ele foi mantido porque apresenta a programação web e a programação em JavaScript. É importante lembrar aqui que um dos objetivos deste TD é apresentar os conceitos frequentemente utilizados no desenvolvimento JEE, ou seja, o desenvolvimento web baseado em frameworks Java. Aqui, complementamos o servidor web utilizado no estudo do banco de dados de produtos e categorias para permitir que ele aceite solicitações entre domínios.

No documento [Tutoriel AngularJS / Spring 4], desenvolve-se uma aplicação cliente/servidor em que o cliente é uma aplicação AngularJS:

  • as páginas HTML / CSS / JS do aplicativo Angular são originárias do servidor [1];
  • em [2], o serviço [dao] faz uma solicitação a outro servidor, o servidor [2]. Ora, isso é proibido pelo navegador que executa o aplicativo Angular, pois se trata de uma falha de segurança. O aplicativo só pode consultar o servidor de onde ele vem, ou seja, o servidor [1];

Na verdade, não é correto dizer que o navegador impede que o aplicativo Angular faça uma consulta ao servidor [2]. Na realidade, ele faz essa consulta para perguntar se o servidor autoriza que um cliente que não seja do próprio servidor faça uma consulta a ele. Essa técnica de compartilhamento é chamada de CORS (Cross-Origin Resource Sharing). O servidor [2] dá sua autorização enviando cabeçalhos HTTP específicos.

Vamos criar a seguinte arquitetura:

  • em [1], um aplicativo web fornece as páginas HTML / jS;
  • em [2], o navegador executa o JavaScript incorporado nas páginas HTML para consultar o serviço web seguro [3];

18.1. Support

  

Os projetos deste capítulo estão na pasta [support / chap-18].

18.2. O projeto do cliente

Criamos o seguinte projeto no Eclipse:

  

18.3. Configuração do Maven

O projeto é um projeto Maven com o seguinte arquivo [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>
  • linhas 11-15: trata-se de um projeto Spring Boot;
  • linhas 23-26: utiliza-se a dependência [spring-boot-starter-web], que inclui um servidor Tomcat e o Spring MVC;

18.4. Configuração do Spring

  

A classe [WebConfig], que configura o projeto Spring, é a seguinte:


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 {

    // -------------------------------- configuração da camada [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/");
    }
}
  • linha 15: a classe configura um projeto Spring MVC;
  • linha 16: a classe estende a classe [WebMvcConfigurerAdapter] para redefinir alguns de seus métodos;
  • linhas 18-36: já nos deparamos com esses beans, por exemplo, no parágrafo 13.5.3.1. Observe, na linha 35, que o serviço web funcionará na porta 8081;
  • linhas 38-42: o método [addResourceHandlers] permite definir recursos estáticos, ou seja, recursos não processados pelo [DispatcherServlet] da linha 23;
  • linha 40: qualquer solicitação de um recurso com a extensão .html receberá como resposta o arquivo solicitado pela solicitação e encontrado na pasta [static] do Classpath do projeto;
  • linha 41: qualquer solicitação de um recurso com a extensão .js terá como resposta o arquivo JavaScript solicitado pela solicitação e encontrado na pasta [static/js] do Classpath do projeto;
  

18.5. Noções básicas sobre jQuery e JavaScript

A página HTML do cliente será a seguinte:

 

Ela incluirá código JavaScript (jS) executado no navegador. Apresentaremos alguns conceitos básicos de JavaScript que nos permitirão compreender o código. O cliente fará chamadas HTTP utilizando a biblioteca jQuery [https://jquery.com/], que oferece diversas funções que facilitam o desenvolvimento em JavaScript. Criamos um arquivo estático HTML [jQuery.html], que colocamos na pasta [static]:

 

Esse arquivo terá o seguinte conteúdo:


<!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>
  • linha 6: importação de jQuery;
  • linhas 10-12: um elemento da página com o ID [element1]. Vamos trabalhar com esse elemento.

Precisamos baixar o arquivo [jquery-2.1.3.min.js]. A versão mais recente do jQuery pode ser encontrada no URL [http://jquery.com/download/]:

Image

Coloque o arquivo baixado na pasta [static / js] e altere a linha 6 do arquivo HTML de acordo com a versão instalada.

Feito isso, acesse a visualização estática [jQuery.html] usando o Chrome [1-2]:

No Google Chrome, execute o comando [Ctrl-Maj-I] para exibir as ferramentas de desenvolvimento [3]. A aba [Console] [4] permite executar código JavaScript. A seguir, apresentamos comandos JavaScript a serem digitados, acompanhados de uma explicação.

JS
résultat
$("#element1")
: torna a coleção de todos os elementos com o id [element1],
portanto, normalmente uma coleção de 0 ou 1 elemento
, pois não é possível ter dois IDs idênticos em uma página HTML.
$("#element1").text("blabla")
: atribui o texto [blabla] a todos os elementos
da coleção. Isso faz com que o
conteúdo exibido pela página
$("#element1").hide()
oculta os elementos da coleção.
O texto [blabla] não é mais exibido.
$("#element1")
: exibe a coleção novamente. Isso nos
permite ver que o elemento com id [element1] tem
o atributo CSS style='display: none;', que faz
que o elemento fique oculto.
$("#element1").show()
: exibe os elementos da coleção. O texto
[blabla] reaparece. É o atributo
CSS style='display: block;' que garante essa
exibição.
$("#element1").attr('style','color: red')
: define um atributo para todos os elementos da
coleção. O atributo aqui é [style] e seu valor
[color: red]. O texto [blabla] fica em vermelho.
Tableau
Dictionnaire

Observe-se que o URL do navegador não sofreu alterações durante todas essas operações. Não houve troca de dados com o servidor web. Tudo ocorre internamente no navegador. Agora, vamos visualizar o código-fonte da página:

Este é o texto inicial. Ele não reflete de forma alguma as alterações que fizemos no elemento nas linhas 10 a 12. É importante lembrar disso ao depurar JavaScript. Portanto, muitas vezes é desnecessário visualizar o código-fonte da página exibida.

18.6. O código JavaScript do aplicativo

Voltemos à página do aplicativo cliente que irá consultar o serviço web / jSON:

  
 

O código HTML desta página é o seguinte:


<!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">
        <!-- identificador -->
        Identifiant :
        <!--  -->
        <input type="text" id="identifiant" name="identifiant" value="" />
        <!--  senha -->
        <br /> <br /> Mot de passe :
        <!--  -->
        <input type="text" id="password" name="password" value="" />
        <!--  método 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 />
        <!-- valor lançado -->
        <br /> Chaîne jSON à poster : <input type="text" id="posted"
            size="50" />
        <!--  botão de confirmação -->
        <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>
  • linha 6: importamos a biblioteca jQuery;
  • linha 7: importa-se um código que iremos escrever;
  • linhas 15, 19, 26, 29, 31: anotamos os identificadores [id] dos componentes da página. O JavaScript faz referência a esses componentes por meio desses identificadores;

O código [client.js] é o seguinte:


// dados globais
var url;
var posted;
var response;
var method;
var baseUrl = 'http://localhost:8080';
var identifiant;
var password;
var authorizationHeader;

function requestServer() {
    // recuperamos as informações
    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);
    // apaga-se a resposta anterior
    response.text("");
    // faz-se uma chamada Ajax manualmente
    if (method === "get") {
        doGet(urlValue);
    } else {
        doPost(urlValue, postedValue);
    }
}

function doGet(url) {
    // faz-se uma chamada Ajax manualmente
    $.ajax({
        headers : {
            'Autorização: 'Basic '+authorizationCode
        },
        url : baseUrl + url,
        type : 'GET',
        dataType : 'text',
        beforeSend : function() {
        },
        success : function(data) {
            // resultado em texto
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // erro de sistema
            response.text(JSON.stringify(jqXHR.statusCode()));
        }
    })
}

function doPost(url, posted) {
    // está sendo feita uma chamada Ajax manualmente
    $.ajax({
        headers : {
            'Autorização':'Básica '+authorizationCode
        },
        url : baseUrl + url,
        type : 'POST',
        contentType : 'application/json; charset=UTF-8',
        data : posted,
        dataType : 'text',
        beforeSend : function() {
        },
        success : function(data) {
            // resultado de texto
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // erro do sistema
            response.text(JSON.stringify(jqXHR.statusCode()));
        }
    })
}

// ao carregar o documento
$(document).ready(function() {
    // recuperamos as referências dos componentes da página
    identifiant = $("#identifiant");
    password = $("#password");
    url = $("#url");
    posted = $("#posted");
    response = $("#response");
});
  • linhas 80-87: do código jS executado ao final do carregamento do documento no navegador;
  • linhas 81-86: recuperam-se as referências dos diferentes elementos do documento HTML, por meio de seus identificadores [id];
  • linhas 2-9: variáveis globais conhecidas em todas as funções definidas no arquivo jS;
  • linha 13: recupera-se o valor URL digitado pelo usuário;
  • linha 14: recupera-se o valor que ele deseja enviar (vazio se for a operação GET);
  • linha 15: recupera-se o identificador digitado pelo usuário;
  • linha 16: recupera-se a senha do usuário;
  • linha 17: recupera-se o formato [get] ou [post] a ser usado para solicitar o URL da linha 9:
    • [document] designa o documento carregado pelo navegador, o que é chamado de DOM (Document Object Model),
    • [document.forms[0]] designa o primeiro formulário do documento, já que um documento pode conter vários. Neste caso, há apenas um,
    • [document.forms[0].elements['method']] refere-se ao elemento do formulário que possui o atributo [name='method']. Existem dois:

<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<input type="radio" id="post" name="method" value="post" />POST
  • (continuação)
    • [document.forms[0].elements['method'].value] é o valor que será enviado para o componente que possui o atributo [name='method']. Sabemos que o valor enviado é o valor do atributo [value] do botão de opção marcado. Aqui, será, portanto, uma das cadeias ['get', 'post'];
  • linha 18: construímos a codificação Base74 da string “identifiant:password”. Essa string codificada será usada no cabeçalho HTTP [Authorization] que enviaremos ao servidor para autenticar a solicitação;
  • linhas 22-26: dependendo do método HTTP a ser utilizado, executa-se o método [doGet] ou [doPost];
  • o método jQuery [$.ajax] realiza uma chamada ao método HTTP;
  • linhas 32-34: acessa-se um servidor que exige um cabeçalho HTTP ou [Authorization: Basic code];
  • linha 35: o usuário inserirá URL do tipo [/cors-getAllCategories,/cors-addProduits, ...]. Portanto, é necessário preencher esses URL com o URL do servidor da linha 6;
  • linha 36: método HTTP a ser utilizado;
  • linha 37: o servidor retorna o jSON. Indica-se o tipo [text] como tipo de resultado para exibi-lo exatamente como foi recebido;
  • linha 42: exibição da resposta de texto do servidor;
  • linhas 48-49: exibição de uma eventual mensagem de erro;
  • linha 53: o método [doPost] recebe um segundo parâmetro, que é o valor a ser enviado;
  • linha 61: para indicar que o valor enviado será na forma de uma string jSON;

18.7. Execução do cliente

O aplicativo cliente é um aplicativo Spring Boot iniciado pela seguinte classe executável [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);
    }
}
  • linha 10: o método [SpringApplication.run] utiliza o arquivo de configuração [WebConfig]. A página [client.html] será implantada no servidor Tomcat presente no Classpath do projeto;

18.8. O URL [/getAllCategories]

Estamos lançando:

  • o servidor web/json na porta 8080;
  • o cliente desse servidor na porta 8081;

em seguida, solicitamos o URL [http://localhost:8081/client.html] [1]:

  • em [2], executamos um GET no URL [http://localhost:8080/getAllCategories];

Não obtemos resposta do servidor. Ao verificar o console de desenvolvimento do Chrome (Ctrl+Shift+I), encontramos um erro:

  • em [1], estamos na aba [Network];
  • em [2], percebemos que a solicitação HTTP que foi feita não é [GET], mas sim [OPTIONS]. No caso de uma solicitação entre domínios, o navegador verifica junto ao servidor se determinadas condições estão atendidas, enviando-lhe uma solicitação HTTP [OPTIONS]. Nesse caso, as solicitações são aquelas indicadas pelos marcadores [5-6];
  • em [5], o navegador pergunta se o destino URL pode ser alcançado com um GET. O cabeçalho da solicitação [Access-Control-Request-Method] solicita uma resposta com um cabeçalho HTTP [Access-Control-Allow-Methods] indicando que o método solicitado é aceito;
  • em [6], o navegador envia o cabeçalho HTTP [Origin: http://localhost:8081]. Esse cabeçalho solicita uma resposta no cabeçalho HTTP [Access-Control-Allow-Origin] indicando que a origem especificada foi aceita;
  • em [7], o navegador pergunta se os cabeçalhos HTTP, [accept] e [authorization] são aceitos. O cabeçalho da solicitação [Access-Control-Request-Headers] aguarda uma resposta com um cabeçalho HTTP [Access-Control-Allow-Headers] indicando que os cabeçalhos solicitados são aceitos;
  • ocorre um erro em [3]. Ao clicar no ícone, aparece o erro [4];
  • em [4], a mensagem indica que o servidor não enviou o cabeçalho HTTP [Access-Control-Allow-Origin], que indica se a origem da solicitação foi aceita;
  • em [8], pode-se constatar que o servidor efetivamente não enviou esse cabeçalho. Consequentemente, o navegador recusou-se a realizar a solicitação HTTP GET solicitada inicialmente;

Precisamos modificar o servidor web / jSON.

18.9. O novo serviço web / json

Criamos um novo projeto Maven [intro-spring-cors-server-jpa]:

18.9.1. Configuração do Maven

A configuração do Maven para o novo serviço web é a seguinte:


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

    <!-- plug-ins -->
    <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>
  • linhas 23-27: aproveitamos todo o trabalho realizado até o momento, utilizando o arquivo do servidor web / json seguro;

18.9.2. Configuração do Spring

A classe de configuração [AppConfig] é a seguinte:

  

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 {

    // solicitações entre domínios
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }
}
  • linha 10: a classe é uma classe de configuração do Spring;
  • linha 11: outros componentes Spring devem ser buscados no pacote [spring.cors.server.service];
  • linhas 16-19: criamos um componente Spring chamado [isCorsEnabled] que indica se aceitamos ou não clientes externos ao domínio do servidor;

18.9.3. A classe [AbstractCorsController]

A classe [AbstractCorsController], que será a classe pai de todos os controladores desta aplicação:

 

Seu código é o seguinte:


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;

    // envio das opções ao cliente
    public void setHeaders(String origin, HttpServletResponse response) {
        // CORS permitido?
        if (!isCorsEnabled || origin == null || !origin.startsWith("http://localhost")) {
            return;
        }
        // define-se o cabeçalho CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // autorizando determinados cabeçalhos
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
        // autoriza-se o GET
        response.addHeader("Access-Control-Allow-Methods", "GET");
    }
}
  • linha 7: a classe [CorsController] é abstrata, pois foi projetada para ser estendida e não instanciada;
  • linhas 13-24: o método [setHeaders] insere na resposta [HttpServletResponse response] (linha 13) enviada ao cliente os cabeçalhos HTTP exigidos pelas solicitações entre domínios;
  • linha 33: o método [/setHeaders] aceita como parâmetros:
    • a string [origin] presente nos cabeçalhos HTTP e [Origin] das solicitações entre domínios:
Origin:http://localhost:8081

Nesse caso, o parâmetro [origin] da linha 13 teria o valor [http://localhost:8081]. Caso a solicitação não contenha o cabeçalho HTTP [Origin], providenciaremos para que seja [origin==null];

  • (continuação)
    • o objeto [HttpServletResponse response], que será devolvido ao cliente que fez a solicitação;

Esses dois parâmetros são injetados pelo Spring;

  • linhas 15-175: se a aplicação estiver configurada para aceitar solicitações entre domínios e se o remetente tiver enviado o cabeçalho HTTP [Origin] e se essa origem começar por [http://localhost], então aceitaremos a solicitação entre domínios; caso contrário, a rejeitaremos;
  • linha 19: se o cliente estiver no domínio [http://localhost:port], enviamos o cabeçalho HTTP:
Access-Control-Allow-Origin:  http://localhost:porta

o que significa que o servidor aceita a origem do cliente;

  • linha 21: indicamos dois cabeçalhos HTTP específicos na solicitação HTTP [OPTIONS]:
Access-Control-Request-Method: GET
Access-Control-Request-Headers: accept, authorization

Para os cabeçalhos HTTP e [Access-Control-Request-X], o servidor responde com os cabeçalhos HTTP e [Access-Control-Allow-X], nos quais indica o que está autorizado. As linhas 20 a 23 limitam-se a repetir a solicitação do cliente para indicar que ela foi aceita;

18.9.4. O controlador [MyControllerWithHttpOptions]

Para não precisarmos modificar o servidor web não seguro / jSON [intro-server-webjson-01] analisado no parágrafo 13.5.3, vamos criar um novo controlador que, no lugar em que o servidor não seguro processa o URL [/url], o novo controlador processará os URL e [/cors-url], e este URL aceitará as solicitações entre domínios.

A classe [MyControllerWithHttpOptions] é o controlador responsável por processar as solicitações HTTP do tipo [OPTIONS]:

 

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){
        // cabeçalhos CORS
        setHeaders(origin, httpServletResponse);
    }
...
  • linha 14: a classe é um controlador Spring MVC;
  • linha 15: a classe [MyControllerWithHttpOptions] estende a classe [AbstractCorsController] que acabamos de descrever;
  • linhas 17-18: o método [getAllCategories] (linha 18) processa o URL ["/cors-getAllCategories"] quando é chamado com o método HTTP [OPTIONS];
  • linha 18: o método [getAllCategories] aceita dois parâmetros:
    • [@RequestHeader(value = "Origin", required = false) String origin] para recuperar o valor do cabeçalho HTTP [Origin:http://localhost:8081], quando este estiver presente. Neste exemplo, o parâmetro [String origin] receberá o valor [http://localhost:8081]. Este cabeçalho não é obrigatório [required = false]. Quando não estiver presente, o parâmetro [String origin] terá o valor null;
    • [HttpServletResponse httpServletResponse]: a resposta que será enviada ao cliente;
  • linha 21: enviam-se os cabeçalhos HTTP que permitem solicitações entre domínios. O método [setHeaders] está definido na classe pai [AbstractCorsController];

Isso é feito para todos os URL expostos pelo servidor web / jSON não seguro [intro-server-webjson-01], analisado no parágrafo 13.5.3. Quando esse serviço expõe o URL e o [/url], a classe [MyControllerWithHttpOptions] acima expõe o URL e o [/cors-url].

18.9.5. O controlador [MyControllerWithCors]

 

A classe [MyControllerWithCors] é o controlador que processará as solicitações HTTP dos tipos [GET] e [POST]:


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 {

    // dependências do Spring
    @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 {
        // resposta
        return myController.getAllCategories();
    }
...
  • linha 17: a classe [MyControllerWithCors] é um controlador Spring MVC
  • linha 18: ela estende a classe [AbstractCorsController];
  • linhas 21-22: injeção do controlador [MyController] do servidor web / jSON não seguro [intro-server-webjson-01], analisado no parágrafo 13.5.3;
  • linhas 25-27: o método [getAllCategories] processa o URL [/cors-getAllCategories] (linha 28) quando solicitado pelo método HTTP [GET];
  • linha 26: o resultado do método [getAllCategories] será enviado ao cliente. Esse resultado é um fluxo jSON (atributo [produces] da linha 27 e tipo [String] do resultado da linha 25);
  • linha 27: o método recebe os mesmos parâmetros que o método [getAllCategories] do controlador [MyControllerWithHttpOptions] que acabamos de analisar;
  • linha 30: solicita-se ao método [myController.getAllCategories()] que envie a resposta;

No final, é o método [myController.getAllCategories()] do servidor não seguro que envia a resposta. Simplesmente enriquecemos sua resposta com os cabeçalhos necessários para as solicitações entre domínios.

Isso é feito da mesma forma para todos os URL expostos pelo servidor web / jSON não seguro [intro-server-webjson-01] analisado no parágrafo 13.5.3. Quando esse serviço expõe o URL e o [/url], a classe [MyControllerWithCors] acima expõe o URL e o [/cors-url].

Uma solicitação entre domínios ocorrerá da seguinte maneira:

  • o código JS do cliente solicita oURL e [/cors-url] por meio de uma solicitação HTTP, GET ou POST;
  • o navegador que executa esse código intercepta essa solicitação e solicita primeiro oURL [/cors-url] com uma solicitação HTTP OPTIONS para verificar se o serviço web de destino aceita solicitações entre domínios;
  • um dos métodos do controlador [MyControllerWithHttpOptions] envia os cabeçalhos entre domínios esperados pelo navegador;
  • o navegador então solicita a resposta inicial URL ([/cors-url]) com uma solicitação HTTP, GET ou POST;
  • um dos métodos do controlador [MyControllerWithCors] responde a ele;

18.9.6. Testes

A classe de inicialização do projeto [intro-spring-cors-server-jpa] é a seguinte:

  

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);
    }
}
  • linha 10: o método estático [SpringApplication.run] é executado com a configuração Spring [AppConfig]. Devido a essa configuração, o servidor Tomcat incorporado nos arquivos do projeto é executado e a aplicação web [intro-spring-cors-server-jpa] é implantada nele. A aplicação web do servidor não seguro [intro-server-webjson-01], que faz parte dos arquivos do projeto, também é implantada nele. Como o projeto [intro-spring-security-server-01] também faz parte dos arquivos, dois tipos de URL são, por fim, expostos:
    • as do serviço web seguro: /url;
    • as do serviço web que aceita solicitações entre domínios: /cors-url;

Agora estamos prontos para novos testes. Lançamos a nova versão do serviço web e descobrimos que o problema persiste. Nada mudou. Se, na linha 7 abaixo, colocarmos uma saída de console, ela nunca será exibida, o que demonstra que o método [getAllCategories] da classe [MyControllerWithHttpOptions] nunca é chamado;


@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) ;
        // cabeçalhos CORS
        setHeaders(origin, httpServletResponse);
    }

Após algumas pesquisas, descobrimos que, por padrão, o Spring MVC processa por conta própria os comandos HTTP e [OPTIONS]. Portanto, é sempre o Spring que responde, e nunca o método [getAllCategories] da linha 5 acima. Esse comportamento padrão do Spring MVC pode ser alterado. Modificamos a classe [AppConfig] existente:

  

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 {

    // solicitações entre domínios
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }

    @Autowired
    private DispatcherServlet dispatcherServlet;

    @PostConstruct
    public void init() {
        // a própria aplicação processa as solicitações HTTP [OPTIONS]
        dispatcherServlet.setDispatchOptionsRequest(true);
    }
}
  • linhas 25-26: injeção do bean [dispatcherServlet], que gerencia as solicitações dos clientes. Esse bean foi definido na configuração do servidor web / jSON não seguro [intro-server-webjson-01], analisado no parágrafo 13.5.3;
  • linhas 28-29: o método [init] (linha 29) será executado assim que a classe [AppConfig] for instanciada e as injeções do Spring forem realizadas. Portanto, quando ele for executado, o campo da linha 26 já terá sido inicializado;
  • linha 31: configuramos o bean [dispatcherServlet] para que permita que a aplicação web processe por conta própria os comandos HTTP e [OPTIONS];

Repetimos os testes com essa nova configuração. Obtemos o seguinte resultado:

  • em [1], vemos que há duas solicitações HTTP para o URL [http://localhost:8080/cors-getAllCategories];
  • em [2], a solicitação [OPTIONS];
  • no [3], os três cabeçalhos HTTP que acabamos de configurar na resposta do servidor;

Vamos agora examinar a segunda solicitação:

  • em [1], a solicitação analisada;
  • em [2], que é a solicitação GET. Graças à primeira solicitação [OPTIONS], o navegador recebeu as informações que solicitava. Agora, ele realiza a solicitação [GET] solicitada inicialmente;
  • em [3], a resposta do servidor;
  • em [4], o servidor envia jSON;
  • em [5], ocorreu um erro;
  • em [6], a mensagem de erro;

É mais difícil explicar o que aconteceu aqui. A resposta [3] do servidor é normal, [HTTP/1.1 200 OK]. Portanto, deveríamos ter o documento solicitado. É possível que o servidor tenha realmente enviado o documento, mas que seja o navegador que esteja impedindo seu uso porque exige que, também para a solicitação GET, a resposta inclua o cabeçalho HTTP [Access-Control-Allow-Origin:http://localhost:8081].

Modificamos, então, o controlador [MyControllerWithCors] para que ele também envie os cabeçalhos necessários para as solicitações entre domínios:


    @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 {
        // cabeçalhos CORS
        setHeaders(origin, httpServletResponse);
        // resposta
        return myController.getAllCategories();
}
  • linha 6: os cabeçalhos necessários para as solicitações entre domínios estão incluídos na resposta;

Após essa modificação, os resultados são os seguintes:

Conseguimos obter a lista de categorias.

18.10. Os demais: URL, [GET]

Nos controladores [MyControllerWithCors, MyControllerWithHttpOptions], o código das ações que processam os URL solicitados com um [GET] segue o modelo das ações que processaram anteriormente o URL e o [/cors-getAllCategories]. O leitor pode verificar o código nos exemplos fornecidos com este documento. Aqui está um exemplo para o URL e o [/cors-getAllProduits]:

no [MyControllerWithHttpOptions]


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

em [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 {
        // cabeçalhos CORS
        setHeaders(origin, httpServletResponse);
        // resposta
        return myController.getAllProduits();
}

O resultado obtido é o seguinte:

18.11. Os URL [POST]

Vamos examinar o seguinte caso:

  • fazemos um POST [1] em direção ao URL [2];
  • no [3], o valor lançado. Trata-se de uma sequência jSON;
  • no total, queremos criar uma categoria chamada [categorie2];

Por enquanto, não alteramos nenhum código. O resultado obtido é o seguinte:

  • em [1], assim como nas solicitações [GET], o navegador realiza uma solicitação [OPTIONS];
  • em [2], ele solicita autorização de acesso para uma solicitação [POST]. Anteriormente, era [GET];
  • em [3], ele solicita autorização para enviar os cabeçalhos HTTP e [accept, authorization, content-type]. Anteriormente, havia apenas os dois primeiros cabeçalhos;
  • em [4], o serviço web não concede todas as autorizações solicitadas, o que provoca o erro [5];

Modificamos o método [AbstractController.sendHeaders] da seguinte maneira:


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;

    // envio das opções ao cliente
    public void setHeaders(String origin, HttpServletResponse response) {
        // Cors permitido?
        if (!isCorsEnabled || origin == null || !origin.startsWith("http://localhost")) {
            return;
        }
        // definimos o cabeçalho CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // autorizando determinados cabeçalhos
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
        // autoriza-se o GET e o POST
        response.addHeader("Access-Control-Allow-Methods", "GET, POST");
    }
}
  • linha 21: adicionamos o cabeçalho HTTP [Content-Type] (não importa se está em maiúsculas ou minúsculas);
  • linha 23: adicionamos o método HTTP [POST];

Com isso, os métodos [POST] são tratados da mesma forma que as solicitações [GET]. Aqui está o exemplo do URL [/cors-addArticles]:

em [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 {
        // cabeçalhos CORS
        setHeaders(origin, httpServletResponse);
        // resposta
        return myController.addCategories(request);
}

em [MyControllerWithHttpOptions]


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

O resultado obtido é o seguinte:

 

A categoria [categorie2] foi adicionada corretamente ao banco de dados. O SGBD atribuiu a ela a chave primária 1729.

18.12. O controlador [AuthenticateCorsController]

  

O controlador [AuthenticateCorsController] serve para fornecer oURL [/cors-authenticate], que permite chamar o URL [/authenticate] já existente, por meio de uma consulta entre domínios. Seu código é o seguinte:


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 {
        // cabeçalhos CORS
        setHeaders(origin, response);
        // método de origem
        return authenticateController.authenticate();
    }

    @RequestMapping(value = "/cors-authenticate", method = RequestMethod.OPTIONS)
    public void corsAuthenticate(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse response) {
        // cabeçalhos CORS
        setHeaders(origin, response);
    }

}

Aqui estão dois exemplos:

  • As respostas exibidas são geradas pelo código jS a seguir:

function doGet(url) {
    // fazemos uma chamada Ajax manualmente
    $.ajax({
        headers : {
            'Authorization':'Basic '+authorizationCode
        },
        url : baseUrl + url,
        type : 'GET',
        dataType : 'text',
        beforeSend : function() {
        },
        success : function(data) {
            // resultado em texto
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // erro de sistema
            response.text(JSON.stringify(jqXHR.statusCode()));
        }
    })
}
  • a resposta [1] é exibida pela linha 14 da função [success];
  • a resposta [2] é exibida pela linha 20 da função [error]. A função [JSON.stringify] cria a cadeia jSON do objeto [jqXHR.statusCode()], que é o objeto que encapsula o erro ocorrido. Esse objeto fornece poucas informações. É possível utilizar outros métodos do objeto [jqXHR] para obter, por exemplo, os cabeçalhos HTTP retornados pelo servidor;

18.13. Conclusion

Nosso aplicativo agora suporta solicitações entre domínios. Essas solicitações podem ser autorizadas ou não por meio da configuração na classe [AppConfig]:


@ComponentScan(basePackages = { "spring.cors.server.service" })
@Import({ SecurityConfig.class })
public class AppConfig {

    // solicitações entre domínios
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }

    @Autowired
    private DispatcherServlet dispatcherServlet;

    @PostConstruct
    public void init() {
        // o próprio aplicativo processa as solicitações HTTP [OPTIONS]
        dispatcherServlet.setDispatchOptionsRequest(true);
    }
}