Menu

Spring Boot Aplicacion Web Parte 9.

0 Comment


Spring Security

Paso a paso de como agregarle Spring Security a nuestro proyecto, encriptar passwords y manejo de acceso por roles.

Contenido

  1. Agregar Librerías y Configuración.
  2. Encriptar y Actualizar Passwords.
  3. Cerrar Sesion o Logout.
  4. Agregar Seguridad por Roles.
  5. Git Commit

1.Agregar Librerias y Configuracion

Una vez agregues la dependencia de Spring Security en tu pom.xml la aplicacion se asegurara automaticamente, pero necesitaremos configurar esa seguridad, por ejemplo decirle como se manejara el acceso a las paginas HTML, recursos estaticos (css, js, img, etc.).

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

	<dependency>
		<groupId>org.springframework.security</groupId>
		<artifactId>spring-security-test</artifactId>
		<scope>test</scope>
	</dependency>
.
.
.
</dependencies>

Necesitaremos dos nuevas clases, la primera implementara el login en la aplicacion implementando una clase de Spring Security. Y la segunda clase servira para sobre-escribir la configuracion web predeterminada.

En el paquete de servicios, crea la clase UserDetailsServiceImpl.java.

UserDetailsServiceImpl.java
package com.cristianruizblog.springbootApp.service;

import java.util.HashSet;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.cristianruizblog.springbootApp.entity.Role;
import com.cristianruizblog.springbootApp.repository.UserRepository;

@Service
@Transactional
public class UserDetailsServiceImpl implements UserDetailsService{

	@Autowired
    UserRepository userRepository;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

		//Buscar nombre de usuario en nuestra base de datos
		com.cristianruizblog.springbootApp.entity.User appUser = 
	                 userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("User does not exist!"));
		
	    Set grantList = new HashSet(); 
	    
	    //Crear la lista de los roles/accessos que tienen el usuarios
	    for (Role role: appUser.getRoles()) {
	        GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getDescription());
	            grantList.add(grantedAuthority);
	    }
			
	    //Crear y retornar Objeto de usuario soportado por Spring Security
	    UserDetails user = (UserDetails) new User(appUser.getUsername(), appUser.getPassword(), grantList);
	    return user;
		
	}

}
WebSecurityConfig.java
package com.cristianruizblog.springbootApp;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.bcrypt.BCryptPasswordEncoder;

import com.cristianruizblog.springbootApp.service.UserDetailsServiceImpl;

//Indica que esta clase es de configuracion y necesita ser cargada durante el inicio del server
@Configuration

//Indica que esta clase sobreescribira la implmentacion de seguridad web
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    String[] resources = new String[]{
            "/include/**","/css/**","/icons/**","/img/**","/js/**","/layer/**"
    };
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	http
        .authorizeRequests()
        .antMatchers(resources).permitAll()  
        .antMatchers("/","/index").permitAll()
            .anyRequest().authenticated()
            .and()
        .formLogin()
            .loginPage("/login")
            .permitAll()
            .defaultSuccessUrl("/userForm")
            .failureUrl("/login?error=true")
            .usernameParameter("username")
            .passwordParameter("password")
            .and()
            .csrf().disable()
        .logout()
            .permitAll()
            .logoutSuccessUrl("/login?logout");
    }
    
    BCryptPasswordEncoder bCryptPasswordEncoder;

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
		bCryptPasswordEncoder = new BCryptPasswordEncoder(4);
        return bCryptPasswordEncoder;
    }
    
    @Autowired
    UserDetailsServiceImpl userDetailsService;
    
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { 
    	//Especificar el encargado del login y encriptacion del password
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
}

Y el ultimo toque para complementar la configuracion es asegurarte que tu formulario de login tenga los siguientes atributos, en especial el metodo post.

index.html
<form class="col-12" th:action="@{/login}" method="post">

2.Encriptar y Actualizar Passwords

Necesitas actualizar los password existentes en la Base datos para que se acoplen al encriptador de passwords, crearemos una clase tools/util para generar estos password y luego actualizarlos manualmente en tu base de datos.


Normalmente tu aplicacion deberia de iniciar con un usuario “admin”, esta clase te servira para generar su password tambien.

