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