- Inclusión de dependencias
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>
- 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;
}
- 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;
}
}
- 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();
}
}
- 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);
}
}
- 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();
}
}
- 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();
}
}