PassGenerator.java
package com.cristianruizblog.springbootApp.util;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class Passgenerator {

	public static void main(String ...args) {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(4);
       
        //El String que mandamos al metodo encode es el password que queremos encriptar.
        System.out.println(bCryptPasswordEncoder.encode("1234"));
        /*
         * Resultado: $2a$04$n6WIRDQlIByVFi.5rtQwEOTAzpzLPzIIG/O6quaxRKY2LlIHG8uty
         */
    }
}

El siguiente paso sera ejecutar esta clase como una aplicacion java y como resultado tendras tus passwords encriptados para actualizar en la base de datos.

PassGenerator Util Class
PassGenerator Util Class

3.Cerrar Sesion o Logout.

Cerrar sesion es muy facil de implementar, tan simple como tener un enlace que redireccione a “logout”.

Agrega una nueva pestaña (tab) en user-view.html de la siguiente manera. Ademas mostraremos el nombre del usuario en sesion.

user-view.html
<li class="nav-item">
<a class="nav-link" href="#" th:href="@{/logout}"><span th:text="${#authentication.getPrincipal().getUsername()}"></span> - <span>logout  </span><span class="float-right"><i class="fas fa-sign-out-alt"></i></span></a>				   	
</li>
Logout Tab
Logout Tab

4.Agregar Seguridad por Roles.

Ahora limitaremos las acciones de los usuarios dependiendo del role asignado, en este caso limitaremos al rol USER porque el ADMIN va a tener acceso a todo.

ADMINUSER
1.Crear Usuarios
2.Editar Cualquier Usuario Editar Propia informacion
3.Eliminar Cualquier Usuario
4.Cambiar Password Cualquier Usuario Cambiar su Propio Password (ingresando el actual)

1.Crear Usuarios

Vamos a utilizar dos intrucciones nuevas en la vista y una en el backend.

Vista/Thymeleaf

1.Para preguntar por un rol especifico poner la sentencia hasRole, para preguntar por mas de un rol poner la sentencia hasAnyRole separando cada rol con una coma, por ejemplo. (‘rol1′,’rol2’)

<div th:if="${#authorization.expression('hasRole(''ROLE_ADMIN'')')}"></div>

2.Para validar el usuario que inicio sesion se utiliza un objeto de spring llamdado httpServletRequest.remoteUser.

<div th:if="${#httpServletRequest.remoteUser==user.username}"></div>

Backend/Java

Para decidir si un usuario en sesion tiene accesso o no a utilizar un metedo necesitas activar la anotacion @PreAuthorize y anotar el metodo deseado. Utiliza la misma expression que usamos en thymeleaf. Para activar la anotacion de seguridad necesitamos crear otra clase de de configuracion en el paquete raiz.

MethodSecurityConfig.java
package com.cristianruizblog.springbootApp;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;

@Configuration
@EnableGlobalMethodSecurity(
		prePostEnabled=true
)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

}
UserServiceImpl.java
@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER')")
	public User updateUser(User formUser) throws Exception{
...
}


Pero para poder utilizar estas expresiones necesitamos agregar la libreria de seguridad de thymeleaf a nuestro pom.xml.

<dependency>
	<groupId>org.thymeleaf.extras</groupId>
	<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>


Ok, ahora implementemos esto en nuestro proyecto. vamos a empezar por bloquear todos los campos del formulario para el rol USER cuando no este en modo editar.

Creamos un div que encapsule el formulario con el atributo th:with para crear variable en thymeleaf llamada disableFields y la agregamos a cada campo del formulario con th:disabled.

user-form.html

<div th:with="disableFields=!${editMode} and ${#authorization.expression('hasRole(''ROLE_USER'')')}">
.... FORMULARIO 
    <input class="form-control" type="text" th:field="${userForm.firstName}" th:disabled="${disableFields}">
</div>

2.Editar Usuarios

Recordemos que cada rol tiene sus condiciones para editar o no usuarios.

Rol ADMIN puede editar cualquier usuario.
Rol USER solo puede editar su propio usuario.

Estas condiciones las agregaremos a los links de editar usuario en la lista de usuarios, archivo user-list.html, observa que para mejor interpretacion he encapsulado el link en una etiqueta span y la condicion la agregue con th:if.


