Voltar para o Blog
Spring Boot Security: Best Practices for Production Applications
11 min de leitura
Spring BootSecurityBest PracticesAuthentication
Segurança não é opcional. Aprenda como implementar autenticação robusta, autorização granular, e proteger sua aplicação Spring Boot contra as vulnerabilidades mais comuns.
Configuração Básica de Segurança
Spring Security 6.0+ Configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
}Autenticação JWT
JWT Token Provider
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private long jwtExpirationMs;
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
return Keys.hmacShaKeyFor(keyBytes);
}
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationMs);
return Jwts.builder()
.setSubject(userPrincipal.getId().toString())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(getSigningKey(), SignatureAlgorithm.HS512)
.compact();
}
public Long getUserIdFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
public boolean validateToken(String authToken) {
try {
Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(authToken);
return true;
} catch (JwtException | IllegalArgumentException e) {
log.error("JWT validation error: {}", e.getMessage());
}
return false;
}
}JWT Authentication Filter
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
log.error("Could not set user authentication", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}OAuth2 e Social Login
Configuração OAuth2
@Configuration
public class OAuth2Config {
@Bean
public SecurityFilterChain oauth2FilterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authorization -> authorization
.baseUri("/oauth2/authorize")
.authorizationRequestRepository(cookieAuthorizationRequestRepository())
)
.redirectionEndpoint(redirection -> redirection
.baseUri("/oauth2/callback/*")
)
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService)
)
.successHandler(oAuth2AuthenticationSuccessHandler)
.failureHandler(oAuth2AuthenticationFailureHandler)
);
return http.build();
}
@Bean
public HttpCookieOAuth2AuthorizationRequestRepository
cookieAuthorizationRequestRepository() {
return new HttpCookieOAuth2AuthorizationRequestRepository();
}
}Autorização com Method Security
Anotações de Segurança
@Configuration
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig {
// Configuração automática
}
@Service
public class UserService {
// Apenas ADMIN pode executar
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) {
userRepository.deleteById(userId);
}
// Usuário pode acessar apenas seus próprios dados
@PreAuthorize("#userId == authentication.principal.id")
public User getUserProfile(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
}
// Expressão complexa
@PreAuthorize("hasRole('ADMIN') or (#userId == authentication.principal.id and hasRole('USER'))")
public void updateUser(Long userId, UserUpdateRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
user.update(request);
userRepository.save(user);
}
// Post-filter results
@PostFilter("filterObject.userId == authentication.principal.id or hasRole('ADMIN')")
public List<Document> getUserDocuments() {
return documentRepository.findAll();
}
}Proteção CSRF
CSRF Token Configuration
@Configuration
public class CsrfConfig {
@Bean
public SecurityFilterChain csrfFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers("/api/public/**")
)
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class);
return http.build();
}
}
// Filter para enviar CSRF token no cookie
public class CsrfCookieFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf");
// Force token generation
csrfToken.getToken();
filterChain.doFilter(request, response);
}
}CORS Configuration
Configuração Segura de CORS
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// Especifique origins permitidas (nunca use "*" em produção com credentials)
configuration.setAllowedOrigins(Arrays.asList(
"https://app.example.com",
"https://admin.example.com"
));
configuration.setAllowedMethods(Arrays.asList(
"GET", "POST", "PUT", "DELETE", "OPTIONS"
));
configuration.setAllowedHeaders(Arrays.asList(
"Authorization",
"Content-Type",
"X-Requested-With",
"X-CSRF-TOKEN"
));
configuration.setExposedHeaders(Arrays.asList(
"X-Total-Count",
"X-CSRF-TOKEN"
));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}
}Rate Limiting
Bucket4j Rate Limiter
@Component
public class RateLimitingFilter extends OncePerRequestFilter {
private final Map<String, Bucket> cache = new ConcurrentHashMap<>();
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String key = getClientKey(request);
Bucket bucket = resolveBucket(key);
ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);
if (probe.isConsumed()) {
response.addHeader("X-Rate-Limit-Remaining",
String.valueOf(probe.getRemainingTokens()));
filterChain.doFilter(request, response);
} else {
response.setStatus(429);
response.addHeader("X-Rate-Limit-Retry-After-Seconds",
String.valueOf(probe.getNanosToWaitForRefill() / 1_000_000_000));
response.getWriter().write("Too many requests");
}
}
private Bucket resolveBucket(String key) {
return cache.computeIfAbsent(key, k -> createNewBucket());
}
private Bucket createNewBucket() {
// 100 requests per minute
Bandwidth limit = Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1)));
return Bucket.builder()
.addLimit(limit)
.build();
}
private String getClientKey(HttpServletRequest request) {
String userId = getUserId(request);
return userId != null ? userId : getClientIP(request);
}
}Security Headers
Configuração de Headers de Segurança
@Configuration
public class SecurityHeadersConfig {
@Bean
public SecurityFilterChain securityHeaders(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; " +
"script-src 'self' 'unsafe-inline'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self' data:; " +
"connect-src 'self' https://api.example.com")
)
.frameOptions(frame -> frame.deny())
.xssProtection(xss -> xss.enable())
.contentTypeOptions(Customizer.withDefaults())
.referrerPolicy(referrer -> referrer
.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
)
.permissionsPolicy(permissions -> permissions
.policy("geolocation=(self), microphone=()")
)
);
return http.build();
}
}Audit Logging
Security Event Listener
@Component
public class AuthenticationEventListener {
@Autowired
private AuditLogRepository auditLogRepository;
@EventListener
public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
String username = ((UserDetails) event.getAuthentication().getPrincipal()).getUsername();
log.info("Login successful for user: {}", username);
auditLogRepository.save(AuditLog.builder()
.username(username)
.action("LOGIN_SUCCESS")
.timestamp(LocalDateTime.now())
.ipAddress(getClientIP())
.build());
}
@EventListener
public void onAuthenticationFailure(AuthenticationFailureBadCredentialsEvent event) {
String username = (String) event.getAuthentication().getPrincipal();
log.warn("Login failed for user: {}", username);
auditLogRepository.save(AuditLog.builder()
.username(username)
.action("LOGIN_FAILED")
.timestamp(LocalDateTime.now())
.ipAddress(getClientIP())
.build());
}
}Melhores Práticas de Segurança
Checklist de Segurança
- Sempre use HTTPS em produção
- Hash passwords com BCrypt (strength 12+)
- Implemente rate limiting em endpoints públicos
- Valide e sanitize todos os inputs
- Use tokens de curta duração (15-30 minutos)
- Implemente refresh tokens para sessões longas
- Configure CORS restritivamente
- Habilite CSRF protection para formulários
- Adicione security headers apropriados
- Log eventos de segurança para auditoria
- Mantenha dependências atualizadas
- Use secrets management (Vault, AWS Secrets Manager)
Testando Segurança
Security Tests
@SpringBootTest
@AutoConfigureMockMvc
public class SecurityTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(roles = "USER")
public void whenUserAccessUserEndpoint_thenSuccess() throws Exception {
mockMvc.perform(get("/api/user/profile"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
public void whenUserAccessAdminEndpoint_thenForbidden() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isForbidden());
}
@Test
public void whenNoAuth_thenUnauthorized() throws Exception {
mockMvc.perform(get("/api/user/profile"))
.andExpect(status().isUnauthorized());
}
}Conclusão
Segurança é uma jornada contínua, não um destino. Implemente estas práticas, mantenha-se atualizado com vulnerabilidades conhecidas, e sempre assuma que seu sistema pode ser atacado.
Lembre-se: segurança em camadas é essencial. Nenhuma medida única é suficiente - combine múltiplas estratégias para criar uma defesa robusta.