28.11.18

Ejemplo Spring: ejemplo básico de autenticación


Primero tendremos que tener un área que queramos restringir, por ejemplo esta... con información muy confidencial:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class RestrictedController
{
    @GetMapping ("/restricted")
    @ResponseBody
    public String accessToRestricted()
    {
        return "Hola hola mundo!";
    }
}
 
Si no hay nada que restringir no tiene sentido todo esto la verdad. Después, en nuestro pom.xml, (en mi caso uso Maven... buff), deberíamos incluir la siguiente dependencia:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

Con esa dependencia integramos este framework de seguridad de Spring de forma muy sencilla. Luego deberíamos incluir la configuración de seguridad, a través de una clase:
  • Clase con anotación @Configuration para indicar que es de configuración.
  • Anotación @EnableWebSecurity para habilitar la configuración de seguridad.
  • Extendemos el adaptador WebSecurityConfigurerAdapter que tiene métodos configure para configurar la seguridad.
  • Sobrescribimos el método configure (HttpSecurity httpsec) con las restricciones que deseamos.
  • Creamos un método en el que inyectamos AuthenticationManagerBuilder auth, con @Autowired, para crear una configuración básica de usuario.
Veamos:

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter
{ 

    private final Log logger = LogFactory.getLog(SecurityConfig.class);

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    protected void configure(HttpSecurity httpsec) throws Exception{
        httpsec.authorizeRequests()
                .antMatchers("/").permitAll()
                .anyRequest().authenticated()
            .and()
                .formLogin().permitAll();   
    }

    @Autowired
    protected void confDatosAccesoUsuarios (AuthenticationManagerBuilder auth) {
        try {
            auth.inMemoryAuthentication()
            .withUser("test").password(passwordEncoder.encode("test")).roles("USER");
        } catch (Exception e)
        {
            logger.error(e.toString());
        }
    }

}

Dentro del método configure, se emplea la instancia httpsec para indicar la configuración de seguridad... un poco extraño al principio la verdad.

De momento le estamos diciendo que solo permita acceder a la página raíz (.antMatchers("/").permitAll()) y al resto no (.anyRequest().authenticated()). Después, que habrá un formulario de login al que tendrá todo el mundo acceso (.formLogin().permitAll()). Como no hemos indicado ninguno propio, este framework de Spring Security proveerá uno por defecto, algo así:



Y falta una cosa, la parte del PasswordEncoder que está @Autowired en la configuración anterior. Esto es un @Bean que debemos poner en algún lado (que tenga @Configuration o que lo incluya, como es el caso de @SpringBootApplication). Por ejemplo, en la clase principal de nuestra aplicación Spring Boot:


@SpringBootApplication
public class Application {

  ...

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

  ...
}
 
Así todas nuestras contraseñas estarán codificadas en memoria y no en texto plano, dado que ese bean proveerá el mecanismo para ello.

Y esto es todo... poca cosa.

25.11.18

Ejemplo Spring: validación personalizada y otras cosas.

La idea ahora es conseguir que se valide la información recibida del formulario antes de usarla. Dado el siguiente formulario, se usará para generar una combinación válida.


El aspecto del formulario en JSP/JSTL es el siguiente:


       <form:form modelAttribute="combinacion" action="/" method="post">
            <fmt:message key="home.label.minimum"/> <form:input path="min" type="number" /> <form:errors path="min" /><br>
            <fmt:message key="home.label.maximum"/> <form:input path="max" type="number" /><form:errors path="max" /><br>
            <fmt:message key="home.label.quantity"/> <form:input path="n" type="number" /> <form:errors path="n" /><br>
            <button type="submit">
                <fmt:message key="home.button.submit"/>
            </button>
        </form:form>

Las etiquetas <form:errors ...> que vienen de  <%@taglib prefix = "form" uri = "http://www.springframework.org/tags/form" %> ; mostrarán los errores en los campos.

Primero tendremos la clase en sí (genera una combinación de números n numeros aleatorios entre min y max):

