Spring Boot provides robust security features through Spring Security - configure them properly for production on Heroku.
Expedited WAF for Spring Boot
Expedited WAF adds network level request filtering, preventing malicious attacks from ever touching your application. This enables true defense in depth for your Spring Boot app—even if your framework has a vulnerability or is improperly configured, Expedited WAF dynamically blocks threats before they reach your code.
From experience, we’ve seen many applications where attacks technically didn’t succeed, but burned through so many server resources that the application went down anyway. By filtering malicious requests at the network edge, Expedited WAF protects both your security and your availability.
New Security Features for Your Spring Boot App
These capabilities aren’t available out of the box with Spring Boot, but Expedited WAF adds them instantly:
- Block IP Addresses - Stop malicious actors by IP or CIDR range
- Block User Agents - Filter out bad bots and scrapers
- Block by Geolocation - Restrict traffic by country or region
- DDoS Protection - Stop attacks with CAPTCHA challenges
- Block Anonymous Proxies - Prevent traffic from VPNs and proxy networks
Spring Security Configuration
-
Add Spring Security dependency
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> -
Configure HTTP Security
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .and() .authorizeHttpRequests(auth -> auth .requestMatchers("/", "/public/**").permitAll() .anyRequest().authenticated() ) .formLogin() .and() .logout().logoutSuccessUrl("/"); return http.build(); } } -
Enable HTTPS enforcement
@Configuration public class WebSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.requiresChannel() .anyRequest() .requiresSecure(); return http.build(); } } -
Configure security headers
http .headers(headers -> headers .frameOptions().deny() .xssProtection().and() .contentSecurityPolicy("default-src 'self'") );
Authentication & Password Security
-
Use BCrypt for password encoding
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(12); // strength 12 } // Encoding passwords String encoded = passwordEncoder.encode(rawPassword); -
Implement UserDetailsService
@Service public class CustomUserDetailsService implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User not found")); return org.springframework.security.core.userdetails.User .withUsername(user.getUsername()) .password(user.getPassword()) .roles(user.getRoles().toArray(new String[0])) .build(); } } -
Implement account locking
@Entity public class User { private boolean accountNonLocked = true; private int failedAttempts = 0; private Date lockTime; // Lock account after 5 failed attempts public void incrementFailedAttempts() { this.failedAttempts++; if (this.failedAttempts >= 5) { this.accountNonLocked = false; this.lockTime = new Date(); } } }
JWT Authentication (for APIs)
-
Implement JWT authentication
@Component public class JwtTokenProvider { @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private long expiration; public String generateToken(Authentication authentication) { UserDetails userDetails = (UserDetails) authentication.getPrincipal(); Date now = new Date(); Date expiryDate = new Date(now.getTime() + expiration); return Jwts.builder() .setSubject(userDetails.getUsername()) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(secret).parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { return false; } } } -
Create JWT authentication filter
public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtTokenProvider tokenProvider; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String jwt = getJwtFromRequest(request); if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { String username = tokenProvider.getUsernameFromToken(jwt); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } }
Input Validation
-
Use Bean Validation
public class UserDTO { @NotBlank(message = "Username is required") @Size(min = 3, max = 50) private String username; @NotBlank(message = "Email is required") @Email(message = "Invalid email format") private String email; @NotBlank(message = "Password is required") @Size(min = 12, message = "Password must be at least 12 characters") private String password; } @PostMapping("/register") public ResponseEntity<?> register(@Valid @RequestBody UserDTO userDTO) { // Process validated data } -
Sanitize input
import org.springframework.web.util.HtmlUtils; String sanitized = HtmlUtils.htmlEscape(userInput);
SQL Injection Prevention
-
Use JPA/Hibernate parameterized queries
// Good - parameterized @Query("SELECT u FROM User u WHERE u.email = :email") User findByEmail(@Param("email") String email); // Bad - SQL injection risk @Query(value = "SELECT * FROM users WHERE email = '" + email + "'", nativeQuery = true) -
Use Criteria API for dynamic queries
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<User> query = cb.createQuery(User.class); Root<User> user = query.from(User.class); query.select(user).where(cb.equal(user.get("email"), email));
Database Security
-
Configure Heroku Postgres
# application.properties spring.datasource.url=${DATABASE_URL} spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect spring.datasource.hikari.maximum-pool-size=5 -
Enable SSL for database
spring.datasource.url=${DATABASE_URL}?sslmode=require -
Don’t log SQL queries in production
spring.jpa.show-sql=false logging.level.org.hibernate.SQL=WARN
CORS Configuration
- Configure CORS properly
@Configuration public class CorsConfig { @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("https://yourdomain.com") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }; } }
Environment & Secrets
-
Use application.properties for configuration
# application.properties spring.datasource.url=${DATABASE_URL} jwt.secret=${JWT_SECRET} jwt.expiration=86400000 -
Store secrets in Heroku config
heroku config:set DATABASE_URL='...' heroku config:set JWT_SECRET='...' -
Use different profiles for environments
# application-prod.properties spring.profiles.active=prod logging.level.root=WARN
Error Handling
-
Don’t expose stack traces
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public ResponseEntity<?> handleGlobalException(Exception ex) { // Don't expose internal details return ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) .body("An error occurred"); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<?> handleValidationExceptions( MethodArgumentNotValidException ex) { Map<String, String> errors = new HashMap<>(); ex.getBindingResult().getAllErrors().forEach(error -> { String fieldName = ((FieldError) error).getField(); String errorMessage = error.getDefaultMessage(); errors.put(fieldName, errorMessage); }); return ResponseEntity.badRequest().body(errors); } } -
Configure error pages
server.error.include-stacktrace=never server.error.include-message=never
Logging
-
Configure logging
logging.level.root=INFO logging.level.org.springframework.security=INFO logging.file.name=/var/log/application.log -
Don’t log sensitive data
- Passwords, tokens, API keys
- Personal information
- Credit card data
Actuator Security
-
Secure Spring Boot Actuator endpoints
management.endpoints.web.exposure.include=health,info management.endpoint.health.show-details=when-authorizedhttp.authorizeHttpRequests(auth -> auth .requestMatchers("/actuator/**").hasRole("ADMIN") );
File Upload Security
-
Validate file uploads
@PostMapping("/upload") public ResponseEntity<?> uploadFile(@RequestParam("file") MultipartFile file) { // Validate file type String contentType = file.getContentType(); if (!Arrays.asList("image/jpeg", "image/png").contains(contentType)) { return ResponseEntity.badRequest().body("Invalid file type"); } // Validate file size (5MB max) if (file.getSize() > 5 * 1024 * 1024) { return ResponseEntity.badRequest().body("File too large"); } // Store to S3, not local filesystem } -
Configure upload limits
spring.servlet.multipart.max-file-size=5MB spring.servlet.multipart.max-request-size=5MB
Heroku Deployment
-
Create Procfile
web: java -Dserver.port=$PORT $JAVA_OPTS -jar target/*.jar -
Specify Java version
# system.properties java.runtime.version=17 -
Configure Maven/Gradle build
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> -
Set production profile
heroku config:set SPRING_PROFILES_ACTIVE=prod
Dependencies & Updates
-
Keep dependencies updated
mvn versions:display-dependency-updates -
Scan for vulnerabilities
mvn dependency-check:check
Testing
- Write security tests
@SpringBootTest @AutoConfigureMockMvc public class SecurityTests { @Autowired private MockMvc mockMvc; @Test public void whenUnauthenticated_thenUnauthorized() throws Exception { mockMvc.perform(get("/api/users")) .andExpect(status().isUnauthorized()); } @Test public void whenInvalidCSRF_thenForbidden() throws Exception { mockMvc.perform(post("/api/users")) .andExpect(status().isForbidden()); } }