18. [Cours]: Administración de accesos entre dominios
Palabras clave: CORS (Cross-Origin Resource Sharing).
Este capítulo se aleja un poco del TD. Se ha mantenido porque introduce la programación web y la programación en JavaScript. Cabe recordar aquí que uno de los objetivos de este TD es presentar los conceptos que se utilizan con frecuencia en el desarrollo JEE, es decir, el desarrollo web basado en marcos de trabajo de Java. Aquí se complementa el servidor web utilizado en el estudio de la base de datos de productos y categorías para permitirle aceptar solicitudes entre dominios.
En el documento [Tutoriel AngularJS / Spring 4], se desarrolla una aplicación cliente/servidor en la que el cliente es una aplicación AngularJS:
![]() |
- las páginas HTML / CSS / JS de la aplicación Angular provienen del servidor [1];
- en [2], el servicio [dao] realiza una solicitud a otro servidor, el servidor [2]. Pues bien, esto está prohibido por el navegador que ejecuta la aplicación Angular porque constituye una falla de seguridad. La aplicación solo puede consultar al servidor del que proviene, es decir, el servidor [1];
De hecho, no es correcto decir que el navegador le prohíbe a la aplicación Angular consultar al servidor [2]. En realidad, la aplicación consulta al servidor para preguntarle si permite que un cliente que no proviene de su propio dominio lo consulte. A esta técnica de intercambio se le llama CORS (Cross-Origin Resource Sharing). El servidor [2] da su consentimiento al enviar encabezados HTTP específicos.
Vamos a crear la siguiente arquitectura:
![]() |
- en [1], una aplicación web entrega páginas HTML / jS;
- en [2], el navegador ejecuta el JavaScript integrado en las páginas HTML para consultar el servicio web seguro [3];
18.1. Support
![]() |
Los proyectos de este capítulo se encuentran en la carpeta [support / chap-18].
18.2. El proyecto del cliente
Creamos el siguiente proyecto de Eclipse:
![]() |
18.3. Configuración de Maven
El proyecto es un proyecto de Maven con el siguiente archivo [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>
- líneas 11-15: es un proyecto de Spring Boot;
- líneas 23-26: se utiliza la dependencia [spring-boot-starter-web], que incluye un servidor Tomcat y Spring MVC;
18.4. Configuración de Spring
![]() |
La clase [WebConfig] que configura el proyecto Spring es la siguiente:
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 {
// -------------------------------- configuración de la capa [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/");
}
}
- línea 15: la clase configura un proyecto Spring MVC;
- línea 16: la clase extiende la clase [WebMvcConfigurerAdapter] para redefinir algunos de sus métodos;
- líneas 18-36: ya hemos visto estos beans, por ejemplo, en el párrafo 13.5.3.1. Cabe destacar, en la línea 35, que el servicio web funcionará en el puerto 8081;
- líneas 38-42: el método [addResourceHandlers] permite definir recursos estáticos, es decir, recursos que no son procesados por el método [DispatcherServlet] de la línea 23;
- línea 40: cualquier solicitud de un recurso con la extensión .html recibirá como respuesta el archivo solicitado en la solicitud y que se encuentre en la carpeta [static] del Classpath del proyecto;
- línea 41: cualquier solicitud de un recurso con la extensión .js recibirá como respuesta el archivo JavaScript solicitado en la solicitud y que se encuentre en la carpeta [static/js] del Classpath del proyecto;
![]() |
18.5. Conceptos básicos de jQuery y JavaScript
La página HTML del cliente será la siguiente:
![]() |
Incluirá código de JavaScript (jS) que se ejecutará en el navegador. Vamos a presentar algunos conceptos básicos de JavaScript que nos permitirán entender el código. El cliente realizará llamadas a HTTP utilizando la biblioteca jQuery [https://jquery.com/], la cual ofrece numerosas funciones que facilitan el desarrollo en JavaScript. Creamos un archivo estático HTML [jQuery.html] que colocamos en la carpeta [static]:
![]() |
Este archivo tendrá el siguiente contenido:
<!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>
- línea 6: importación de jQuery;
- líneas 10-12: un elemento de la página con el ID [element1]. Vamos a trabajar con este elemento.
Necesitamos descargar el archivo [jquery-2.1.3.min.js]. Encontraremos la última versión de jQuery en URL [http://jquery.com/download/]:

Colocaremos el archivo descargado en la carpeta [static / js] y modificaremos la línea 6 del archivo HTML según la versión instalada.
Una vez hecho esto, se solicita la vista estática [jQuery.html] con Chrome [1-2]:
![]() |
Con Google Chrome, ejecuta [Ctrl-Maj-I] para que aparezcan las herramientas de desarrollo [3]. La pestaña [Console] [4] permite ejecutar código JavaScript. A continuación, te presentamos algunos comandos de JavaScript que puedes escribir, junto con una explicación de cada uno.
|
: crea la colección de todos los elementos con el identificador [element1], por lo que normalmente será una colección de 0 o 1 elemento porque no se pueden tener dos identificadores idénticos en una página HTML. | ![]() |
|
: asigna el texto [blabla] a todos los elementos de la colección. Esto tiene como efecto cambiar el contenido que muestra la página | ![]() |
|
oculta los elementos de la colección. El texto [blabla] ya no se muestra. | ![]() |
|
: vuelve a mostrar la colección. Esto nos permite ver que el elemento con id [element1] tiene el atributo CSS style='display: none;' que hace que el elemento quede oculto. | |
|
: muestra los elementos de la colección. El texto [blabla] vuelve a aparecer. Es el atributo CSS style='display: block;' el que garantiza esta visualización. | ![]() |
|
: asigna un atributo a todos los elementos de la colección. El atributo aquí es [style] y su valor [color: red]. El texto [blabla] se vuelve rojo. | ![]() |
![]() | |
![]() |
Cabe señalar que el URL del navegador no ha cambiado durante todas estas operaciones. No ha habido intercambio alguno con el servidor web. Todo ocurre dentro del navegador. Ahora, veamos el código fuente de la página:
![]() | ![]() |
Este es el texto inicial. No refleja en absoluto las modificaciones que hemos realizado en el elemento de las líneas 10 a 12. Es importante recordar esto al depurar JavaScript. Por lo tanto, a menudo es innecesario ver el código fuente de la página que se muestra.
18.6. El código JavaScript de la aplicación
Volvamos a la página de la aplicación cliente que consultará el servicio web / jSON:
![]() |
![]() |
El código HTML de esta página es el siguiente:
<!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="" />
<!-- contraseña -->
<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 ingresado -->
<br /> Chaîne jSON à poster : <input type="text" id="posted"
size="50" />
<!-- botón de validación -->
<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>
- línea 6: importamos la biblioteca jQuery;
- línea 7: se importa un código que vamos a escribir;
- líneas 15, 19, 26, 29, 31: anotamos los identificadores [id] de los componentes de la página. El JavaScript hace referencia a estos componentes mediante estos identificadores;
El código [client.js] es el siguiente:
// datos globales
var url;
var posted;
var response;
var method;
var baseUrl = 'http://localhost:8080';
var identifiant;
var password;
var authorizationHeader;
function requestServer() {
// se recupera la información
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);
// se borra la respuesta anterior
response.text("");
// se realiza una llamada Ajax manualmente
if (method === "get") {
doGet(urlValue);
} else {
doPost(urlValue, postedValue);
}
}
function doGet(url) {
// se realiza una llamada Ajax manualmente
$.ajax({
headers : {
'Autorización: 'Básica '+authorizationCode
},
url : baseUrl + url,
type : 'GET',
dataType : 'text',
beforeSend : function() {
},
success : function(data) {
// resultado de texto
response.text(data);
},
complete : function() {
},
error : function(jqXHR) {
// error del sistema
response.text(JSON.stringify(jqXHR.statusCode()));
}
})
}
function doPost(url, posted) {
// se realiza una llamada Ajax manualmente
$.ajax({
headers : {
'Autorización':'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) {
// error del sistema
response.text(JSON.stringify(jqXHR.statusCode()));
}
})
}
// al cargar el documento
$(document).ready(function() {
// se recuperan las referencias de los componentes de la página
identifiant = $("#identifiant");
password = $("#password");
url = $("#url");
posted = $("#posted");
response = $("#response");
});
- líneas 80-87: del código jS que se ejecuta al finalizar la carga del documento en el navegador;
- líneas 81-86: se recuperan las referencias de los distintos elementos del documento HTML, a través de su identificador [id];
- líneas 2-9: variables globales conocidas en todas las funciones definidas en el archivo jS;
- línea 13: se recupera el valor URL ingresado por el usuario;
- línea 14: se recupera el valor que el usuario desea enviar (vacío si se trata de la operación GET);
- línea 15: se recupera el nombre de usuario ingresado por el usuario;
- línea 16: se recupera su contraseña;
- línea 17: se recupera el formato [get] o [post] que se utilizará para solicitar el URL de la línea 9:
- [document] se refiere al documento cargado por el navegador, lo que se conoce como DOM (Modelo de Objetos de Documento),
- [document.forms[0]] se refiere al primer formulario del documento; un documento puede contener varios. En este caso, solo hay uno,
- [document.forms[0].elements['method']] se refiere al elemento del formulario que tiene el atributo [name='method']. Hay dos:
<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<input type="radio" id="post" name="method" value="post" />POST
- (continuación)
- [document.forms[0].elements['method'].value] es el valor que se enviará para el componente que tiene el atributo [name='method']. Sabemos que el valor enviado es el valor del atributo [value] del botón de opción seleccionado. En este caso, será una de las cadenas ['get', 'post'];
- línea 18: se genera la codificación Base74 de la cadena «identificador:contraseña». Esta cadena codificada se utilizará en el encabezado HTTP [Authorization] que enviaremos al servidor para autenticar la solicitud;
- líneas 22-26: según el método HTTP que se vaya a utilizar, se ejecuta el método [doGet] o [doPost];
- el método jQuery [$.ajax] realiza una llamada a HTTP;
- líneas 32-34: se establece comunicación con un servidor que requiere un encabezado HTTP o [Authorization: Basic code];
- línea 35: el usuario ingresará URL del tipo [/cors-getAllCategories,/cors-addProduits, ...]. Por lo tanto, hay que completar estos URL con el URL del servidor de la línea 6;
- línea 36: se debe utilizar el método HTTP;
- línea 37: el servidor devuelve jSON. Se indica el tipo [text] como tipo de resultado para mostrarlo tal como se recibió;
- línea 42: visualización de la respuesta de texto del servidor;
- líneas 48-49: visualización de cualquier mensaje de error;
- línea 53: el método [doPost] recibe un segundo parámetro, que es el valor que se va a enviar;
- línea 61: para indicar que el valor que se enviará será en forma de cadena jSON;
18.7. Ejecución del cliente
La aplicación cliente es una aplicación Spring Boot iniciada por la siguiente clase ejecutable [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);
}
}
- línea 10: el método [SpringApplication.run] utiliza el archivo de configuración [WebConfig]. La página [client.html] se implementará en el servidor Tomcat presente en el Classpath del proyecto;
18.8. El URL [/getAllCategories]
Estamos lanzando:
- el servidor web/json en el puerto 8080;
- el cliente de este servidor en el puerto 8081;
luego solicitamos el URL [http://localhost:8081/client.html] [1]:
![]() |
- en [2], hacemos un GET sobre el URL [http://localhost:8080/getAllCategories];
No recibimos respuesta del servidor. Al revisar la consola de desarrollo de Chrome (Ctrl-Mayús-I), encontramos un error:
![]() |
- en [1], estamos en la pestaña [Network];
- en [2], vemos que la solicitud HTTP que se realizó no es [GET], sino [OPTIONS]. En el caso de una solicitud entre dominios, el navegador verifica con el servidor que se cumplan ciertas condiciones enviándole una solicitud HTTP [OPTIONS]. En este caso, las solicitudes son las indicadas por los marcadores [5-6];
- en [5], el navegador pregunta si se puede acceder al destino URL mediante un GET. El encabezado de la solicitud [Access-Control-Request-Method] solicita una respuesta con un encabezado HTTP [Access-Control-Allow-Methods] que indique que el método solicitado es aceptado;
- en [6], el navegador envía el encabezado HTTP [Origin: http://localhost:8081]. Esta cabecera solicita una respuesta en una cabecera HTTP [Access-Control-Allow-Origin] que indique que el origen especificado es aceptado;
- en [7], el navegador pregunta si se aceptan los encabezados HTTP, [accept] y [authorization]. El encabezado de la solicitud [Access-Control-Request-Headers] espera una respuesta con un encabezado HTTP [Access-Control-Allow-Headers] que indique que los encabezados solicitados son aceptados;
- se produce un error en [3]. Al hacer clic en el ícono, aparece el error [4];
- en [4], el mensaje indica que el servidor no envió el encabezado HTTP [Access-Control-Allow-Origin], que indica si se acepta el origen de la solicitud;
- en [8], se puede observar que, efectivamente, el servidor no envió este encabezado. Por lo tanto, el navegador se negó a realizar la solicitud HTTP GET solicitada inicialmente;
Debemos modificar el servidor web / jSON.
18.9. El nuevo servicio web / json
Creamos un nuevo proyecto Maven [intro-spring-cors-server-jpa]:
![]() | ![]() |
18.9.1. Configuración de Maven
La configuración de Maven para el nuevo servicio web es la siguiente:
<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>
- líneas 23-27: aprovechamos todo el trabajo realizado hasta ahora basándonos en el archivo del servidor web / json seguro;
18.9.2. Configuración de Spring
La clase de configuración [AppConfig] es la siguiente:
![]() |
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 {
// solicitudes entre dominios
@Bean
public boolean isCorsEnabled() {
return true;
}
}
- línea 10: la clase es una clase de configuración de Spring;
- línea 11: hay que buscar otros componentes de Spring en el paquete [spring.cors.server.service];
- líneas 16-19: creamos un componente de Spring llamado [isCorsEnabled] que indica si se aceptan o no clientes ajenos al dominio del servidor;
18.9.3. La clase [AbstractCorsController]
La clase [AbstractCorsController], que será la clase padre de todos los controladores de esta aplicación:
![]() |
Su código es el siguiente:
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;
// envío de opciones al cliente
public void setHeaders(String origin, HttpServletResponse response) {
// ¿Se permite CORS?
if (!isCorsEnabled || origin == null || !origin.startsWith("http://localhost")) {
return;
}
// se establece el encabezado CORS
response.addHeader("Access-Control-Allow-Origin", origin);
// se permiten ciertos encabezados
response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
// Se permite el GET
response.addHeader("Access-Control-Allow-Methods", "GET");
}
}
- línea 7: la clase [CorsController] es abstracta, ya que está diseñada para ser extendida y no instanciada;
- líneas 13-24: el método [setHeaders] incluye en la respuesta [HttpServletResponse response] (línea 13) enviada al cliente los encabezados HTTP requeridos por las solicitudes entre dominios;
- línea 33: el método [/setHeaders] admite como parámetros:
- la cadena [origin] presente en los encabezados HTTP y [Origin] de las solicitudes entre dominios:
En este caso, el parámetro [origin] de la línea 13 tendría el valor [http://localhost:8081]. En caso de que la solicitud no contenga el encabezado HTTP [Origin], nos aseguraremos de tener [origin==null];
- (continuación)
- el objeto [HttpServletResponse response] que se devolverá al cliente que realizó la solicitud;
Estos dos parámetros son inyectados por Spring;
- líneas 15-175: si la aplicación está configurada para aceptar solicitudes entre dominios y si el emisor ha enviado el encabezado HTTP [Origin] y si este origen comienza con [http://localhost], entonces se aceptará la solicitud entre dominios; de lo contrario, se rechazará;
- línea 19: si el cliente está en el dominio [http://localhost:port], se envía el encabezado HTTP:
lo que significa que el servidor acepta el origen del cliente;
- línea 21: hemos señalado dos encabezados HTTP específicos en la solicitud HTTP [OPTIONS]:
Ante los encabezados HTTP y [Access-Control-Request-X], el servidor responde con los encabezados HTTP y [Access-Control-Allow-X], en los que indica lo que está permitido. Las líneas 20 a 23 simplemente repiten la solicitud del cliente para indicar que ha sido aceptada;
18.9.4. El controlador [MyControllerWithHttpOptions]
Para no tener que modificar el servidor web no seguro / jSON [intro-server-webjson-01] analizado en el párrafo 13.5.3, vamos a crear un nuevo controlador que, en lugar de que el servidor no seguro procese el URL [/url], el nuevo controlador procesará los URL y [/cors-url], y este URL aceptará las solicitudes entre dominios.
La clase [MyControllerWithHttpOptions] es el controlador que procesará las solicitudes HTTP del 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){
// encabezados CORS
setHeaders(origin, httpServletResponse);
}
...
- línea 14: la clase es un controlador Spring MVC;
- línea 15: la clase [MyControllerWithHttpOptions] extiende la clase [AbstractCorsController] que acabamos de describir;
- líneas 17-18: el método [getAllCategories] (línea 18) procesa URL ["/cors-getAllCategories"] cuando se solicita con el método HTTP [OPTIONS];
- línea 18: el método [getAllCategories] admite dos parámetros:
- [@RequestHeader(value = "Origin", required = false) String origin] para recuperar el valor del encabezado HTTP [Origin:http://localhost:8081] cuando esté presente. En este ejemplo, el parámetro [String origin] recibirá el valor [http://localhost:8081]. Este encabezado no es obligatorio [required = false]. Cuando no esté presente, el parámetro [String origin] tendrá el valor null;
- [HttpServletResponse httpServletResponse]: la respuesta que se enviará al cliente;
- línea 21: se envían los encabezados HTTP que permiten las solicitudes entre dominios. El método [setHeaders] se define en la clase padre [AbstractCorsController];
Esto se aplica a todos los URL expuestos por el servidor web / jSON no seguro [intro-server-webjson-01] analizado en el párrafo 13.5.3. Cuando este servicio expone el URL y el [/url], la clase [MyControllerWithHttpOptions] mencionada anteriormente expone el URL y el [/cors-url].
18.9.5. El controlador [MyControllerWithCors]
![]() |
La clase [MyControllerWithCors] es el controlador que procesará las solicitudes HTTP de tipo [GET] y [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 {
// dependencias de 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 {
// respuesta
return myController.getAllCategories();
}
...
- línea 17: la clase [MyControllerWithCors] es un controlador Spring MVC
- línea 18: extiende la clase [AbstractCorsController];
- líneas 21-22: inyección del controlador [MyController] del servidor web / jSON no seguro [intro-server-webjson-01] analizado en el párrafo 13.5.3;
- líneas 25-27: el método [getAllCategories] procesa el URL [/cors-getAllCategories] (línea 28) cuando se solicita mediante el método HTTP [GET];
- línea 26: el resultado del método [getAllCategories] se enviará al cliente. Este resultado es un flujo jSON (atributo [produces] de la línea 27 y tipo [String] del resultado de la línea 25);
- línea 27: el método recibe los mismos parámetros que el método [getAllCategories] del controlador [MyControllerWithHttpOptions] que acabamos de analizar;
- línea 30: se le pide al método [myController.getAllCategories()] que envíe la respuesta;
Al final, es el método [myController.getAllCategories()] del servidor no seguro el que envía la respuesta. Simplemente se ha enriquecido su respuesta con los encabezados necesarios para las solicitudes entre dominios.
Esto se hace así para todos los URL expuestos por el servidor web / jSON no seguro [intro-server-webjson-01] analizado en el párrafo 13.5.3. Cuando este servicio expone el URL y el [/url], la clase [MyControllerWithCors] mencionada anteriormente expone el URL y el [/cors-url].
Una solicitud entre dominios se llevará a cabo de la siguiente manera:
- el código JS del cliente solicita elURL y [/cors-url] mediante una solicitud HTTP, GET o POST;
- el navegador que ejecuta este código intercepta esta solicitud y solicita primero elURL [/cors-url] con una solicitud HTTP OPTIONS para verificar que el servicio web de destino acepte solicitudes entre dominios;
- uno de los métodos del controlador [MyControllerWithHttpOptions] envía los encabezados entre dominios que espera el navegador;
- el navegador solicita entonces la URL inicial [/cors-url] con una solicitud HTTP GET o una POST;
- uno de los métodos del controlador [MyControllerWithCors] le responde;
18.9.6. Pruebas
La clase de inicio del proyecto [intro-spring-cors-server-jpa] es la siguiente:
![]() |
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);
}
}
- línea 10: se ejecuta el método estático [SpringApplication.run] con la configuración de Spring [AppConfig]. Debido a esta configuración, se ejecuta el servidor Tomcat integrado en los archivos del proyecto y se implementa en él la aplicación web [intro-spring-cors-server-jpa]. La aplicación web del servidor no seguro [intro-server-webjson-01], que forma parte de los archivos del proyecto, también se implementa en él. Como el proyecto [intro-spring-security-server-01] también forma parte de los archivos, finalmente se exponen dos tipos de URL:
- las del servicio web seguro: /url;
- las del servicio web que acepta solicitudes entre dominios: /cors-url;
Ahora estamos listos para nuevas pruebas. Lanzamos la nueva versión del servicio web y descubrimos que el problema persiste. No ha cambiado nada. Si en la línea 7 a continuación colocamos una salida de consola, esta nunca se muestra, lo que indica que el método [getAllCategories] de la clase [MyControllerWithHttpOptions] nunca se invoca;
@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) ;
// encabezados CORS
setHeaders(origin, httpServletResponse);
}
Tras investigar un poco, descubrimos que, por defecto, Spring MVC procesa por sí mismo los comandos HTTP y [OPTIONS]. Por lo tanto, siempre es Spring quien responde y nunca el método [getAllCategories] de la línea 5 anterior. Este comportamiento predeterminado de Spring MVC se puede modificar. Modificamos la clase [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 {
// solicitudes entre dominios
@Bean
public boolean isCorsEnabled() {
return true;
}
@Autowired
private DispatcherServlet dispatcherServlet;
@PostConstruct
public void init() {
// la aplicación procesa las solicitudes por sí misma HTTP [OPTIONS]
dispatcherServlet.setDispatchOptionsRequest(true);
}
}
- líneas 25-26: inyección del bean [dispatcherServlet] que gestiona las solicitudes de los clientes. Este bean se definió en la configuración del servidor web / jSON no seguro [intro-server-webjson-01] analizado en el párrafo 13.5.3;
- líneas 28-29: el método [init] (línea 29) se ejecutará tan pronto como se haya instanciado la clase [AppConfig] y se hayan realizado las inyecciones de Spring. Por lo tanto, cuando se ejecuta, el campo de la línea 26 ya se ha inicializado;
- línea 31: se configura el bean [dispatcherServlet] para que permita que la aplicación web procese por sí misma los comandos HTTP y [OPTIONS];
Repetimos las pruebas con esta nueva configuración. Obtenemos el siguiente resultado:
![]() |
- en [1], vemos que hay dos solicitudes HTTP dirigidas a URL y [http://localhost:8080/cors-getAllCategories];
- en [2], la solicitud [OPTIONS];
- en [3], las tres cabeceras HTTP que acabamos de configurar en la respuesta del servidor;
Ahora analicemos la segunda solicitud:
![]() |
- en [1], la solicitud analizada;
- en [2], que es la solicitud GET. Gracias a la primera solicitud [OPTIONS], el navegador recibió la información que solicitaba. Ahora realiza la solicitud [GET] solicitada inicialmente;
- en [3], la respuesta del servidor;
- en [4], el servidor envía jSON;
- en [5], se produjo un error;
- en [6], el mensaje de error;
Es más difícil explicar lo que pasó aquí. La respuesta [3] del servidor es normal: [HTTP/1.1 200 OK]. Por lo tanto, deberíamos tener el documento solicitado. Es posible que el servidor sí haya enviado el documento, pero que sea el navegador el que impida su uso porque exige que, para la solicitud GET también, la respuesta incluya el encabezado HTTP [Access-Control-Allow-Origin:http://localhost:8081].
Por lo tanto, modificamos el controlador [MyControllerWithCors] para que también envíe los encabezados necesarios para las solicitudes entre dominios:
@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 {
// encabezados CORS
setHeaders(origin, httpServletResponse);
// respuesta
return myController.getAllCategories();
}
- línea 6: se incluyen en la respuesta los encabezados necesarios para las solicitudes entre dominios;
Tras esta modificación, los resultados son los siguientes:
![]() |
Sí obtuvimos la lista de categorías.
18.10. Los demás URL [GET]
En los controladores [MyControllerWithCors, MyControllerWithHttpOptions], el código de las acciones que procesan los URL solicitados con un [GET] sigue el modelo de las acciones que procesaron anteriormente el URL y el [/cors-getAllCategories]. El lector puede verificar el código en los ejemplos que se incluyen con este documento. A continuación se muestra un ejemplo para el URL y el [/cors-getAllProduits]:
en [MyControllerWithHttpOptions]
@RequestMapping(value = "/cors-getAllProduits", method = RequestMethod.OPTIONS)
public void getAllProduits(@RequestHeader(value = "Origin", required = false) String origin,
HttpServletResponse httpServletResponse) {
// encabezados CORS
setHeaders(origin, httpServletResponse);
}
en [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 {
// encabezados CORS
setHeaders(origin, httpServletResponse);
// respuesta
return myController.getAllProduits();
}
El resultado obtenido es el siguiente:
![]() |
18.11. Los URL [POST]
Analicemos el siguiente caso:
![]() |
- se realiza un POST [1] hacia el URL [2];
- en [3], el valor publicado. Se trata de una cadena jSON;
- en total, buscamos crear una categoría llamada [categorie2];
Por el momento, no modificamos ningún código. El resultado obtenido es el siguiente:
![]() |
- en [1], al igual que con las solicitudes [GET], el navegador realiza una solicitud [OPTIONS];
- en [2], solicita autorización de acceso para una solicitud [POST]. Anteriormente era [GET];
- en [3], solicita autorización para enviar los encabezados HTTP y [accept, authorization, content-type]. Anteriormente, solo se contaban con los dos primeros encabezados;
- en [4], el servicio web no otorga todas las autorizaciones solicitadas, lo que provoca el error [5];
Modificamos el método [AbstractController.sendHeaders] de la siguiente manera:
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;
// envío de opciones al cliente
public void setHeaders(String origin, HttpServletResponse response) {
// ¿Se permiten los CORS?
if (!isCorsEnabled || origin == null || !origin.startsWith("http://localhost")) {
return;
}
// se establece el encabezado CORS
response.addHeader("Access-Control-Allow-Origin", origin);
// se permiten ciertos encabezados
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// Se permiten GET y POST
response.addHeader("Access-Control-Allow-Methods", "GET, POST");
}
}
- línea 21: se ha agregado el encabezado HTTP [Content-Type] (no importa si se escribe en mayúsculas o minúsculas);
- línea 23: se ha agregado el método HTTP [POST];
Con esto, los métodos [POST] se procesan de la misma manera que las solicitudes [GET]. A continuación se muestra un ejemplo de URL [/cors-addArticles]:
en [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 {
// encabezados CORS
setHeaders(origin, httpServletResponse);
// respuesta
return myController.addCategories(request);
}
en [MyControllerWithHttpOptions]
@RequestMapping(value = "/cors-addCategories", method = RequestMethod.OPTIONS)
public void addCategories(HttpServletRequest request,
@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse httpServletResponse)
throws JsonProcessingException {
// encabezados CORS
setHeaders(origin, httpServletResponse);
}
El resultado obtenido es el siguiente:
![]() |
La categoría [categorie2] se ha agregado correctamente a la base de datos. El SGBD le ha asignado la clave primaria 1729.
18.12. El controlador [AuthenticateCorsController]
![]() |
El controlador [AuthenticateCorsController] está ahí para proporcionar elURL [/cors-authenticate], que permite llamar a la URL [/authenticate] ya existente, mediante una consulta entre dominios. Su código es el siguiente:
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 {
// encabezados CORS
setHeaders(origin, response);
// método de origen
return authenticateController.authenticate();
}
@RequestMapping(value = "/cors-authenticate", method = RequestMethod.OPTIONS)
public void corsAuthenticate(@RequestHeader(value = "Origin", required = false) String origin,
HttpServletResponse response) {
// encabezados CORS
setHeaders(origin, response);
}
}
A continuación se muestran dos ejemplos:
![]() |
- Las respuestas se muestran mediante el siguiente código jS:
function doGet(url) {
// se realiza una llamada Ajax manualmente
$.ajax({
headers : {
'Authorization':'Basic '+authorizationCode
},
url : baseUrl + url,
type : 'GET',
dataType : 'text',
beforeSend : function() {
},
success : function(data) {
// resultado de texto
response.text(data);
},
complete : function() {
},
error : function(jqXHR) {
// error del sistema
response.text(JSON.stringify(jqXHR.statusCode()));
}
})
}
- la respuesta [1] se muestra mediante la línea 14 de la función [success];
- la respuesta [2] se muestra en la línea 20 de la función [error]. La función [JSON.stringify] crea la cadena jSON del objeto [jqXHR.statusCode()], que es el objeto que encapsula el error que se produjo. Este objeto proporciona poca información. Es posible utilizar otros métodos del objeto [jqXHR] para obtener, por ejemplo, los encabezados HTTP devueltos por el servidor;
18.13. Conclusion
Nuestra aplicación ahora admite solicitudes entre dominios. Estas pueden autorizarse o no mediante la configuración en la clase [AppConfig]:
@ComponentScan(basePackages = { "spring.cors.server.service" })
@Import({ SecurityConfig.class })
public class AppConfig {
// solicitudes entre dominios
@Bean
public boolean isCorsEnabled() {
return true;
}
@Autowired
private DispatcherServlet dispatcherServlet;
@PostConstruct
public void init() {
// la aplicación procesa las solicitudes por sí misma HTTP [OPTIONS]
dispatcherServlet.setDispatchOptionsRequest(true);
}
}







































