Configuración de Seguridad con Spring Boot y JWT

  1. Inclusión de dependencias

https://central.sonatype.com/

lombok y mybatis-plus deben agregarse según las necesidades específicas del proyecto

<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>
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-api</artifactId>
	<version>0.12.6</version>
</dependency>
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-impl</artifactId>
	<version>0.12.6</version>
</dependency>
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-jackson</artifactId>
	<version>0.12.6</version>
</dependency>


  1. Implementación de la clase de entidad PerfilUsuario
create table perfil_usuario
(
    id       int      not null
        primary key,
    nombre_usuario tinytext not null,
    contrasena tinytext not null,
    avatar   tinytext null,
    tipo_usuario        int          null comment 'Tipo de usuario, para gestión de permisos',
);


@Data
@NoArgsConstructor
@AllArgsConstructor
public class PerfilUsuario {
    
    @TableId(type = IdType.AUTO)
    private Integer id;

    private String nombreUsuario;

    private String contrasena;

    private String avatar;
    
    private Integer tipoUsuario;
}


  1. Implementación de la clase DetallesPerfilUsuario

Hereda de la interfaz UserDetailsService, utilizada para conectar con la base de datos

public class DetallesPerfilUsuario implements UserDetails {

    private PerfilUsuario perfilUsuario;

    public DetallesPerfilUsuario(PerfilUsuario perfilUsuario) {
        this.perfilUsuario = perfilUsuario;
    }

    public PerfilUsuario getPerfilUsuario() {
        return perfilUsuario;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> listaPermisos = new ArrayList<>();
        listaPermisos.add((GrantedAuthority) () -> switch (perfilUsuario.getTipoUsuario()) {
            case TIPO_ADMIN -> "administrador"; // Administrador
            case TIPO_MODERADOR -> "moderador"; // Moderador
            default -> "usuario"; // Usuario estándar
        });

        return listaPermisos;
    }

    @Override
    public String getPassword() {
        return perfilUsuario.getContrasena();
    }

    @Override
    public String getUsername() {
        return perfilUsuario.getNombreUsuario();
    }

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

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

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

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


  1. Implementación de la clase ManejadorJwt

Clase utilitaria para JWT, encargada de crear y analizra tokens JWT

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

@Component
public class ManejadorJwt {
    public static final long DURACION_VIDA_JWT = 60 * 60 * 1000L * 24 * 14;  // 14 días de validez
    public static final CLAVE_SECRETA_JWT = "SDFGjhdsfalshdSAFKSDBJKJBASKJdsasgdkfkjsdhgfakjbfjk14651354565fasdf1353453524324fHFdsjkdsfds12123sd2131afasdfac"; // Debe ser suficientemente larga

    public static String generarUUID() {
        return UUID.randomUUID().toString().replaceAll("-", "");
    }

    public static String crearTokenJWT(String asunto) {
        JwtBuilder constructor = obtenerConstructorJWT(asunto, null, generarUUID());
        return constructor.compact();
    }

    private static JwtBuilder obtenerConstructorJWT(String asunto, Long milisegundosVida, String uuid) {
        SecretKey claveSecreta = generarClaveSecreta();
        long milisegundosActuales = System.currentTimeMillis();
        Date fechaActual = new Date(milisegundosActuales);
        if (milisegundosVida == null) {
            milisegundosVida = ManejadorJwt.DURACION_VIDA_JWT;
        }

        long milisegamosExpiracion = milisegundosActuales + milisegundosVida;
        Date fechaExpiracion = new Date(milisegamosExpiracion);
        return Jwts.builder()
                .id(uuid)
                .subject(asunto)
                .issuer("sg")
                .issuedAt(fechaActual)
                .signWith(claveSecreta)
                .expiration(fechaExpiracion);
    }

    public static SecretKey generarClaveSecreta() {
        byte[] codificarClave = Base64.getDecoder().decode(ManejadorJwt.CLAVE_SECRETA_JWT);
        return new SecretKeySpec(codificarClave, 0, codificarClave.length, "HmacSHA256");
    }

    public static Claims analizarJWT(String jwt) throws Exception {
        SecretKey claveSecreta = generarClaveSecreta();
        return Jwts.parser()
                .verifyWith(claveSecreta)
                .build()
                .parseSignedClaims(jwt)
                .getPayload();
    }
}


  1. Implementación de la clase FiltroAutenticacionJwt

Se encarga de verificar los tokens JWT, y si la verificación es exitosa, inyecta la información del usuario en el contexto

@Component
public class FiltroAutenticacionJwt extends OncePerRequestFilter {
    
    @Autowired
    private MapperUsuario mapperUsuario;
 
    @Override
    protected void doFilterInternal(HttpServletRequest peticion, @NotNull HttpServletResponse respuesta, @NotNull FilterChain cadenaFiltros) throws ServletException, IOException {
        String token = peticion.getHeader("Authorization");
 
        if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
            cadenaFiltros.doFilter(peticion, respuesta);
            return;
        }
 
        token = token.substring(7);
 