<span th:if="${#authorization.expression('hasRole(''ROLE_ADMIN'')')} or (${#authorization.expression('hasRole(''ROLE_USER'')')} and ${#httpServletRequest.remoteUser==user.username})">
    <a href="#" th:href="@{'/editUser/'+ ${user.id}}"><i class="fas fa-edit"></i></a>
</span>

3.Eliminar Usuario

Utilizaremos la primer condicion que usamos en el paso anterior sobre el link de eliminar para dar acceso a esa accion solo a usuarios con rol ADMIN

<span th:if="${#authorization.expression('hasRole(''ROLE_ADMIN'')')}" >
	| <a href="#" th:onclick="'javascript:confirmDelete(\''+ ${user.id} +'\');'"><i class="fas fa-user-times"></i></a>
</span>

4.Cambiar Password

Ambos roles van a tener acceso a cambiar la contraseña con los mismo limitantes de las otras acciones, el ADMIN actualiza cualquier password sin ingresar contraseña actual y el USER solo podra actualizar su propia contraseña validando la actual.
Utilizaremos la misma validacion que utilizamos en la accion editar usuario para mostrar el boton.

<button type="button" class="btn btn-secondary" data-toggle="modal" data-target="#changePasswordModal"
	th:if="${editMode} and (${#authorization.expression('hasRole(''ROLE_ADMIN'')')} 
								or (${#httpServletRequest.remoteUser==userForm.username}))" 
>Change Password</button>

En el formulario de cambiar contraseña validamos tambien si se muestra o no el campo de “Current Password”, sino se muestra el campo tenemos que crear un campo escondido (hidden) y mandar un valor cualquiera para no tener conflicto con la anotacion @Valid.

change-password.html
<input th:if="${#authorization.expression('hasRole(''ROLE_ADMIN'')')}"  type="hidden" th:field="${passwordForm.currentPassword}" value="blank"  />
<div   th:unless="${#authorization.expression('hasRole(''ROLE_ADMIN'')')}" class="form-group row">
	<label class="col-lg-3 col-form-label 	form-control-label">Current Password</label>
	<div class="col-lg-9">
		<input class="form-control" type="password" th:field="${passwordForm.currentPassword}">
		<div class="alert-danger" th:if="${#fields.hasErrors('currentPassword')}" th:errors="*{currentPassword}">Password</div>
	</div>
</div>

Ya esta lista la validacion en la vista, ahora pasemos al backend. Crearemos un metodo que nos retorne verdadero (true) si el usuario es ADMIN. Este metodo yo lo puse en la misma clase donde se valida el password para el USER.

UserServiceImpl.java
private boolean isLoggedUserADMIN() {
	Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
	UserDetails loggedUser = null;
	if (principal instanceof UserDetails) {
		loggedUser = (UserDetails) principal;
	
		loggedUser.getAuthorities().stream()
				.filter(x -> "ADMIN".equals(x.getAuthority() ))      
				.findFirst().orElse(null);
	}
	return loggedUser != null ?true :false;
}

public User changePassword(ChangePasswordForm form) throws Exception{
	User storedUser = userRepository
			.findById( form.getId() )
			.orElseThrow(() -> new Exception("UsernotFound in ChangePassword."));

	if( !isLoggedUserADMIN() && form.getCurrentPassword().equals(storedUser.getPassword())) {
		throw new Exception("Current Password Incorrect.");
	}
	....
}

5.Git Commit

Secure Application - Spring Security
Secure Application – Spring Security

Gihub Commit: https://github.com/cruizg93/Spring-Boot-Web-Application/commit/9c4874f9c043ba6cfb9cc27e381251710d436988

Menu

  1. Setup
  2. Entidades y POJOS
  3. Basic HTML
  4. Lista de Usuarios
  5. Crear Usuario y Validar Campos
  6. Editar Usuario
  7. Eliminar Usuario
  8. Cambiar Contraseña
  9. Spring Security
  10. Paginas de Error

Gracias por llegar al final de este post.
No se te olvide dejar tus comentario o preguntas aca abajo o en mi twitter @Cruizg93

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *