18. [Cours]: Gestió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. Hay que 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 completa el servidor web utilizado en el estudio de la base de datos de productos y categorías para que pueda 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 proceden del servidor [1];
- en [2], el servicio [dao] realiza una solicitud a otro servidor, el servidor [2]. Pues bien, esto lo prohíbe el navegador que ejecuta la aplicación Angular porque supone un fallo de seguridad. La aplicación solo puede consultar al servidor del que procede, es decir, el servidor [1];
De hecho, no es correcto afirmar que el navegador impide que la aplicación Angular consulte al servidor [2]. En realidad, la aplicación consulta a dicho servidor para preguntarle si autoriza a un cliente que no proviene de su propio dominio a realizar consultas. A esta técnica de intercambio se la denomina CORS (Cross-Origin Resource Sharing). El servidor [2] da su consentimiento enviando unos encabezados HTTP específicos.
Vamos a crear la siguiente arquitectura:
![]() |
- en [1], una aplicación web sirve 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: se trata de un proyecto 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 hereda de la clase [WebMvcConfigurerAdapter] para redefinir algunos de sus métodos;
- líneas 18-36: ya nos hemos encontrado con estos beans, por ejemplo, en el apartado 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 no 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] de la ruta de clases 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] de la ruta de clases del proyecto;
![]() |
18.5. Conceptos básicos de jQuery y de JavaScript
La página HTML del cliente será la siguiente:
![]() |
Incluirá código JavaScript (jS) que se ejecutará en el navegador. Vamos a presentar algunos conceptos básicos de JavaScript que nos permitirán comprender el código. El cliente realizará llamadas HTTP utilizando la biblioteca jQuery [https://jquery.com/], que 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 modificar este elemento.
Tenemos que descargar el archivo [jquery-2.1.3.min.js]. La última versión de jQuery se encuentra 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 indicamos los comandos de JavaScript que debes escribir y te ofrecemos una explicación de cada uno de ellos.
|
: 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 , ya que no pueden existir 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. | ![]() |
|
: establece un atributo para todos los elementos de la colección. El atributo es, en este caso, [style] y su valor [color: red]. El texto [blabla] se muestra en rojo. | ![]() |
![]() | |
![]() |
Cabe destacar que el URL del navegador no ha cambiado durante todas estas operaciones. No ha habido ningún intercambio 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-12. Es importante tenerlo en cuenta a la hora de depurar JavaScript. Por lo tanto, a menudo resulta inútil visualizar el código fuente de la página mostrada.
18.6. El código JavaScript de la aplicación
Volvamos a la página de la aplicación cliente que va a 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 introducido -->
<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: se importa la biblioteca jQuery;
- línea 7: se importa un código que vamos a escribir;
- líneas 15, 19, 26, 29 y 31: anotaremos los identificadores [id] de los componentes de la página. El JavaScript hace referencia a estos componentes a través de dichos 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 : {
'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()));
}
})
}
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 introducido por el usuario;
- línea 14: se recupera el valor que quiere enviar (vacío si se trata de la operación GET);
- línea 15: se recupera el nombre de usuario introducido por el usuario;
- línea 16: se recupera su contraseña;
- línea 17: se recupera el método [get] o [post] que se debe utilizar para solicitar el URL de la línea 9:
- [document] hace referencia al documento cargado por el navegador, lo que se denomina DOM (Document Object Model),
- [document.forms[0]] hace referencia al primer formulario del documento, ya que 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 va a 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 vamos a enviar 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 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 introducirá 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 y como se ha recibido;
- línea 42: visualización de la respuesta de texto del servidor;
- líneas 48-49: visualización del posible 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 enviado se hará 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 va a desplegar en el servidor Tomcat presente en el Classpath del proyecto;
18.8. El URL [/getAllCategories]
Ponemos en marcha:
- el servidor web/json en el puerto 8080;
- el cliente de este servidor en el puerto 8081;
y, a continuación, solicitamos el URL [http://localhost:8081/client.html] [1]:
![]() |
- en [2], realizamos un GET sobre el URL [http://localhost:8080/getAllCategories];
No obtenemos respuesta del servidor. Al consultar la consola de desarrollo de Chrome (Ctrl+Mayús+I), aparece un error:
![]() |
- en [1], estamos en la pestaña [Network];
- en [2], vemos que la solicitud HTTP que se ha realizado no es [GET], sino [OPTIONS]. En el caso de una solicitud entre dominios, el navegador comprueba con el servidor que se cumplen una serie de 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 se acepta el método solicitado;
- 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 se acepta el origen especificado;
- 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 se aceptan los encabezados solicitados;
- se produce un error en [3]. Al hacer clic en el icono, aparece el error [4];
- en [4], el mensaje indica que el servidor no ha enviado el encabezado HTTP [Access-Control-Allow-Origin], que indica si se acepta el origen de la solicitud;
- en [8], se puede comprobar que, efectivamente, el servidor no ha enviado este encabezado. Por lo tanto, el navegador se ha negado a realizar la solicitud HTTP GET solicitada inicialmente;
Tenemos que 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 del 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>
<!-- complementos -->
<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 denominado [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 determinados 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 que sea [origin==null];
- (continuación)
- el objeto [HttpServletResponse response] que se devolverá al cliente que ha realizado 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 por [http://localhost], entonces se aceptará la solicitud entre dominios; de lo contrario, se rechazará;
- línea 19: si el cliente se encuentra 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 concretos en la solicitud HTTP [OPTIONS]:
A la cabecera HTTP [Access-Control-Request-X], el servidor responde con una cabecera HTTP [Access-Control-Allow-X] en la que indica lo que está autorizado. Las líneas 20-23 se limitan a repetir 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 apartado 13.5.3, vamos a crear un nuevo controlador que, en los casos en 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 de 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] hereda de la clase [AbstractCorsController] que acabamos de describir;
- líneas 17-18: el método [getAllCategories] (línea 18) procesa URL ["/cors-getAllCategories"] cuando se le solicita mediante 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 de la cabecera 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 hace así para todos los URL expuestos por el servidor web / jSON no seguro [intro-server-webjson-01] analizado en el apartado 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 apartado 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 solicita 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] estudiado en el apartado 13.5.3. Cuando este servicio expone el URL y el [/url], la clase [MyControllerWithCors] anterior expone el URL y el [/cors-url].
Una solicitud entre dominios se desarrollará 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 comprobar que el servicio web de destino acepta solicitudes entre dominios;
- uno de los métodos del controlador [MyControllerWithHttpOptions] envía los encabezados entre dominios que espera el navegador;
- a continuación, el navegador solicita la respuesta inicial URL ([/cors-url]) con una solicitud HTTP, GET o POST;
- A continuación, 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: el método estático [SpringApplication.run] se ejecuta 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 despliega 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 despliega en él. Dado que 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 admite solicitudes entre dominios: /cors-url;
Ya estamos listos para realizar 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 que aparece a continuación se incluye una salida de consola, esta nunca se muestra, lo que demuestra 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, se descubre que, por defecto, Spring MVC gestiona 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 por defecto 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 propia aplicación gestiona las solicitudes 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 apartado 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 propia aplicación web procese 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;
Veamos ahora la segunda solicitud:
![]() |
- en [1], la solicitud analizada;
- en [2], que es la solicitud GET. Gracias a la primera solicitud [OPTIONS], el navegador ha recibido 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 ha producido un error;
- en [6], el mensaje de error;
Es más difícil explicar lo que ha ocurrido 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 haya enviado correctamente el documento, pero que sea el navegador el que impida su uso porque exige que, para la solicitud GET, la respuesta también incluya el encabezado HTTP [Access-Control-Allow-Origin:http://localhost:8081].
Modificamos entonces 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:
![]() |
Hemos obtenido correctamente 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 las solicitudes URL con un [GET] sigue el modelo de las acciones que procesaron anteriormente el URL y el [/cors-getAllCategories]. El lector puede consultar el código en los ejemplos que se incluyen con este documento. A continuación se muestra un ejemplo para los URL y [/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, queremos 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 tenían los dos primeros encabezados;
- en [4], el servicio web no concede 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 permite el uso de CORS?
if (!isCorsEnabled || origin == null || !origin.startsWith("http://localhost")) {
return;
}
// se establece el encabezado CORS
response.addHeader("Access-Control-Allow-Origin", origin);
// Se autorizan determinados 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 añadido el encabezado HTTP [Content-Type] (no importa si se escribe en mayúsculas o minúsculas);
- línea 23: se ha añadido el método HTTP [POST];
De este modo, los métodos [POST] se tratan de la misma forma que las consultas [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 añadido correctamente a la base de datos. El SGBD le ha asignado la clave primaria 1729.
18.12. El controlador [AuthenticateCorsController]
![]() |
El controlador [AuthenticateCorsController] sirve 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 en 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 ha producido. 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 admite ahora las 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 propia aplicación gestiona las solicitudes HTTP [OPTIONS]
dispatcherServlet.setDispatchOptionsRequest(true);
}
}







































