Spring Security
Paso a paso de como agregarle Spring Security a nuestro proyecto, encriptar passwords y manejo de acceso por roles.
Contenido
- Agregar Librerías y Configuración.
- Encriptar y Actualizar Passwords.
- Cerrar Sesion o Logout.
- Agregar Seguridad por Roles.
- Git Commit.
- Video Paso a Paso.
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<GrantedAuthority> grantList = new HashSet<GrantedAuthority>();
//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.
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>
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.
ADMIN | USER |
---|---|
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/Thymeleaf1.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 (Actualizado 06/14/19)
public boolean isLoggedUserADMIN(){
return loggedUserHasRole("ROLE_ADMIN");
}
public boolean loggedUserHasRole(String role) {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
UserDetails loggedUser = null;
Object roles = null;
if (principal instanceof UserDetails) {
loggedUser = (UserDetails) principal;
roles = loggedUser.getAuthorities().stream()
.filter(x -> role.equals(x.getAuthority() ))
.findFirst().orElse(null); //loggedUser = null;
}
return roles != 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
Gihub Commit: https://github.com/cruizg93/Spring-Boot-Web-Application/commit/9c4874f9c043ba6cfb9cc27e381251710d436988
6.Video Paso a Paso
Menu
- Setup
- Entidades y POJOS
- Basic HTML
- Lista de Usuarios
- Crear Usuario y Validar Campos
- Editar Usuario
- Eliminar Usuario
- Cambiar Contraseña
- Spring Security
- Paginas de Error
- Bonus, Arreglando Cositas
- Formulario de registro
- Despliegue en Heroku
Gracias por llegar al final de este post.
No se te olvide dejar tus comentario o preguntas aca abajo o en mi twitter @Cruizg93
Tengo una duda, dices que para que el encriptar contraseña funcione debemos actualizar o cambiar el password, pero yo quisiera que la contraseña quedara encriptada desde el momento que creo el nuevo usuario, sera posible poder hacer esto, o es obligatorio actualizarla?
Gracias por responder soy tu suscriptor en Youtube y voy por esta parte pero el cambiar contraseña no lo implemente, por eso tengo esta duda.
Claro por supuesto, disculpa de antemano se me paso ese detalle…
Actualiza tu metodo de crear usuario en UserServiceImpl.java, antes de decirle al repositiorio que cree el usuario hay que hacer lo mismo que hicimos donde se actualiza el password.
@Override
public User createUser(User user) throws Exception {
if (checkUsernameAvailable(user) && checkPasswordValid(user)) {
String encodePassword = bCryptPasswordEncoder.encode(user.getPassword());
user.setPassword(encodePassword);
user = repository.save(user);
}
return user;
}
Gracias por responder compañero, voy a intentarlo e igual el checkear el usuario no se porque no me funcionó, el mensaje de error nunca se muestra, por eso se lo quite lo creo sin esa validación. Gracias compañero te aviso si me funciona.
Reynaldo puedes dejar tu codigo en los comentarios y yo lo reviso, o si lo tienes en algun repositorio o carpeta compartido en la nube tambien lo puedo mirar y lo resolvemos juntos
Gracias compañero tu aporte funciona correctamente, solo tengo dudas con una implementacion que quiero hacer, al momento de loguearme como un usuario quisiera que mi redireccion fuera a alguna pagina en especifico por así decirlo del lado del cliente y no de la administración y no al UserFrom como lo tenemos definido, pero si ingreso como admin que me redireccione al userFrom, espero recibir de tu ayuda compañero y muchas gracias
Claro dejame te ayudo.
1)Cambia el metodo isLogginUserADMIN() de privado a publico en tu servicio de usuario y agregalo a la interface.
2)Recuerda que en tu archivo de WebSecurityConfig.java redireccionamos todos los logins a la ruta “/userForm”
3)Una vez entres a ese metodo en UserController tu puedes hacer uso del metodo que verifica si el usuario es admin o no para retornar a la pagina deseada.
Ejemplo:
.
UserService
Que gran trabajo viejo…! muchas gracias me ayudo mucho… Solo una inquietud, cuando registro un usuario me lo registra correctamente, pero al momento de iniciar sesion no quiere loguear… sabes por que es eso?
Hola Henry, muchas gracias por dejar tu comentario, lo que pasa es que me falto poner la encriptacion en el servicio que crea el usuario. Lee los comentarios que tengo con Reynaldo o mira la parte final del video donde explico ese error. Tengo q actualizar el post
Hermano estoy teniendo un gran problema…. no puedo iniciar sesión… me envía el “/login?error=true” y se mantiene en el index… tal vez sea una tontría pero no la estoy logrando resolver, ¿me podrás ayudar?
Alejandro, tienes q compartir el codigo para poder ver que esta mal.
Hola Cristian, bro te dejo el link del repo: https://github.com/alearmas/SpringSecurity
Alejandro, disculpa la demora pero he estado muy ocupado, todavia tienes problemas con el login.???
Muy buenas, ante todo felicitaciones por este curso, me esta siendo de gran ayuda ya tengo mi web casi terminada. Lo que me ocurre actualmente es que despues de añadir la seguridad quiero usar la autentificación de los usuarios para obtener datos de base de datos, de tal forma que si un usuario ingresa en la aplicacion en la tabla le muestre informacion que haya registrado el previamente y si accede otro usuario le muestre su información. He cambiado el metodo que tu tenias implementado de findByAlL de esta forma:
@Override
public Iterable getAllAnimales(){
String idUsuario = “11”;
return (Iterable) animalRepository.findByIdUsuario(idUsuario);
}
El caso es que me gustaria pasarle el id del usuario que inicio sesion o el username del usuario que inicio sesion para que asi dependediendo de que usuario inicie sesion le muestre sus datos solamente.
Un saludo!!
Hola Juan, en el post #11 hay una seccion que se llama 2# Fix, ahi muestro como conseguir el usuario que inicio session.
aqui te dejo el metodo.
http://cristianruizblog.com/spring-boot-aplicacion-web-parte-11/
Solo recuerda que el objeto User, utilizado en ese metodo es tu entidad y no la de spring
Buenas de nuevo, gracias por la rapidez en la respuesta. Conseguí resolver el problema que tenía. Eres genial 🙂 Sigue asi!!