2.12.18

Spring Security: formulario de login personalizado (JSP/JSTL).

Veamos... crear el formulario para hacer login:


<%@page contentType="text/html; charset=UTF-8" %>
<%@taglib prefix = "c" uri = "http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
    <head>
        <title>Login</title>
    </head>
    <body>

    <c:if test="${param.error!=null}">
        <div>
            Invalid username and password.
        </div>
    </c:if>
    
    <c:if test="${param.logout!=null}">
        <div>
            You have been logged out.
        </div>
    </c:if>

    <c:url value="/login" var="loginURL" />
    <form action="${loginURL}" method="post">            
        <div><label> User Name : <input name="username" type="text" /> </label></div>
        <div><label> Password: <input name="password" type="password" /> </label></div>
        <div><input type="submit" value="Sign In"/></div>
        <%-- CSRF Token para evitar ataques CSRF --%>
        <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
    </form>
    </body>
</html>

Lo del CSRF Token es mejor activarlo, no como pone en otros sitios donde se desactiva. Es cuestión de hacer el servicio más seguro.

En este formulario aparece lo de "param.logout!=null" y es porque resulta que por defecto, al hacer logout, se redireccionará a la url  "/login?logout". Se podría cambiar, pero de momento así se queda.

También aparece lo del "param.error!=null" por el mismo motivo, cuando hay un fallo de autenticación pues se redirige a "/login?error".

Después, tenemos que añadir un controlador que lleve a esa vista "/login":


import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry){
        registry.addViewController("/login").setViewName("login");        
    }

}

No es necesario por tanto crear una clase @Controller para algo tan nimio. Después, en la configuración de seguridad debemos permitir el acceso a todo el mundo al login y al logout también, sino no podrán acceder a dichas secciones:


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()
                .loginPage("/login")
                .defaultSuccessUrl("/restricted")
                .permitAll()
            .and()
                .logout()      
                .permitAll()
            .and()
                .csrf();   
    }

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

}

Significativo es también el uso de defaultSuccessUrl para indicar cual es la página por defecto a la que hay que llevar después de hacer login correcto.

Ahora hay que hacer posible el logout. Para poder hacer el logout con la protección frente a ataques CSRF, creamos un pequeño formulario para introducir el token CSRF. Así evitamos que alguien vaya desconectando sesiones ilícitamente.

Podría hacerse en un JSP/JSTL aparte que se incluiría después (logoutForm.jsp por ejemplo):


<c:url value="/logout" var="logoutURL" />
<form style="display:inline-block" action="${logoutURL}" method="post">            
       <button style="border-radius: 5px; padding: 2px 10px;" type="submit">salir</button>
       <%-- CSRF Token para evitar ataques CSRF --%>
       <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
 </form>

Después lo integraremos en la página restringida. Veamos ahora como podría ser el controlador de la página restringida "/restricted":


import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/restricted")
public class RestrictedController
{
    @GetMapping
    public String accessToRestricted(@AuthenticationPrincipal UserDetails userDetails, Model m)
    {
        if (userDetails!=null)
        {  
            m.addAttribute("userName", userDetails.getUsername());       
            return "restricted";
        }
        return "redirect:/";
    }
}

Este controlador puede acceder a la información de autenticación a través de @AuthenticationPrincipal, cosas como por ejemplo el nombre de usuario (aquí se verifica que userDetails sea distinto de null, lo cual es redundante con respecto a la configuración de seguridad, pero previene de un fallo de configuración de seguridad). Es cuestionable, porque luego para cualquier cambio hay que acordarse que está ahí :-(, perdiendo la ventaja de la configuración de seguridad centralizada.

Y luego pues su vista (restricted.jsp):


<!DOCTYPE html>
<html>
    <head>
        <title>Restricted Area</title>
    </head>
    <body>
   
    Bienvenido, ${userName}. 
    
    El mundo es maravilloso, ¿quieres <%@include file="logoutForm.jsp"%>?
         
</body>
</html>

Y esto es todo... 'ta chulo.

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.