import java.util.Random;
import java.util.Set;
import java.util.TreeSet;

import javax.validation.constraints.Min;

@CombGenConstraint
public class GeneradorCombinacion {

        private Set<Integer> numbers;
        private int min;
        private int max;

        @Min(value=0, message="{combinacion.nError}") 
        private int n;
        
        GeneradorCombinacion()
        {
            this(6, 1, 49);
        }

        GeneradorCombinacion (int n, int min, int max)
        {            
            this.min=min;
            this.max=max;
            this.n=n;            
            this.calcular();            
        }
        
        public void calcular()
        {
            int lmax=getMax();
            int lmin=getMin();
            int ln=getN();
            if (lmax>lmin && lmax-lmin+1>=ln)
            {
                Random r=new Random();
                numbers=new TreeSet<>();            
                while (numbers.size()<ln)
                {
                    int number=r.nextInt(lmax-lmin+1)+lmin;
                    if (!numbers.contains(number))
                        numbers.add(number);
                }
            }            
        }

        public Set<Integer>getNumbers()
        {            
            return numbers;
        }        
        
        public int getMin() {
            return min;
        }

        public int getMax() {            
            return max;
        }

        public int getN() {
            return n;
        }

        public void setMin(int min) {
            this.min=min;            
        }

        public void setMax(int max) {
            this.max=max;            
        }

        public void setN (int n) {
            this.n=n;            
        }
    }   

En el ejemplo anterior se anotaron validaciones:
  • @Min(value=0, message="{combinacion.nError}")  --> validación predefinida que podemos usar cuando queramos (hay muchas más).
  • @CombGenConstraint --> validación creada para este caso concreto.
Para  crear la validación CombGenConstraint primero debemos crear una anotación y luego la clase que la valida:

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;

import javax.validation.Constraint;
import javax.validation.Payload;


