Skip to content

18. [Cours]: Gestão de acessos entre domínios

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

Este capítulo afasta-se um pouco do TD. Foi mantido porque introduz a programação web e a programação em JavaScript. É importante recordar aqui que um dos objetivos deste TD é apresentar os conceitos frequentemente utilizados no desenvolvimento JEE, ou seja, o desenvolvimento web baseado em frameworks Java. Completa-se aqui o servidor web utilizado no estudo da base de dados de produtos e categorias, para que este possa aceitar pedidos 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 da aplicação Angular provêm 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 a aplicação Angular, porque constitui uma falha de segurança. A aplicação só pode consultar o servidor de onde provém, ou seja, o servidor [1];

Na verdade, não é correto dizer que o navegador impede a aplicação Angular de consultar o servidor [2]. Na realidade, a aplicação consulta-o para perguntar se este autoriza um cliente que não provém do seu próprio domínio a consultá-lo. A esta técnica de partilha chama-se CORS (Cross-Origin Resource Sharing). O servidor [2] dá o seu consentimento enviando cabeçalhos HTTP específicos.

Vamos criar a seguinte arquitetura:

  • em [1], uma aplicação web fornece 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 encontram-se 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 ficheiro [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 dos seus métodos;
  • linhas 18-36: já nos deparámos com estes beans, por exemplo, no parágrafo 13.5.3.1. Note-se, 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 pedido de um recurso com a extensão .html terá como resposta o ficheiro solicitado pelo pedido e encontrado na pasta [static] do Classpath do projeto;
  • linha 41: qualquer pedido de um recurso com a extensão .js terá como resposta o ficheiro JavaScript solicitado pelo pedido 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:

 

Esta página incluirá código JavaScript (jS) executado no navegador. Vamos apresentar alguns conceitos básicos de JavaScript que nos permitirão compreender o código. O cliente irá efetuar chamadas HTTP utilizando a biblioteca jQuery [https://jquery.com/], que disponibiliza inúmeras funções que facilitam o desenvolvimento em JavaScript. Criamos um ficheiro estático HTML [jQuery.html] que colocamos na pasta [static]:

 

Este ficheiro 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 experimentar com este elemento.

Temos de descarregar o ficheiro [jquery-2.1.3.min.js]. A versão mais recente do jQuery encontra-se no URL [http://jquery.com/download/]:

Image

Colocaremos o ficheiro descarregado na pasta [static / js] e alteraremos a linha 6 do ficheiro HTML de acordo com a versão instalada.

Feito isto, acedemos à vista estática [jQuery.html] com o Chrome [1-2]:

No Google Chrome, execute o comando [Ctrl-Maj-I] para aceder às ferramentas de desenvolvimento [3]. O separador [Console] [4] permite executar código JavaScript. Apresentamos a seguir alguns comandos JavaScript a introduzir, acompanhados de uma explicação.

JS
résultat
$("#element1")
: torna a coleção de todos os elementos com o id [element1],
ou seja, normalmente uma coleção de 0 ou 1 elemento
porque não é possível ter dois IDs idênticos numa página HTML.
$("#element1").text("blabla")
: atribui o texto [blabla] a todos os elementos
da coleção. Isto tem como efeito alterar o
conteúdo exibido pela página
$("#element1").hide()
oculta os elementos da coleção.
O texto [blabla] já não é apresentado.
$("#element1")
: volta a exibir a coleção. Isto permite-nos
permite ver que o elemento com o 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] volta a aparecer. É o atributo
CSS style='display: block;' que garante esta
exibição.
$("#element1").attr('style','color: red')
: define um atributo para todos os elementos da
coleção. O atributo é, neste caso, [style] e o seu valor
[color: red]. O texto [blabla] fica a vermelho.
Tableau
Dictionnaire

Note-se que o URL do navegador não se alterou durante todas estas operações. Não houve qualquer comunicação com o servidor web. Tudo decorre no interior do navegador. Agora, vamos visualizar o código-fonte da página:

Este é o texto inicial. Não reflete de forma alguma as alterações que fizemos no elemento nas linhas 10 a 12. É importante ter isto em conta ao depurar JavaScript. Por isso, muitas vezes é inútil visualizar o código-fonte da página apresentada.

18.6. O código JavaScript da aplicação

Voltemos à página da aplicação 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="" />
        <!--  palavra-passe -->
        <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 validaçã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: importa-se 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 estes componentes através destes 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() {
    // recuperam-se 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) {
    // efetua-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á a ser efetuada uma chamada Ajax manualmente
    $.ajax({
        headers : {
            'Autorização':'Basic '+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() {
    // recuperam-se 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 no final do carregamento do documento no navegador;
  • linhas 81-86: recuperam-se as referências dos diferentes elementos do documento HTML, através do seu identificador [id];
  • linhas 2-9: variáveis globais conhecidas em todas as funções definidas no ficheiro jS;
  • linha 13: recupera-se o valor URL introduzido pelo utilizador;
  • linha 14: recupera-se o valor que o utilizador pretende enviar (vazio se for a operação GET);
  • linha 15: recupera-se o identificador introduzido pelo utilizador;
  • linha 16: recupera-se a sua palavra-passe;
  • linha 17: recupera-se o método [get] ou [post] a utilizar para solicitar o URL da linha 9:
    • [document] designa o documento carregado pelo navegador, o que se denomina DOM (Document Object Model),
    • [document.forms[0]] designa o primeiro formulário do documento, podendo um documento conter vários. Neste caso, existe apenas um,
    • [document.forms[0].elements['method']] designa o 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']. Sabe-se que o valor enviado é o valor do atributo [value] do botão de opção selecionado. Neste caso, será, portanto, uma das cadeias ['get', 'post'];
  • linha 18: constrói-se a codificação Base74 da cadeia «identifiant:password». Esta cadeia codificada será utilizada no cabeçalho HTTP [Authorization] que iremos enviar ao servidor para autenticar o pedido;
  • linhas 22-26: dependendo do método HTTP a utilizar, executa-se o método [doGet] ou [doPost];
  • o método jQuery [$.ajax] efetua uma chamada ao método HTTP;
  • linhas 32-34: contacta-se um servidor que exige um cabeçalho HTTP ou [Authorization: Basic code];
  • linha 35: o utilizador irá introduzir URL do tipo [/cors-getAllCategories,/cors-addProduits, ...]. Por isso, é necessário completar estes URL com o URL do servidor da linha 6;
  • linha 36: método HTTP a utilizar;
  • linha 37: o servidor devolve o jSON. Indica-se o tipo [text] como tipo de resultado, para que seja apresentado tal 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 enviar;
  • linha 61: para indicar que o valor enviado será na forma de uma cadeia jSON;

18.7. Execução do cliente

A aplicação cliente é uma aplicação Spring Boot iniciada 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 ficheiro de configuração [WebConfig]. A página [client.html] será implementada no servidor Tomcat presente no Classpath do projeto;

18.8. O URL [/getAllCategories]

Estamos a lançar:

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

depois solicitamos o URL [http://localhost:8081/client.html] [1]:

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

Não obtemos resposta do servidor. Ao consultar a consola de desenvolvimento do Chrome (Ctrl+Shift+I), descobrimos um erro:

  • em [1], estamos no separador [Network];
  • em [2], verifica-se que a solicitação HTTP que foi efetuada não é [GET], mas sim [OPTIONS]. No caso de um pedido entre domínios, o navegador verifica junto do servidor se um determinado número de condições está preenchido, enviando-lhe um pedido HTTP [OPTIONS]. Neste caso, as solicitações são as 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 é aceite;
  • em [6], o navegador envia o cabeçalho HTTP [Origin: http://localhost:8081]. Este cabeçalho solicita uma resposta num cabeçalho HTTP [Access-Control-Allow-Origin] indicando que a origem indicada é aceite;
  • em [7], o navegador pergunta se os cabeçalhos HTTP, [accept] e [authorization] são aceites. O cabeçalho do pedido [Access-Control-Request-Headers] aguarda uma resposta com um cabeçalho HTTP [Access-Control-Allow-Headers] indicando que os cabeçalhos solicitados são aceites;
  • ocorre um erro em [3]. Ao clicar no ícone, surge 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 do pedido é aceite;
  • em [8], verifica-se que o servidor efetivamente não enviou esse cabeçalho. Consequentemente, o navegador recusou-se a efetuar a solicitação HTTP GET inicialmente solicitada;

Temos de alterar 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>

    <!-- plugins -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • linhas 23-27: aproveitamos todo o trabalho realizado até agora, recorrendo ao 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 procurados no pacote [spring.cors.server.service];
  • linhas 16-19: criamos um componente Spring denominado [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:

 

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 para o 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);
        // autorizam-se 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 concebida 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 cadeia [origin] presente nos cabeçalhos HTTP e [Origin] das solicitações entre domínios:
Origin:http://localhost:8081

Neste 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], providenciar-se-á para que se tenha [origin==null];

  • (continuação)
    • o objeto [HttpServletResponse response] que será devolvido ao cliente que efetuou o pedido;

Estes dois parâmetros são inseridos pelo Spring;

  • linhas 15-175: se a aplicação estiver configurada para aceitar pedidos 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 aceitar-se-á a solicitação entre domínios; caso contrário, rejeitar-se-á;
  • 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: indicámos dois cabeçalhos HTTP específicos na solicitação HTTP [OPTIONS]:
Access-Control-Request-Method: GET
Access-Control-Request-Headers: accept, authorization

Em resposta aos 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-23 limitam-se a repetir o pedido do cliente para indicar que este foi aceite;

18.9.4. O controlador [MyControllerWithHttpOptions]

Para não ter de alterar o servidor web não seguro / jSON [intro-server-webjson-01] analisado no parágrafo 13.5.3, vamos criar um novo controlador que, nos casos em que o servidor não seguro processa o URL [/url], o novo controlador processará os URL e [/cors-url], e este URL aceitará os pedidos entre domínios.

A classe [MyControllerWithHttpOptions] é o controlador que irá processar os pedidos 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 acabámos de descrever;
  • linhas 17-18: o método [getAllCategories] (linha 18) processa o URL ["/cors-getAllCategories"] quando é solicitado 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 as solicitações entre domínios. O método [setHeaders] está definido na classe pai [AbstractCorsController];

O mesmo se aplica a todos os URL expostos pelo servidor web / jSON não seguro [intro-server-webjson-01] analisado no parágrafo 13.5.3. Quando este serviço expõe o URL e o [/url], a classe [MyControllerWithHttpOptions] acima referida expõe o URL e o [/cors-url].

18.9.5. O controlador [MyControllerWithCors]

 

A classe [MyControllerWithCors] é o controlador que irá 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: 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 através do método HTTP [GET];
  • linha 26: o resultado do método [getAllCategories] será enviado ao cliente. Este 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 acabámos 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 a sua resposta com os cabeçalhos necessários para as solicitações entre domínios.

Isto é 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 este serviço expõe o URL e o [/url], a classe [MyControllerWithCors] acima referida expõe o URL e o [/cors-url].

Uma solicitação entre domínios decorrerá da seguinte forma:

  • o código JS do cliente solicita oURL e [/cors-url] com uma solicitação HTTP, GET ou POST;
  • o navegador que executa este código intercepta este pedido 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 interdomínios esperados pelo navegador;
  • o navegador solicita então a resposta inicial URL ([/cors-url]) com uma solicitação HTTP, GET ou POST;
  • um dos métodos do controlador [MyControllerWithCors] responde-lhe então;

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 esta configuração, o servidor Tomcat incorporado nos arquivos do projeto é executado e a aplicação web [intro-spring-cors-server-jpa] é implementada nele. A aplicação web do servidor não seguro [intro-server-webjson-01], que faz parte dos arquivos do projeto, também é implementada nesse servidor. Como o projeto [intro-spring-security-server-01] também faz parte dos arquivos, acabam por ser expostos dois tipos de URL:
    • as do serviço web seguro: /url;
    • as do serviço web que aceita pedidos entre domínios: /cors-url;

Estamos agora 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 consola, esta nunca é 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 predefinição, o Spring MVC trata ele próprio os comandos HTTP e [OPTIONS]. Assim, é sempre o Spring que responde e nunca o método [getAllCategories] da linha 5 acima. Este comportamento por predefiniçã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 {

    // pedidos entre domínios
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }

    @Autowired
    private DispatcherServlet dispatcherServlet;

    @PostConstruct
    public void init() {
        // a própria aplicação processa os pedidos HTTP [OPTIONS]
        dispatcherServlet.setDispatchOptionsRequest(true);
    }
}
  • linhas 25-26: injeção do bean [dispatcherServlet], que gere os pedidos dos clientes. Este 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] tiver sido instanciada e as injeções do Spring tiverem sido realizadas. Assim, quando 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 ela própria os comandos HTTP e [OPTIONS];

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

  • em [1], verificamos que existem 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 acabámos de configurar na resposta do servidor;

Vamos agora analisar a segunda solicitação:

  • em [1], a solicitação analisada;
  • em [2], trata-se da solicitação GET. Graças à primeira solicitação [OPTIONS], o navegador recebeu as informações que solicitava. Agora, efetua a solicitação [GET] inicialmente solicitada;
  • 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]. Por isso, deveríamos ter o documento solicitado. É possível que o servidor tenha efetivamente enviado o documento, mas que seja o navegador a impedir a sua utilização porque exige que, também para o pedido GET, a resposta inclua o cabeçalho HTTP [Access-Control-Allow-Origin:http://localhost:8081].

Alteramos, então, o controlador [MyControllerWithCors] para que também envie os cabeçalhos necessários às 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 esta alteração, os resultados são os seguintes:

Conseguimos, de facto, obter a lista de categorias.

18.10. Os outros 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]

Analisemos o seguinte caso:

  • faz-se um POST [1] para o URL [2];
  • no [3], o valor lançado. Trata-se de uma cadeia jSON;
  • no total, pretendemos criar uma categoria denominada [categorie2];

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

  • em [1], tal como nas solicitações [GET], o navegador efetua uma solicitação [OPTIONS];
  • em [2], solicita uma autorização de acesso para uma solicitação [POST]. Anteriormente, era [GET];
  • em [3], solicita autorização para enviar os cabeçalhos HTTP e [accept, authorization, content-type]. Anteriormente, existiam 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];

Alteramos o método [AbstractController.sendHeaders] da seguinte forma:


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);
        // autorizam-se determinados cabeçalhos
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
        // autorizam-se o GET e o POST
        response.addHeader("Access-Control-Allow-Methods", "GET, POST");
    }
}
  • linha 21: adicionámos o cabeçalho HTTP [Content-Type] (não importa se é maiúsculo ou minúsculo);
  • linha 23: adicionámos o método HTTP [POST];

Com isto, 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 efetivamente adicionada à base de dados. O SGBD atribuiu-lhe a chave primária 1729.

18.12. O controlador [AuthenticateCorsController]

  

O controlador [AuthenticateCorsController] existe para fornecer oURL [/cors-authenticate], que permite chamar o URL [/authenticate] já existente, com uma consulta entre domínios. O 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);
    }

}

Eis dois exemplos:

  • As respostas apresentadas são geradas pelo código jS, conforme se segue:

function doGet(url) {
    // faz-se 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] é apresentada pela linha 14 da função [success];
  • a resposta [2] é apresentada 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 que ocorreu. Este objeto fornece poucas informações. É possível utilizar outros métodos do objeto [jqXHR] para obter, por exemplo, os cabeçalhos HTTP devolvidos pelo servidor;

18.13. Conclusion

A nossa aplicação suporta agora pedidos entre domínios. Estes podem ser autorizados ou não através 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() {
        // a própria aplicação processa os pedidos HTTP [OPTIONS]
        dispatcherServlet.setDispatchOptionsRequest(true);
    }
}