        String idUsuario;
        try {
            Claims claims = ManejadorJwt.analizarJWT(token);
            idUsuario = claims.getSubject();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
 
        PerfilUsuario usuario = mapperUsuario.seleccionarPorId(Integer.parseInt(idUsuario));
 
        if (usuario == null) {
            throw new RuntimeException("Usuario no autenticado");
        }

        DetallesPerfilUsuario usuarioAutenticado = new DetallesPerfilUsuario(usuario);

        TokenAutenticacionUsuarioPassword tokenAutenticacion =
                new TokenAutenticacionUsuarioPassword(usuarioAutenticado, null, usuarioAutenticado.getAuthorities());
 
        SecurityContextHolder.getContext().setAuthentication(tokenAutenticacion);
 
        cadenaFiltros.doFilter(peticion, respuesta);
    }
}


  1. Adición de la clase de configuración ConfiguracionSeguridad

Implementa el almacenamiento cifrado de conrtaseñas de usuario y permite el acceso a endpoints de login y registro

@Configuration
@EnableWebSecurity
public class ConfiguracionSeguridad {
 
    private final FiltroAutenticacionJwt filtroAutenticacionJwt;
 
    public ConfiguracionSeguridad(FiltroAutenticacionJwt filtroAutenticacionJwt) {
        this.filtroAutenticacionJwt = filtroAutenticacionJwt;
    }
 
    @Bean
    public CodificadorContrasena codificadorContrasena() {
        return new CodificadorBCrypt();
    }
 
    @Bean
    public CadenaFiltrosSeguridad cadenaFiltrosSeguridad(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .gestionSesion(session -> session
                        .politicaCreacionSesion(SessionCreationPolicy.STATELESS))
                .solicitudesAutorizacion(auth -> auth
                        .requestMatchers("/usuario/login", "/usuario/registro").permitAll()
                        .requestMatchers(HttpMethod.OPTIONS).permitAll()
                        .requestMatchers(HttpMethod.POST, "/publicacion/top").hasRole("MODERADOR")
                        .requestMatchers(HttpMethod.POST, "/publicacion/maravilla").hasRole("MODERADOR")
                        .requestMatchers(HttpMethod.POST, "/publicacion/eliminar").hasRole("ADMINISTRADOR")
                        .anyRequest().authenticated());
 
        http.addFilterBefore(filtroAutenticacionJwt, FiltroAutenticacionUsuarioPassword.class);
 
        return http.build();
    }
 
    @Bean
    public AdministradorAutenticacion adminAutenticacion(HttpSecurity http) throws Exception {
        ConstructorAdministradorAutenticacion constructorAdministradorAutenticacion =
                http.getSharedObject(ConstructorAdministradorAutenticacion.class);
        return constructorAdministradorAutenticacion.build();
    }
}


  1. Implementación de la interfaz ServicioDetallesUsuario, búsqueda de usuario por nombre
@Service
public class ServicioDetallesUsuarioImpl implements ServicioDetallesUsuario {

    @Autowired
    private MapperUsuario mapperUsuario;

    @Override
    public UserDetails cargarUsuarioPorNombre(String nombreUsuario) throws NombreUsuarioNoEncontradoException {
        ContenedorConsulta<PerfilUsuario> contenedorConsulta = new ContenedorConsulta<>();
        contenedorConsulta.eq("nombre_usuario", nombreUsuario);
        PerfilUsuario usuario = mapperUsuario.seleccionarUno(contenedorConsulta);
        if (usuario == null) {
            throw new RuntimeException("El usuario no existe");
        }

        return new DetallesPerfilUsuario(usuario);
    }

}


Nota 1: Formato de referencia para que el cliente realice solicitudes a APIs protegidas mediente axios

const obtenerDatos = async (pagina: any, tamano: any) => {
    const respuesta = await axios({
      method: "get",
      url: "http://localhost:8080/api/registro/obtenerLista",
      headers: {
        Authorization: `Bearer ${useUsuarioStore.getState().usuario.token}`,
      },
      params: {
        pagina: pagina, // Número de página actual
        tamano_pagina: tamano, // Tamaño de página
      },
    });

	console.log(respuesta.data);
  };


Nota 2: Método para configurar APIs que solo permiten acceso local desde 127.0.0.1

@Configuration
@EnableWebSecurity
public class ConfiguracionSeguridad {

    private static final ComparadorIp IP_PERMITIDA = new ComparadorIp("127.0.0.1");

    @Bean
    public CadenaFiltrosSeguridad cadenaFiltrosSeguridad(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .gestionSesion(session -> session
                        .politicaCreacionSesion(SessionCreationPolicy.STATELESS))
                .solicitudesAutorizacion(auth -> auth
                        .requestMatchers("/bot/agregar")
                        .access(((autenticacion, contexto) -> new DecisionAutorizacion(IP_PERMITIDA.matches(contexto.getRequest()))))
                        .requestMatchers(HttpMethod.OPTIONS).permitAll()
                        .anyRequest().authenticated());

        return http.build();
    }

}


Etiquetas: Spring Boot JWT seguridad autenticación java

Publicado el 7-13 00:42