@Documented
@Constraint(validatedBy = CombGenValidator.class)
@Target( { ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface CombGenConstraint {
    String message() default "{CombGenConstraint.NotValid}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

La parte de "message()..." define el mensaje asociado al error (que se mostrará al usuario), cuando se produzca este error. Si está entre llaves es porque puede ir en un archivo tipo ".properties".

La parte de "@Constraint(validatedBy = CombGenValidator.class)" indica que esto es una restricción y que es validada por la clase CombGenValidator.

La parte de @Target( { ElementType.TYPE }) indica que se puede aplicar a clases (sin fuera para atributos hay que poner ElementType.FIELD, sino recuerdo mal).

Y ahora la clase que hace la validación:

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class CombGenValidator implements ConstraintValidator<CombGenConstraint, GeneradorCombinacion> {

    @Override
    public boolean isValid(GeneradorCombinacion c, ConstraintValidatorContext context) {
        boolean b=c.getMax()>c.getMin() && c.getMax()-c.getMin()+1>=c.getN();
        return b;
 }

}

Aquí es mportante implementar la clase ConstraintValidator, poniendo la anotación asociada (CombGenConstraint en este caso), y la clase (superclase si fuese necesario) a la que se aplica (GeneradorCombinacion en este caso).

Los mensajes de validación de la restricción pueden ir en el archivo ValidationMessages.properties por ejemplo:

combinacion.nError=El valor máximo para la cantidad es 0. 
CombGenConstraint.NotValid=Los datos insertados no permiten generar ninguna combinación. 

También es lógico añadir los siguientes siguientes mensajes:

typeMismatch.min = Debe proporcionarse un valor válido para el mínimo. 
typeMismatch.max = Debe proporcionarse un valor válido para el máximo. 
typeMismatch.n = Debe proporcionarse un valor válido para n

Estos pueden ir por ejemplo en el archivo messages.properties, y están destinados a sobrescribir los mensajes de error por defecto producidos cuando no se pueda procesar la entrada recibido por el formulario.

El controlador podría ser así ahora (método homePost):

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import javax.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
@RequestMapping("/")
public class HomeController  { 
    
    @GetMapping
    public String homeGet (Model model, 
            @RequestParam(defaultValue="6") int n,
            @RequestParam(defaultValue="1") int min, 
            @RequestParam(defaultValue="49") int max) {       
        model.addAttribute("combinacion", new GeneradorCombinacion(n, min, max));        
        return "home";
    }    

    @PostMapping
    public String homePost (@Valid @ModelAttribute("combinacion") GeneradorCombinacion combinacion,
    BindingResult result, Model model)
    {   
        List<String> problemasGlobales=new ArrayList<>();
        String problemasCampos=null;

        if (result.hasFieldErrors())
        {
            List<FieldError> lfe=result.getFieldErrors();
            problemasCampos=lfe.stream().map(fe->fe.getField()).collect(Collectors.joining(", "));
        }
        
        for (ObjectError oe:result.getGlobalErrors())
            problemasGlobales.add(oe.getDefaultMessage());  
        

        if (problemasGlobales.size()==0 && problemasCampos==null)
        {
            combinacion.calcular();                   
        }
        else
        {
            model.addAttribute("problemasGlobales",problemasGlobales);  
            model.addAttribute("problemasCampos",problemasCampos);                   
        }

       return "home";
    }
    

}



Y por último, el archivo JSP/JSTL podría ser así:


<%@taglib prefix = "c" uri = "http://java.sun.com/jsp/jstl/core" %>
<%@taglib prefix = "fmt" uri = "http://java.sun.com/jstl/fmt"%>
<%@taglib prefix = "form" uri = "http://www.springframework.org/tags/form" %> 
<html>
    <head>
         <link rel="stylesheet" type="text/css" href="main.css">
         <meta charset="UTF-8">
    </head>
    <body>        
        <H1><fmt:message key="home.listOfNumbers"/></H1>   
        <c:if test="${not empty problemasGlobales or not empty problemasCampos}">
            <ul>            
            <c:if test="${not empty problemasGlobales}">
                <c:forEach items="${problemasGlobales}" var="mensaje">
                    <li>
                        <span class="error">
                            ${mensaje}
                        </span>
                    </li>
                </c:forEach>
            </c:if>
            <c:if test="${not empty problemasCampos}">
                <fmt:message key="home.error.fields">
                    <fmt:param>${problemasCampos}</fmt:param>
                </fmt:message>
            </c:if>
            </ul>
        </c:if>

        <c:if test="${empty problemasGlobales and empty problemasCampos}">        
        <fmt:message key="home.range">
            <fmt:param>${combinacion.getN()}</fmt:param>
            <fmt:param>${combinacion.getMin()}</fmt:param>
            <fmt:param>${combinacion.getMax()}</fmt:param>
        </fmt:message>
       
        <ul>
            <c:forEach items="${combinacion.getNumbers()}" var="numero">
            <li>
                ${numero}
            </li>           
            </c:forEach>
        </ul>                    
        </c:if> 

        <form:form modelAttribute="combinacion" action="/" method="post">
            <fmt:message key="home.label.minimum"/> <form:input path="min" type="number" /> <form:errors path="min" /><br>
            <fmt:message key="home.label.maximum"/> <form:input path="max" type="number" /><form:errors path="max" /><br>
            <fmt:message key="home.label.quantity"/> <form:input path="n" type="number" /> <form:errors path="n" /><br>
            <button type="submit">
                <fmt:message key="home.button.submit"/>
            </button>
        </form:form>
    </body>
</html>

Aquí la combinación solo se muestra si no hay errores en los campos.

Y esto es todo.

21.11.18

Spring: ejemplo de uso de data binding en formularios

Podríamos tener una clase que contendría la estructura de los datos a recibir (Combinacion en este caso), y un método para manejar la petición POST (homePost en este caso):

import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
@RequestMapping("/")
public class HomeController  {

    public static class Combinacion {
        private Set<Integer> numbers;                        
        private int min;
        private int max;
        private int n;
        
        Combinacion()
        {
            this(6, 1, 49);
        }

        Combinacion (int n, int min, int max)
        {            
            this.min=min;
            this.max=max;
            this.n=n;            
            this.calcular();            
        }
        
        public void calcular()
        {
            int lmax=getMax();
            int lmin=getMin();
            int ln=getN();
            Random r=new Random();
            numbers=new TreeSet<>();            
            while (numbers.size()<ln)
            {
                int number=r.nextInt(lmax-lmin+1)+lmin;
                if (!numbers.contains(number))
                    numbers.add(number);
            }            
        }

        public Set<Integer>getNumbers()
        {            
            return numbers;
        }        
        
        public int getMin() {
            return Math.min(min,max);
        }

        public int getMax() {            
            return Math.max(Math.max(min,max),getMin()+getN());
        }

        public int getN() {
            return n>0?n:0;
        }

        public void setMin(int min) {
            this.min=min;            
        }

        public void setMax(int max) {
            this.max=max;            
        }

        public void setN (int n) {
            this.n=n;            
        }
    }    
    
    @GetMapping
    public String homeGet (Model model, 
            @RequestParam(defaultValue="6") int n,
            @RequestParam(defaultValue="1") int min, 
            @RequestParam(defaultValue="49") int max) {       
        model.addAttribute("combinacion", new Combinacion(n, min, max));        
        return "home";
    }    

    @PostMapping
    public String homePost (@ModelAttribute("combinacion") Combinacion combinacion, BindingResult result, Model model)
    {
       if (result.hasErrors())
        {
            List<FieldError> lfe=result.getFieldErrors();
            String listaParam=lfe.stream().map(fe->fe.getField()).collect(Collectors.joining(", "));
            model.addAttribute("mensaje", "Error en los parámetros: "+listaParam);
        }
        combinacion.calcular();
       
       model.addAttribute("combinacion", combinacion);        
       return "home";
    }
    
}

La clase Combinacion sería a la que se mapearía la entrada (gracias a @ModelAttribute), y después, el BindingResult contiene información de si se pudo o no hacer el binding (y errores de la validación si la hubiera... que sería lo mejor).

Después en el JSP/JSTL pues simplemente se crea un formulario para enviar los datos vía POST a ese manejador anterior usando las etiquetas http://www.springframework.org/tags/form:

<%@taglib prefix = "c" uri = "http://java.sun.com/jsp/jstl/core" %>
<%@taglib prefix = "fmt" uri = "http://java.sun.com/jstl/fmt"%>
<%@taglib prefix = "form" uri = "http://www.springframework.org/tags/form" %> 
<html>
    <head>
         <link rel="stylesheet" type="text/css" href="main.css">
         <meta charset="UTF-8">
    </head>
    <body>        
        <H1><fmt:message key="home.listOfNumbers"/></H1>   
        <c:if test="${not empty mensaje}">
            <P class="error">
                ${mensaje}
            </P>
        </c:if>
        <fmt:message key="home.range">
            <fmt:param>${combinacion.getN()}</fmt:param>
            <fmt:param>${combinacion.getMin()}</fmt:param>
            <fmt:param>${combinacion.getMax()}</fmt:param>
        </fmt:message>

        <ul>
            <c:forEach items="${combinacion.getNumbers()}" var="numero">
            <li>
                ${numero}
            </li>           
            </c:forEach>
        </ul>     
                
        <form:form modelAttribute="combinacion" action="/" method="post">
            Mínimo: <form:input path="min" type="number" /><br>
            Máximo: <form:input path="max" type="number" /><br>
            Numeros: <form:input path="n" type="number" /><br>
            <button type="submit">
                Enviar!
            </button>
        </form:form>
    </body>
</html>

En el formulario "<form:form...>" el action no es necesario cuando el controlador no es otro... pero no está de más. En ese formulario es importante el atributo modelAttribute (que debe coincidir con el @ModelAttribute del método que maneja la petición POST) y los atributos path de los <form:input ...>, que deben coincidir con el campo del objeto destino (en este caso el objeto Combinacion).

Y esto es todo.


19.11.18

Spring: Internationalization & JSP/JSTL

Primero de todo deberíamos configurar el pom.xml con las dependencias adecuadas, para usar JSP y JSTL:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
        </dependency>

Después, proporcionar un componente/bean "LocaleResolver" encargado de resolver la localización de la petición del cliente. Hay varios tipos, pero de los que yo he visto el más adecuado es CookieLocaleResolver, dado que guarda el idioma en una cookie, recordando la última elección.

@SpringBootApplication
public class Application implements WebMvcConfigurer {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public ViewResolver getViewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/");
        resolver.setSuffix(".jsp");
        return resolver;
    }

    @Bean 
    public LocaleResolver localeResolver()
    {
        return new CookieLocaleResolver();
    }
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("l");
        registry.addInterceptor(localeChangeInterceptor);
    }    
}

La parte del "addInterceptors" proviene de la interfaz WebMvcConfigurer (menos mal que con Java 8 las interfaces pueden tener implementaciones por defecto, sino hay que extender WebMvcConfigurerAdapter, supongo). Esta interfaz permite configurar el MVC de Spring vía Java en varios aspectos, como añadir "Interceptores" (método addInterceptors), que permite interceptar la petición antes de ser pasada al controller, o después. En este caso se usa para cargar un "Locale" en base a la información de la cookie, es decir, la localización.


Ahora se podría cambiar el idioma agregando el parámetro "l" a la URI: "/?l=en" (siempre que messages_en.properties esté creado).

Podemos definir varios archivos "messages_XX.properties", uno por idioma (dejando messages.properties para el idioma usado cuando no es especifique nada). Un ejemplo de "messages_es.properties":

home.welcome = Bienvenidos
home.listOfNumbers = Lista de números elegidos al azar 
home.range= {0} números elegidos al azar entre {1} y {2}

Después se usarían en el JSP así (en este caso con JSTL + EL):

<%@ taglib prefix = "c" uri = "http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix = "fmt" uri = "http://java.sun.com/jstl/fmt"%>
<html>
    <head>
         <link rel="stylesheet" type="text/css" href="main.css">
    </head>
    <body>        
        <H1><fmt:message key="home.listOfNumbers"/></H1>   
        <fmt:message key="home.range">
            <fmt:param>${combinacion.getN()}</fmt:param>
            <fmt:param>${combinacion.getMin()}</fmt:param>
            <fmt:param>${combinacion.getMax()}</fmt:param>
        </fmt:message>

        <ul>
            <c:forEach items="${combinacion.getNumbers()}" var="numero">
            <li>
                ${numero}
            </li>           
            </c:forEach>
        </ul>        
    </body>
</html>

El controlador podría ser algo así:


@Controller
public class BasicController  {
  
    public static class Combinacion {
        private Set<Integer> numbers;        
        private int min;
        private int max;
        private int n;
        
        Combinacion (int n, int a, int b)
        {            
            Random r=new Random();
            numbers=new TreeSet<>();            
            min=Math.min(a,b);
            max=Math.max(a,b);
            this.n=n;
            if (max-min<n) max=min+n-1;
            while (numbers.size()<n)
            {
                int number=r.nextInt(max-min+1)+min;
                if (!numbers.contains(number))
                    numbers.add(number);
            }            
        }
        
        public Set<Integer>getNumbers()
        {            
            return numbers;
        }        
        
        public int getMin() {
            return min;
        }

        public int getMax() {
            return max;
        }

        public int getN() {
            return n;
        }
    }    

    @RequestMapping("/")
    public String index(Model model, 
            @RequestParam(defaultValue="6") int n,
            @RequestParam(defaultValue="1") int min, 
            @RequestParam(defaultValue="49") int max) {       
        model.addAttribute("combinacion", new Combinacion(n, min, max));        
        return "home";
    }    

}

En fin... esto es todo.