Curso de Spring Boot | Login con Spring Security 1

Curso de Spring Boot
Login con Spring Security

Curso de Spring Boot | Login con Spring Security 2

Securizar API con autentificación básica

Tras realizar el login, si este tiene éxito, podremos almacenar el usuario y la contraseña en el cliente y enviar estos datos en la cabecera de cada petición.

Añadimos las dependencias de Spring Security al pom.xml.

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

Flujo de una petición de autentificación

Ejemplo de petición que haremos a nuestra aplicación de Spring

Cada vez que se produce una petición con autentificación básica (petición de login y las que van con usuario y contraseña) se ejecuta la siguiente clase.

En el caso de que en la base de datos exista un usuario con el el nombre de usuario que recibimos como parámetro se devuelve una instancia del mismo a partir de la cual SpringSecurity hará automáticamente una validación de la contraseña.

package com.app.services;
...
@Service
public class CustomUserDetailsService implements UserDetailsService {

 @Autowired
 private UserRepository userRepository;
 
 @Override
 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  User user = userRepository.findByUsername(username);
  if (null == user) {
   throw new UsernameNotFoundException("Ususario no encontrado "+username);
  }  
  return user;
 }
}

Si la autentificación anterior es correcta, llegaremos al método del controlador, que en este caso va a devolver el nombre del usuario logueado:

package com.app.controllers;

@RestController
public class BasicAuthController {

 @PostMapping(path = "/login")
 public ResponseEntity<String> basicauth(UsernamePasswordAuthenticationToken upa) {
  // El objeto upa tiene información sobre el usuario y la contraseña
  // Si el login ha sido exitoso, a partir de ese momento, desde el front, mandaremos en la cabecera de cada petición el username y password que han provocado que el login sea exitoso
  User u = (User) upa.getPrincipal(); // Si en IntelliJ nos da un error cannot find symbol, significa que no esta pillando el getter de la id  
  return ResponseEntity.ok().body("{\"resp\":\"Login ok\", \"id\":"+u.getId()+"}");

 }
}

Configuración del entorno

Configuración del contexto de seguridad de Spring

package com.app.config;

// Springboot inspecciona todas las clases anotadas con @Configuration, @Component, @Service, @Repository y @Controller en busca de anotaciones de Springboot dentro de la propia clase
@Configuration
public class Encoder {
        // Creamos un bean de la clase BCryptPasswordEncoder para tenerlo disponible en el contexto, y poder materializar el Autowired de la clase SecurityConfig
 @Bean(name = "passwordEncoder")
 BCryptPasswordEncoder passwordencoder() {
  return new BCryptPasswordEncoder();
 }
}
package com.app.config;

import static org.springframework.security.config.Customizer.withDefaults;
...

@EnableWebSecurity
@Configuration
public class SecurityConfig {

 @Value("${app.local-domain-front}")
 private String localDomainFront;

 @Autowired
 private UserDetailsService userDetailsService;

 @Autowired
 private BCryptPasswordEncoder bCryptPasswordEncoder;

 public AuthenticationSuccessHandler succesHandler() {
  return (request, response, authentication) -> response.sendRedirect("/");
 }

 @Autowired
 public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
  auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
 }

 @Bean
 AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder)
   throws Exception {
  AuthenticationManagerBuilder authenticationManager = http.getSharedObject(AuthenticationManagerBuilder.class);
  authenticationManager.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
  return authenticationManager.build();
 }

 @Bean
 SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  http.csrf(csrf -> csrf.disable())// Deshabilitamos la protección contra ataques Cross-site request forgery
  .cors(withDefaults())
  .authorizeHttpRequests((requests) -> {
   try {           
    // Definimos que urls estarán desprotegidas y no necesitarán recibir las credenciales para poder ser accedidas
    requests.requestMatchers("/endpointdesprotegido").permitAll().anyRequest().authenticated();
   } catch (Exception e) {
    e.printStackTrace();
   }
  }).httpBasic(withDefaults());
  return http.build();
 }

 // Configuración del CORS (Cross-origin resource sharing)
 @Bean
 WebMvcConfigurer corsConfigurer() {
  return new WebMvcConfigurer() {
   @Override
   public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**").allowedOrigins(localDomainFront);
    registry.addMapping("/**").allowedMethods("POST", "PUT", "GET", "DELETE", "OPTIONS");
   }
  };
 }
}

Configuración del modelo de usuario

La clase User tendría que tener como mínimo usuario y contraseña. Sin embargo, cuando usamos Spring Security, la recomendación es que además la base de datos tenga las propiedades indicadas por la interfaz de UserDetails.

Para que el login funcione correctamente, en la base de datos, tendríamos que establecer con valor true las propiedades accountNonExpired, accountNonLocked, credentialsNonExpired, enabled.

package com.app.model;

