Tips, Soluciones y Novedades en Tecnología

06/03/2020

Spring Security Rest





Spring Security es muy potente para gestionar la seguridad de nuestras aplicaciones, adopta muchas buenas prácticas tal como podemos ver en su documentación.



La configuración hasta la versión 3.x se realizaba por XML, posteriores versiones se realiza por anoraciones fascilitando su configuración.









Spring Security permite muchas formas de configurar la validación  de acceso, desde LDAP, hasta validación en memoria con fines de pruebas.





En este Post vamos a ver un escenario muy particular sobre como podemos lograr la validación de Spring Security desde un controllador Rest, para no tener que configurar filtros, datasources y otras configuraciones que hacen nuestro código tenga muchas configuraciones, ademas de simplificar la autenticación.





Lo primero que crearemos es la configuración de Spring Security, WebSecurityConfig.java








package com.dev.segurity.apisecurity.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.cors().disable();
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/api/login/**").permitAll()
.antMatchers("/**").authenticated()
.and()
.logout()
.logoutSuccessUrl("/");
}
}




Como podemos ver nuestra configuración de Spring Security es bastante limpia.



Solo permitidos acceso a /api/login para poder hacer la autenticación por esa ruta, habilitamos tambien la ruta /logout para cerrar sesión automaticamente por parte de spring security.





Ahora tenemos que crear 2 DTOs (Data Transfer Object) para gestionar nuestras credenciales y nuestra sesión.



Lo crearemos en el paquete "dto", Credential.java y SessionInfo.java.



Credential.java es para recibir el usuario y clave desde el cliente y SessionInfo.java es para almacenar en la sessión de Spring Security, a esta clase le podemos agregar mas atributos en función a lo que queremos guardar.









Como paso siguiente, tenemos que agregar un nuevo paquete "session" y crear 3 clases Privilege.java, Role.java y SessionManager.java, las 2 primeras es para utilizar como auxiliar en la sesión, tal como podemos ver en la imagen siguiente.











La clase SessionManager.java es la encargada en injectar y obtener la sesión del contexto Spring Security.






package com.dev.segurity.apisecurity.session;

import com.dev.segurity.apisecurity.dto.SessionInfo;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

public class SessionManager {

private static final SessionManager instance = new SessionManager();
public static SessionManager getInstance() {
return instance;
}
public void add(SessionInfo info) {
Role r = new Role();
r.setName("ROLE_ADMIN");
List<Role> roles = new ArrayList<>();
roles.add(r);
Collection<? extends GrantedAuthority> authorities = roles;
Authentication nwahtu = new UsernamePasswordAuthenticationToken(info, info.getUsername(), authorities);
SecurityContextHolder.getContextHolderStrategy().getContext().setAuthentication(nwahtu);
}

public SessionInfo get() {
SessionInfo info = null;
Authentication auth = SecurityContextHolder.getContextHolderStrategy().getContext().getAuthentication();
if (auth != null) {
info = (SessionInfo) auth.getPrincipal();
}
return info;
}
}




Esta clase lo es todo, es el punto de acceso a Spring Security, y podemos gestionar nuestra sesión desde cualquier componente, solo llamando esta clase singleton.





Ahora tenemos que crear dos controllers para probar, uno LoginController.java que nos permita recibir por Rest el usuario y clave, realizar la validación y llamar a SessionManager.java para inyectar la sessión, tal como vemos a continuación.






package com.dev.segurity.apisecurity.controller;

import com.dev.segurity.apisecurity.dto.Credential;
import com.dev.segurity.apisecurity.dto.SessionInfo;
import com.dev.segurity.apisecurity.session.SessionManager;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class LoginController {

@RequestMapping(method = RequestMethod.POST, path = "/login")
public ResponseEntity<Object> login(HttpServletRequest request, @RequestBody Credential dto) {
SessionInfo info = new SessionInfo();
if (dto.getUsuario().equals("admin") && dto.getClave().equals("admin")) {
info.setUsername(dto.getUsuario());
info.setIp(request.getRemoteAddr());
SessionManager.getInstance().add(info);
}
return new ResponseEntity<>(info, HttpStatus.UNAUTHORIZED);
}
}






Como podemos observar se recibió por POST el usuario y clave, se valida y se instancia la clase SessionInfo,java, ademas se agrega la IP y bien se podria en este punto ir a un repositorio y hacer la validación con la base de datos para hacer la validación.





Ahora vamos a ver el controller SessionController.java que solo se encarga de consultar la sesión.






package com.dev.segurity.apisecurity.controller;

import com.dev.segurity.apisecurity.session.SessionManager;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class SessionController {
@RequestMapping(path = "/info")
public ResponseEntity<Object> login() {
return new ResponseEntity<>(SessionManager.getInstance().get(), HttpStatus.OK);
}
}






Este servicio solo funcionará si existe la sesión, caso contrario mostrará error.





A continuación tenemos que probar nuestra autenticación por REST a Spring Security e injectarlo manualmente.





 Autenticando con REST















Obteniendo la sesión sin autenticar.









Obteniendo la sesión autenticado.













Como se pudo ver, Spring permite múltiples formas de autenticar, en este Post se ha realizado la autenticación por REST y se ha injectado y obtenido la sesión directamente del contexto de Spring secuurity.





El codigo lo pueden obtener desde GitHub.



Saludos.







Multi-idioma en Spring Boot





La internalización es un factor que se debe considerar en las aplicaciones, sobre todo si las aplicaciones que desarrollamos van a escalar fuera de nuestras frontereas, quizá puede ingresar a otros lugares donde el idioma no es el mismo que en lugar donde se desarrolla el software.











Para ello disponemos de herramientas que nos fascilitan la configuración y poder hacer mas facil esta tarea.



Como primer paso vamos a utilizar Spring Boot para nuestro proyecto, la única dependencia requerida es Spring Web, lo pueden hacer directamente desde https://start.spring.io/





Ahora tenemos que crear minimo 3 archivos:




  • messages.properties

  • messages-es.properties

  • messages-en.properties




Estos archivos contienen lo siguiente:











El archivo messages.properties, es por defecto que nuestra aplicación inicializa, por eso se recomienda que alli se copie el contenido a bien de messages-es.properties o messages-en.properties.



Como siguiente paso tenemos que crear un archivo llamado Translator.java, con el siguiente contenido.








package com.dev.app.apptranslation.config;

import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.stereotype.Component;

@Component
public class Translator {

private static ResourceBundleMessageSource messageSource;

@Autowired
Translator(ResourceBundleMessageSource messageSource) {
Translator.messageSource = messageSource;
}

public static String toLocale(String msgCode) {
Locale locale = LocaleContextHolder.getLocale();
return messageSource.getMessage(msgCode, null, locale);
}
}




Adicionalmente tenemos que crear un archivo configuración en este caso InternationalizationConfig.java con contenido:








package com.dev.app.apptranslation.config;

import java.util.Locale;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

@Configuration
public class InternationalizationConfig implements WebMvcConfigurer {

@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver localeResolver = new SessionLocaleResolver();
localeResolver.setDefaultLocale(Locale.US);
return localeResolver;
}

@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("language");
return localeChangeInterceptor;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
}




 Si analizamos el metodo localeChangeInterceptor , podemos ver se setea un parametro "language", este es el que utilizaremos pasando como parametro el idioma respectivo.





Para probar creamos un controllador y realizamos el llamado a nuestro Translator, utilizando el metodo "Translator.toLocale("message.stock")".








package com.dev.app.apptranslation.controller;

import com.dev.app.apptranslation.config.Translator;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ItemController {

@RequestMapping(method = RequestMethod.GET, path = "/stock")
public String welcome() {
return Translator.toLocale("message.stock");
}
}






Vamos a ver el resultado de nuestro controller.

















La internalización se puede configurar de múltiples manera, en esta demo se ha realizado de la manera mas sensilla para comprender como hacer una integración con Spring Boot.





El codigo fuente lo pueden obtener desde GitHub.





Saludos.









Swagger en Spring Boot





La documentación de una API es muy importante, sobre todo cuando separamos el Backend y Frontend, el que hace las tareas de Frontend, facilmente puede probarlos, sin necesidad de estar consultando con el responsable del Backend.









Para ello, vamos a utilizar Spring boot, y realizar la configuración e instalación.



Nos movemos a https://start.spring.io/ para crear nuestro proyecto en spring.



La dependencia que necesitamos es Spring Web.





Ahora vamos a configurar el pom.xml con las dependencias necesarias.








<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.dev.api</groupId>
<artifactId>api-docs</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>api-docs</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
<springfox.swagger.version>2.9.2</springfox.swagger.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${springfox.swagger.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${springfox.swagger.version}</version>
</dependency>
<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>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>






Como podemos ver. las dependencias necesarias son:






<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${springfox.swagger.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${springfox.swagger.version}</version>
</dependency>




 Ahora que nuestro proyecto ya tiene instalado las dependencias, vamos a configurar swagger a nivel de código.



Tenemos que crear una clase llamada: SwaggerConfiguracion.java con el siguiente contenido.






@Configuration
@EnableSwagger2
public class SwaggerConfiguration {
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String DEFAULT_INCLUDE_PATTERN = "/api/.*";
ApiInfo apiInfo() {
return new ApiInfoBuilder().title("API")
.description("Rest Service")
.termsOfServiceUrl("").version("1.0.0").contact(new Contact("DEV", "http://claros-dev.blogspot.com/", "reyiclaros@gmail.com")).build();
}
@Bean
public Docket configureControllerPackageAndConvertors() {
return new Docket(DocumentationType.SWAGGER_2).select()
.apis(RequestHandlerSelectors.basePackage("com.dev.api.apidocs.controller"))
.build()
.forCodeGeneration(true)
.genericModelSubstitutes(ResponseEntity.class)
.ignoredParameterTypes(Pageable.class)
.ignoredParameterTypes(java.sql.Date.class)
.directModelSubstitute(java.time.LocalDate.class, java.sql.Date.class)
.directModelSubstitute(java.time.ZonedDateTime.class, Date.class)
.directModelSubstitute(java.time.LocalDateTime.class, Date.class)
.securityContexts(Lists.newArrayList(securityContext()))
.securitySchemes(Lists.newArrayList(apiKey()))
.apiInfo(apiInfo())
.useDefaultResponseMessages(false);

}
private ApiKey apiKey() {
return new ApiKey("JWT", AUTHORIZATION_HEADER, "header");
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex(DEFAULT_INCLUDE_PATTERN))
.build();
}
List<SecurityReference> defaultAuth() {
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
return Lists.newArrayList(new SecurityReference("JWT", authorizationScopes));
}
}




El resultado los vemos:











Adicionalmente nuestra configuración inclute "Autorize" para agregar un token y enviar en nuestras peticiones, tal como lo vemos en las siguientes imagenes.















Como podemos ver. la configuración de Swagger es muy simple.



Recuerda que como Backend es indispensable documentar nuestras APIS, para fascilitar el desarrollo en el equipo.



El codigo del proyecto lo pueden descargar en GitHub



Saludos.