Volver a Spring Boot Básico

Seguridad con Spring Security

Spring Security proporciona autenticación y autorización completas de forma predeterminada.

Dependencia

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.12.3</version> </dependency>

Configuración de Seguridad

@Configuration @EnableWebSecurity public class ConfiguracionSeguridad { private final FiltroJwt filtroJwt; private final UserDetailsService userDetailsService; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .sessionManagement(sm -> sm .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated()) .addFilterBefore(filtroJwt, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } }

Entidad Usuario e UserDetails

@Entity @Table(name = "usuarios") public class Usuario implements UserDetails { @Id @GeneratedValue private Long id; @Column(unique = true, nullable = false) private String email; private String hashContrasena; @ElementCollection(fetch = FetchType.EAGER) @Enumerated(EnumType.STRING) private Set<Rol> roles = new HashSet<>(); @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles.stream() .map(r -> new SimpleGrantedAuthority("ROLE_" + r.name())) .collect(Collectors.toSet()); } @Override public String getPassword() { return hashContrasena; } @Override public String getUsername() { return email; } // ...demás métodos retornan true }

Servicio JWT

@Service public class ServicioJwt { @Value("${jwt.secreto}") private String secreto; @Value("${jwt.expiracion:86400000}") private long expiracionMs; public String generarToken(UserDetails userDetails) { return Jwts.builder() .subject(userDetails.getUsername()) .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() + expiracionMs)) .signWith(obtenerClaveSecreta()) .compact(); } public String extraerNombreUsuario(String token) { return extraerClaim(token, Claims::getSubject); } public boolean esTokenValido(String token, UserDetails userDetails) { String nombre = extraerNombreUsuario(token); return nombre.equals(userDetails.getUsername()) && !esTokenExpirado(token); } private boolean esTokenExpirado(String token) { return extraerClaim(token, Claims::getExpiration).before(new Date()); } private <T> T extraerClaim(String token, Function<Claims, T> resolver) { Claims claims = Jwts.parser() .verifyWith(obtenerClaveSecreta()) .build() .parseSignedClaims(token) .getPayload(); return resolver.apply(claims); } private SecretKey obtenerClaveSecreta() { return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secreto)); } }

Filtro de Autenticación JWT

@Component public class FiltroJwt extends OncePerRequestFilter { private final ServicioJwt servicioJwt; private final UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { chain.doFilter(request, response); return; } String token = authHeader.substring(7); String usuario = servicioJwt.extraerNombreUsuario(token); if (usuario != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(usuario); if (servicioJwt.esTokenValido(token, userDetails)) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } } chain.doFilter(request, response); } }

Controlador de Autenticación

@RestController @RequestMapping("/api/auth") public class ControladorAuth { private final ServicioAuth servicioAuth; @PostMapping("/registro") @ResponseStatus(HttpStatus.CREATED) public AuthResponse registro(@RequestBody @Valid RegistroRequest request) { return servicioAuth.registrar(request); } @PostMapping("/login") public AuthResponse login(@RequestBody @Valid LoginRequest request) { return servicioAuth.login(request); } }

Seguridad a Nivel de Método

@Configuration @EnableMethodSecurity public class ConfigSegMetodo { } // Uso en el servicio @Service public class ServicioAdmin { @PreAuthorize("hasRole('ADMIN')") public void eliminarUsuario(Long id) { ... } @PreAuthorize("hasRole('ADMIN') or #usuarioId == authentication.principal.id") public PerfilUsuario obtenerPerfil(Long usuarioId) { ... } }

Propiedades

jwt.secreto=tu-clave-secreta-base64-de-256-bits-aqui jwt.expiracion=86400000