Login con Spring Security y securizar API

Por 9.99€ al mes tendrás acceso completo a todos los cursos. Sin matrícula ni permanencia.

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.

Configuración del entorno

Dependencias

El paquete base del proyecto que vamos a crear será com.pablomonteserin.basicauthentification.

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

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

Configuración del contexto de seguridad de Spring

com.pablomonteserin.withspringsecurity.config.SecurityConfig.java

package com.pablomonteserin.basicauthentification.config;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@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
											// para evitar su configuración en la cabecera de la peticción
		// Definimos que urls serán públicas
		http.authorizeHttpRequests((requests) -> {
			try {
				requests.requestMatchers("/login", "/login?logout", "/logout").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");
			}
		};
	}
}

La clase anterior usa esta clase para encriptar los datos:

com.pablomonteserin.withspringsecurity.config.Encoder.java

package com.pablomonteserin.basicauthentification.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class Encoder {

	@Bean(name = "passwordEncoder")
	BCryptPasswordEncoder passwordencoder() {
		return new BCryptPasswordEncoder();
	}
}

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.

com.pablomonteserin.basicauthentification.model.User.java

package com.pablomonteserin.basicauthentification.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;
	}
}

Flujo de una petición de autentificación

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));

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 acaso 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.pablomonteserin.basicauthentification.service;

import org.springframework.beans.factory.annotation.Autowired;
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 com.pablomonteserin.basicauthentification.model.User;
import com.pablomonteserin.basicauthentification.repository.UserRepository;

@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.pablomonteserin.basicauthentification.controller;

import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import com.pablomonteserin.basicauthentification.model.User;

@RestController
public class BasicAuthController {

	@PostMapping(path = "/login")
	public String basicauth(Principal principal) {
		return principal.getName();
	}
}

Por 9.99€ al mes tendrás acceso completo a todos los cursos. Sin matrícula ni permanencia.