import java.util.ArrayList;
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Entity
@Table(name = "user")
public class User implements UserDetails {
 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY) // Para generar números autoincrementados
 private int id;
 private String username;
 private String password;
 private boolean accountNonExpired;
 private boolean accountNonLocked;
 private boolean credentialsNonExpired;
 private boolean enabled;

 @Override
 public Collection<? extends GrantedAuthority> getAuthorities() {
  // Devolvemos un ArrayList vacío porque nuestra app no tiene roles
  return new ArrayList<>();
 }

 @Override
 public boolean isAccountNonExpired() {
  return accountNonExpired;
 }

 @Override
 public boolean isAccountNonLocked() {
  return accountNonLocked;
 }

 @Override
 public boolean isCredentialsNonExpired() {
  return credentialsNonExpired;
 }

 @Override
 public boolean isEnabled() {
  return enabled;
 }

 @Override
 public String getUsername() {
  return username;
 }

 @Override
 public String getPassword() {
  return password;
 }
}

application.properties

...
app.local-domain-front=http://localhost:5173

Ejemplo de petición que haremos a nuestra aplicación de Spring

axios.post("http://localhost:8080/login",{},
  {
    headers: {
      "Content-Type": "application/json",
      Authorization: "basic " + btoa(userName + ":" + password),
    },
  }
).then((response) => console.log(response),(error) => console.log(error));

Ejercicio – Login y acceso a un endpoint restringido

Hacer una aplicación front que accederá a dos endpoints

  • /login → para la autentificación
  • /test → para probar
Curso de Spring Boot | Login con Spring Security 3

Código del front:

api.js

export const login = async (username, password) => {
    const token = btoa(username + ":" + password);
    const response = await i.post("/login",{},
    {
        headers: {
            "Content-Type": "application/json",
            Authorization: "basic " + token,
        },
    });
    if(response.data.resp === "Login ok"){
        setAuth(token);
    }
}


export const setAuth = async (token) => {
    i.defaults.headers.common.Authorization = `basic ${token}`;
};

export const test = () => {
 i.get("/users");
}

Probando el acceso

Para probar el acceso con Postman a una url que requiera autentificación, usaremos la siguiente configuración:

Curso de Spring Boot | Login con Spring Security 4

Recuerda que debemos tener protegida la url de acceso y para ello debe estar en el siguiente listado:

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
 http.csrf(csrf -> csrf.disable()); // Deshabilitamos la protección contra ataques Cross-site request forgery
 http.authorizeHttpRequests((requests) -> {
 try {
  requests.requestMatchers("/evento/","/evento","/user/","/user", "/logout").permitAll().anyRequest().authenticated();
  ...
}

Ten en cuenta que para que el acceso esté habilitado, los campos account_non_expired, account_not_locked, credentials_non_expired, enabled deben valer 1.

INSERT INTO user (id,  account_non_expired, account_non_locked, credentials_non_expired, enabled,  username,password) VALUES ('1', 1, 1, 1, 1,'paco','$2a$10$US2YnaQEPd3geX5s.SsjfOGTwPN8qwSHa1NhiQ82.qJ1Ed1HYPtAi') // la contraseña es hola

Mensajería

Curso de Spring Boot | Login con Spring Security 5

Al generar el JSON, las relaciones oneToMany pueden ser problemáticas, si queremos que no aparezcan en el JSON, podemos utilizar este código en la entidad:

@JsonIgnoreProperties({"mensajesEnviados","mensajesRecibidos"})
public class Usuario{
@Entity
public class User{
 ...
 //Siempre que tengamos una relación OneToMany, usaremos el mappedBy
 @OneToMany(mappedBy = "remitente")
 private List <Mensaje> mensajesEnviados;
 @OneToMany(mappedBy = "destinatario")
 private List <Mensaje> mensajesRecibidos;
@Entity
public class Mensaje {
 @Id
 @GeneratedValue(strategy=GenerationType.IDENTITY)//Para generar números autoincrementados
 private int id;
 @ManyToOne
 private User remitente;
 @ManyToOne
 private User destinatario;
 private String mensaje;
mensaje
idremitentedestinatariomensaje
111Hola!
212Qué pasa tron!
311Hola Caracola
412Te odio
usuario
idnombrepass
1ppkk
2kkkk

Foro

Curso de Spring Boot | Login con Spring Security 6
hilo
idid_usuarionombre_hilotexto_hilo
71Escoger ordenadorDónde comprar uno bueno
81¿Qué opinas de Ubuntu?Dudas sobre Ubuntu

Probando el registro

Para que un nuevo usuario se registre, es necesario encriptar su contraseña para almacenarla en la base de datos. Para ello, usaremos un código similar a este:

@Autowired
private UserRepository userRepository;
 
@Autowired
BCryptPasswordEncoder encoder;

public User saveUser(User user) {
 String encryptedPassword = encoder.encode(user.getPassword());
 user.setPassword(encryptedPassword);
 return userRepository.save(user);
}