pom.xml
Tendremos que añadir las siguientes properties, dependencias, reposiorios y plugins en el pom.xml:
<properties>
<java.version>11</java.version>
<log4j2.version>2.17.2</log4j2.version>
<springdoc.version>1.6.6</springdoc.version>
<fdwjs.version>[1.0.0,)</fdwjs.version>
<spring.cloud.version>2021.0.3</spring.cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Servicios web Java 11 -->
<dependency>
<groupId>com.sun.xml.ws</groupId>
<artifactId>jaxws-ri</artifactId>
<version>2.3.3</version>
<type>pom</type>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>5.4.12.Final</version>
<scope>provided</scope>
</dependency>
<!-- Real DB -->
<dependency>
<groupId>com.oracle</groupId>
<artifactId>jdbc.driver</artifactId>
<version>11.2.0.3.0</version>
</dependency>
<!-- TOMCAT -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- SECURITY -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- TEST -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Test DB -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<!-- Selenium -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<!-- <version>3.141.59</version> -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- Log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-layout-template-json</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- API DOC -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-webmvc-core</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-hateoas</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-data-rest</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-security</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- FUNDEWEBJS -->
<!-- Servicios RRHH Client -->
<dependency>
<groupId>es.um.atica.fundewebjs.fundewebjs-api</groupId>
<artifactId>fundewebjs-serviciosrrhh-client</artifactId>
<version>${fdwjs.version}</version>
</dependency>
<!-- LAGAR -->
<dependency>
<groupId>es.um.atica.fundewebjs.fundewebjs-api</groupId>
<artifactId>fundewebjs-lagar</artifactId>
<version>${fdwjs.version}</version>
</dependency>
<!-- OpenTracing -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Componentes API -->
<dependency>
<groupId>es.um.atica.fundewebjs.fundewebjs-api</groupId>
<artifactId>fundewebjs-api-components</artifactId>
<version>${fdwjs.version}</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>archiva.atica.umu.es</id>
<name>ATICA - UMU Repository</name>
<url>https://archiva.um.es/archiva/repository/FundeWeb/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>fundewebjs.archiva.atica.umu.es</id>
<name>ATICA - UMU Repository - FundeWebJS</name>
<url>https://archiva.um.es/archiva/repository/FundeWebJS/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository> </repositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.hibernate.orm.tooling</groupId>
<artifactId>hibernate-enhance-maven-plugin</artifactId>
<version>${hibernate.version}</version>
<executions>
<execution>
<configuration>
<failOnError>true</failOnError>
<enableLazyInitialization>true</enableLazyInitialization>
<enableDirtyTracking>true</enableDirtyTracking>
<enableAssociationManagement>true</enableAssociationManagement>
<enableExtendedEnhancement>false</enableExtendedEnhancement>
</configuration>
<goals>
<goal>enhance</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- JaCoCo configuration -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
application.properties
Dentro del fichero de application.properties NO se permiten caracteres especiales como acentos, Ñ, etc. Esto causará que el proyecto no compile en local dando un error igual o similar a
Como hemos visto en el apartado anterior, en el archivo application.properties incluiremos algunas variables de configuración del proyecto:
#### Ruta oauth
app.author=[GRUPO]
app.apiname=[SERVICIO]
app.server.path=/${app.author}/${app.apiname}
#### Entorno
app.env=desa
#### Accesslog
# here we say that we want to enable accesslog
server.tomcat.accesslog.enabled=true
# it is important to understand what the options means:
# 'directory/prefix + suffix + file-date-format' will be
# the file that tomcat will try to open.
# /dev/stdout is standard output, so we want tomcat
# to write to that fd. Then, we need to play with
# directory, prefix, suffix and file-date-format to match our desired path.
server.tomcat.accesslog.directory=/dev
server.tomcat.accesslog.prefix=stdout
server.tomcat.accesslog.buffered=false
# Don't use empty double quotes, see below
server.tomcat.accesslog.suffix=
server.tomcat.accesslog.file-date-format=
server.tomcat.accesslog.pattern=%h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" "%{X-Forwarded-For}i" %D "%{UMU-Request-UUID}o"
#### Actuator
management.endpoints.enabled-by-default=false
management.endpoint.health.enabled=true
management.health.ping.enabled=true
management.endpoint.health.group.liveness.include=livenessstate,ping
management.endpoint.health.group.readiness.include=readinessstate,ping,db
management.endpoint.health.group.check.include=readinessstate,dependencies,db
management.endpoint.health.group.check.show-details=always
#### Logs
# FundeWebJS Habilitar filtro para cabeceras en logs MDC de lagar
fdwjs.api.lagar.enable=true
fdwjs.api.lagar.headers=UMU-User-UUID,UMU-Client-APP,UMU-User-SessionID
# FundeWebJS Habilitar filtro para loggear request/response
fdwjs.api.logging.enable=false
fdwjs.api.logging.client.enable=true
fdwjs.api.logging.query.enable=true
fdwjs.api.logging.headers.enable=true
fdwjs.api.logging.payload.enable=true
fdwjs.api.logging.payload.maxLength=6400
fdwjs.api.logging.url-patterns=*
# FundeWebJS Habilitar endpoint jsn.logger
fdwjs.api.frontLogger.enable=false
#### SSO URL
cas.url=https://entrada.test.um.es
client.id=basicbackend
server.scopes=SCOPE_micampus
#### SpringDoc OpenAPI Documentation
springdoc.api-docs.path=/api-docs
# Propiedades inventadas para reubicar las peticiones
springdoc.server.url=https://apidesa.um.es
springdoc.server.path=/${app.author}/${app.apiname}
springdoc.security.issuer-uri=${cas.url}/cas/oidc
#### Spring Security Resource Server
spring.security.oauth2.resourceserver.jwt.jws-algorithm=RS512
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=${cas.url}/cas/oidc/jwks
#spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=${cas.url}/cas/oidc/introspect
#spring.security.oauth2.resourceserver.opaquetoken.client-id=plain_user
#spring.security.oauth2.resourceserver.opaquetoken.client-secret=${client.secret}
#### Base de datos
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.Oracle10gDialect
spring.jpa.generate-ddl=false
spring.jpa.hibernate.ddl-auto = validate
spring.datasource.url=jdbc:oracle:thin:@hydra-prescan.atica.um.es:1526/ZEUSDESA
spring.datasource.username=USER
spring.datasource.password=PASSWORD
De aquí, los valores que tendremos que modficar son los siguiente:
- app.author: será el nombre del grupo. Por ejemplo, "acade".
- app.apiname: el nombre del servicio. Por ejemplo, "resguardomatricula-api".
Estas dos forman la ruta base del servicio. Con estos ejemplos, sería /acade/resguardomatricula-api.
Además:
- app.env: entorno de la aplicación, puede ser "desa", "test" o "env".
- Credenciales de base de datos: son spring.datasource.url, spring.datasource.username y spring.datasource.password. Pondremos lo correspondiente a nuestra aplicación. Hay que tener en cuenta que estas credenciales nunca deben subirse a GitLab.
log4j2.xml
Debemos crear el fichero log4j2.xml en la carpeta /src/main/resources con el siguiente contenido:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorinterval="60" >
<Properties>
<Property name="LOG_EXCEPTION_CONVERSION_WORD">%xwEx</Property>
<Property name="LOG_LEVEL_PATTERN">%5p</Property>
<Property name="LOG_DATEFORMAT_PATTERN">dd-MM-yyyy HH:mm:ss.SSS</Property>
<Property name="CONSOLE_LOG_PATTERN">%d{${LOG_DATEFORMAT_PATTERN}} ${LOG_LEVEL_PATTERN} ${sys:PID} --- [%t] %-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
</Properties>
<Appenders>
<Console name="local" target="SYSTEM_OUT" follow="true">
<PatternLayout pattern="${sys:CONSOLE_LOG_PATTERN}" />
</Console>
<Console name="json" target="SYSTEM_OUT" follow="true">
<JsonTemplateLayout eventTemplateUri="classpath:LogstashJsonEventLayoutV1.json" locationInfoEnabled="true" stackTraceEnabled="true"/>
</Console>
</Appenders>
<Loggers>
<Logger name="org.apache.catalina.startup.DigesterFactory" level="error" />
<Logger name="org.apache.catalina.util.LifecycleBase" level="error" />
<Logger name="org.apache.coyote.http11.Http11NioProtocol" level="warn" />
<logger name="org.apache.sshd.common.util.SecurityUtils" level="warn"/>
<Logger name="org.apache.tomcat.util.net.NioSelectorPool" level="warn" />
<Logger name="org.eclipse.jetty.util.component.AbstractLifeCycle" level="error" />
<Logger name="org.hibernate.validator.internal.util.Version" level="warn" />
<logger name="org.springframework.boot.actuate.endpoint.jmx" level="warn"/>
<Root level="debug">
<AppenderRef ref="local" />
</Root>
</Loggers>
</Configuration>
Este es un archivo de configuración para los logs, que vienen explicados más detalladamente en esta página.
Clase inicial de la aplicación
En la clase que inicia la aplicación, que será algo como NombreApplication.java, sustituyendo Nombre por el de nuestra aplicación, tendremos que añadir a la notación SpringBootApplication el atributo scanBasePackages, con el contenido siguiente:
@SpringBootApplication( scanBasePackages = {
"es.um.atica.[PAQUETE_APPLICATION]", "es.um.atica.fundewebjs"
} )
Ahí, tenemos que cambiar aplicación por el paquete base de nuestra aplicación.
Con esto, un ejemplo de esta clase inicial podría ser el siguiente:
package es.um.atica.pruebaspose;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication ( scanBasePackages = {
"es.um.atica.[PAQUETE_APPLICATION]", "es.um.atica.fundewebjs"
} )
public class PruebasPoseApplication {
public static void main(String[] args) {
SpringApplication.run(PruebasPoseApplication.class, args);
}
}
Configuración CORS
En el paquete es.um.atica.[APLICACION].config añadiremos una clase CorsConfig, que contendrá lo siguiente:
package es.um.atica.[APLICACION].config;
import java.util.Arrays;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class CorsConfig {
// Filtro CORS
@Bean
CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns( Arrays.asList( "*" ) );
configuration.setAllowedMethods( Arrays.asList( "*" ) );
configuration.setAllowedHeaders( Arrays.asList( "*" ) );
configuration.setAllowCredentials( true );
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration( "/**", configuration );
return source;
}
}
¡Ojo! En versiones anteriores de Spring Boot (anteriores a la 2.4.0), en el filtro del CORS, en vez de:
configuration.setAllowedOriginPatterns( Arrays.asList( "*" ) );
Utilizábamos:
configuration.setAllowedOrigins(Arrays.asList("*"));
Pero tras los cambios, con setAllowedOrigins nos salta el siguiente error:
2021-01-08 10:21:50.656 ERROR 10568 --- [ XNIO-1 task-1] io.undertow.request : UT005023: Exception handling request to /loquesea java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value "*"since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.
Así que en función de la versión, podemos encontrarnos algún error, por lo que debemos tener esto en cuenta.
Configuración de seguridad
¡Ojo! Esto será ligeramente diferente según la versión de Spring Boot que tengamos. Nos haremos una clase java para especificar la configuración de seguridad web, que estará también en es.um.atica.[APLICACION].config, y que se llamará SecurityConfig. Contendrá lo siguiente (en función de la versión de Spring Boot):
Versión 2.7.0 y posteriores
Desde la versión 2.7.0, WebSecurityConfigurerAdapter queda obsoleto, por lo que la clase será así:
package es.um.atica.[APLICACION].config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain;
import es.um.atica.fundewebjs.api.filters.FundeWebJSLoggingAuthorizationFilter; @Configuration
public class SecurityConfig {
@Value( "${server.scopes}" )
private String[] serverScopes;
@Value( "${app.server.path}" )
private String apiPath;
@Bean
public FundeWebJSLoggingAuthorizationFilter loggingFilterBean() {
return new FundeWebJSLoggingAuthorizationFilter();
}
/**
* Proveedor de gestion de acceoss
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.requestMatchers().antMatchers( "/public/**" ).and().requestMatchers().antMatchers( apiPath + "/**" ).and()
.sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS )
// configuro politica de sesion sin estado
.and().cors() // Aniado configuracion CORS por defecto
.and().csrf().disable().authorizeRequests().mvcMatchers( apiPath + "/public/**" ).permitAll()
.mvcMatchers( apiPath + "/**" ).hasAnyAuthority( serverScopes ).anyRequest().authenticated().and()
.addFilterAfter( loggingFilterBean(), BearerTokenAuthenticationFilter.class )
.oauth2ResourceServer().jwt();
return http.build();
}
}
Versión 2.6.11 y anteriores
package es.um.atica.[APLICACION].config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import es.um.atica.fundewebjs.api.filters.FundeWebJSLoggingAuthorizationFilter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Value( "${server.scopes}" )
private String[] serverScopes;
@Value( "${app.server.path}" )
private String apiPath;
@Bean
public FundeWebJSLoggingAuthorizationFilter loggingFilterBean() {
return new FundeWebJSLoggingAuthorizationFilter();
}
/**
* Proveedor de gestion de acceoss
*/
@Override
protected void configure( HttpSecurity http ) throws Exception {
http.requestMatchers().antMatchers( "/public/**" ).and().requestMatchers().antMatchers( apiPath + "/**" ).and()
.sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS )
// configuro politica de sesion sin estado
.and().cors() // Aniado configuracion CORS por defecto
.and().csrf().disable().authorizeRequests().mvcMatchers( apiPath + "/public/**" ).permitAll()
.mvcMatchers( apiPath + "/**" ).hasAnyAuthority( serverScopes ).anyRequest().authenticated().and()
.addFilterAfter( loggingFilterBean(), BearerTokenAuthenticationFilter.class )
.oauth2ResourceServer().jwt();
}
}
Si se desea securizar nuestra API con un nivel de seguridad LoA mínimo, diríjase a la página Configuración de seguridad LoA con Oauth.
Utilizar https en local: generar certificado autofirmado
En vez de generar un certificado nuevo, podemos utilizar los que vienen aquí: certs.rar
Contiene keystore.p12, que es el que utilizaremos en el backend, y los dos .pem para el frontend.
Añadir certificado a nuestra aplicación
En application.properties debemos añadir lo siguiente (cambiando el valor de server.ssl.key-store por la ruta en nuestra máquina):
# The format used for the keystore. It could be set to JKS in case it is a JKS file server.ssl.key-store-type=PKCS12 # The path to the keystore containing the certificate server.ssl.key-store=C:/Users/guillermo.castillo/keystore.p12 # The password used to generate the keystore server.ssl.key-store-password=password # The password used to generate the certificate server.ssl.key-password=password # The alias mapped to the certificate server.ssl.key-alias=tomcat
