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.