Spring Boot Security Implementation
Spring Boot-specific implementations for application security using Spring Security, Jakarta Bean Validation, and framework security features.
Overview
This guide covers Spring Boot-specific security implementations. For security concepts and principles, see the Security Overview. For detailed explanations of vulnerabilities like SQL injection and XSS, see Input Validation.
Core Focus Areas:
- Spring Security configuration
- Jakarta Bean Validation integration
- Spring-specific security patterns
- Framework security features
- Input Validation - Validation concepts, SQL injection, XSS, CSRF prevention
- Authentication - OAuth 2.0, JWT, MFA patterns
- Authorization - RBAC, ABAC, access control
- Data Protection - Encryption and sensitive data handling
- Security Testing - Testing security controls
Spring Boot Input Validation
Spring Boot integrates Jakarta Bean Validation (JSR-380) for declarative input validation. For validation concepts and attack explanations, see Input Validation.
Jakarta Bean Validation Setup
Spring Boot includes Jakarta Bean Validation for declarative validation:
build.gradle:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
Basic Validation:
public record PaymentRequest(
@NotBlank(message = "Customer ID is required")
@Size(max = 50, message = "Customer ID too long")
String customerId,
@NotNull(message = "Amount is required")
@DecimalMin(value = "0.01", message = "Amount must be positive")
@DecimalMax(value = "999999.99", message = "Amount exceeds maximum")
@Digits(integer = 6, fraction = 2, message = "Invalid amount format")
BigDecimal amount,
@NotBlank(message = "Currency is required")
@Pattern(regexp = "^[A-Z]{3}$", message = "Currency must be 3 uppercase letters")
String currency,
@Size(max = 500, message = "Description too long")
String description
) {}
What each annotation does:
@NotBlank: Ensures string is not null, empty, or whitespace-only@NotNull: Ensures value is present (for non-strings)@Size: Limits string length (prevents buffer overflow, database errors)@Pattern: Validates format with regex (e.g., currency codes, phone numbers)@DecimalMin/@DecimalMax: Range checking for numbers@Digits: Validates decimal precision (integer + fraction digits)
Enable validation in controller:
@RestController
@RequestMapping("/api/v1/payments")
@Validated // Enables validation on this controller
public class PaymentController {
@PostMapping
public PaymentResponse createPayment(@Valid @RequestBody PaymentRequest request) {
// If validation fails, Spring throws MethodArgumentNotValidException
// Handle this in global exception handler
return paymentService.createPayment(request);
}
}
Why @Valid matters:
Without @Valid, Spring ignores validation annotations on the request object. The @Valid annotation triggers the validation framework to check constraints before the method executes.
Custom Validation
Some validation rules can't be expressed with annotations:
@Component
public class PaymentValidator {
public void validate(PaymentRequest request) {
// Business rule: USD payments must be at least $1
if ("USD".equals(request.currency())
&& request.amount().compareTo(new BigDecimal("1.00")) < 0) {
throw new ValidationException("USD payments must be at least $1.00");
}
// Business rule: Maximum daily transaction limit
if (request.amount().compareTo(new BigDecimal("10000.00")) > 0) {
// This requires additional validation - check customer's daily total
validateDailyLimit(request.customerId(), request.amount());
}
// Check for suspicious patterns
if (isSuspicious(request.description())) {
log.warn("Suspicious payment description: customerId={}", request.customerId());
// Don't reject immediately - flag for review
flagForReview(request);
}
}
private boolean isSuspicious(String description) {
if (description == null) return false;
// Look for patterns indicating potential fraud
String lower = description.toLowerCase();
return lower.contains("test")
|| lower.contains("fraud")
|| lower.matches(".*<script.*") // Basic XSS attempt
|| lower.contains("../"); // Path traversal attempt
}
}
Why separate validator class:
- Testability: Easy to unit test validation logic
- Reusability: Use same validator for API, batch jobs, message handlers
- Clarity: Complex validation doesn't clutter controller
Use in service:
@Service
@RequiredArgsConstructor
public class PaymentService {
private final PaymentValidator validator;
private final PaymentRepository repository;
public Payment createPayment(PaymentRequest request) {
// Bean Validation already passed (from @Valid in controller)
// Now apply business rules
validator.validate(request);
// Proceed with business logic
var payment = buildPayment(request);
return repository.save(payment);
}
}
SQL Injection Prevention in Spring Boot
Spring Boot's data access frameworks (Spring Data JPA, JdbcTemplate) prevent SQL injection through parameterized queries. For detailed explanation of SQL injection attacks, see Input Validation - SQL Injection.
Safe Patterns with Spring Data JPA
Spring Data JPA (safe by default):
@Repository
public interface PaymentRepository extends JpaRepository<Payment, String> {
// Parameters automatically escaped
List<Payment> findByCustomerId(String customerId);
// Named parameters are safe
@Query("SELECT p FROM Payment p WHERE p.customerId = :customerId")
List<Payment> findPayments(@Param("customerId") String customerId);
}
JdbcTemplate (use placeholders):
// GOOD: SAFE: Parameterized query
public List<Payment> findByCustomerId(String customerId) {
String sql = "SELECT * FROM payments WHERE customer_id = ?";
return jdbcTemplate.query(sql, paymentRowMapper, customerId);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^
// Parameter automatically escaped
}
// GOOD: SAFE: Named parameters
public List<Payment> findByCustomerIdAndStatus(String customerId, PaymentStatus status) {
String sql = "SELECT * FROM payments WHERE customer_id = :customerId AND status = :status";
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("customerId", customerId)
.addValue("status", status.name());
return namedParameterJdbcTemplate.query(sql, params, paymentRowMapper);
}
Why this works: The database driver treats parameters as data, not SQL code. Even if user input contains SQL syntax, it's escaped and treated as a literal string value.
Dynamic queries (when query structure changes):
public List<Payment> searchPayments(PaymentSearchCriteria criteria) {
StringBuilder sql = new StringBuilder("SELECT * FROM payments WHERE 1=1");
MapSqlParameterSource params = new MapSqlParameterSource();
// Build query dynamically but use parameters
if (criteria.customerId() != null) {
sql.append(" AND customer_id = :customerId");
params.addValue("customerId", criteria.customerId());
}
if (criteria.minAmount() != null) {
sql.append(" AND amount >= :minAmount");
params.addValue("minAmount", criteria.minAmount());
}
if (criteria.status() != null) {
sql.append(" AND status = :status");
params.addValue("status", criteria.status().name());
}
return namedParameterJdbcTemplate.query(sql.toString(), params, paymentRowMapper);
}
Key points:
- SQL structure is built safely (no user input in string concatenation)
- User values are always passed as parameters
- Database handles escaping automatically
XSS Prevention in Spring Boot
Spring Boot's JSON serialization automatically escapes special characters. For detailed XSS attack explanations and frontend prevention, see Input Validation - XSS Prevention.
Automatic JSON Escaping
For REST APIs returning JSON, Spring Boot handles encoding automatically:
@RestController
public class PaymentController {
@GetMapping("/{id}")
public PaymentResponse getPayment(@PathVariable String id) {
return paymentService.getPayment(id);
// Spring's JSON serializer automatically escapes special characters
// "<script>" becomes "\\u003Cscript\\u003E" in JSON
}
}
For HTML responses (if you render HTML from Spring Boot):
// Thymeleaf automatically escapes by default
<p th:text="${payment.description}">Description</p>
// Even if description contains <script>, it's rendered as text
// To render raw HTML (DANGEROUS - only for trusted content):
<p th:utext="${trustedContent}">Content</p>
Input Sanitization
For fields that accept rich text, sanitize on input:
@Component
public class HtmlSanitizer {
private final PolicyFactory policy;
public HtmlSanitizer() {
// Define allowed HTML tags and attributes
this.policy = new HtmlPolicyBuilder()
.allowElements("p", "br", "strong", "em", "ul", "ol", "li")
.allowAttributes("class").onElements("p")
.toFactory();
}
public String sanitize(String input) {
if (input == null) return null;
// Removes any disallowed HTML, keeps safe tags
return policy.sanitize(input);
}
}
// Usage
@Service
@RequiredArgsConstructor
public class CommentService {
private final HtmlSanitizer sanitizer;
public Comment createComment(String userId, String text) {
// Strip dangerous HTML before saving
String safeText = sanitizer.sanitize(text);
return commentRepository.save(new Comment(userId, safeText));
}
}
build.gradle (for OWASP Java HTML Sanitizer):
dependencies {
implementation 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
}
Sensitive Data Protection in Spring Boot
For general data protection principles, see Data Protection. This section covers Spring-specific encryption utilities.
Spring Security Crypto for Encryption
For sensitive data at rest, use Spring's encryption support:
@Configuration
public class EncryptionConfig {
@Bean
public TextEncryptor textEncryptor() {
// Get encryption key from environment (not hardcoded!)
String salt = System.getenv("ENCRYPTION_SALT");
String secret = System.getenv("ENCRYPTION_SECRET");
return Encryptors.text(secret, salt);
}
}
@Service
@RequiredArgsConstructor
public class CustomerService {
private final TextEncryptor encryptor;
private final CustomerRepository repository;
public Customer createCustomer(CustomerRequest request) {
// Encrypt SSN before saving
String encryptedSsn = encryptor.encrypt(request.ssn());
var customer = new Customer();
customer.setName(request.name());
customer.setEncryptedSsn(encryptedSsn); // Store encrypted
return repository.save(customer);
}
public String getCustomerSsn(String customerId) {
var customer = repository.findById(customerId)
.orElseThrow(() -> new NotFoundException("Customer not found"));
// Decrypt when needed
return encryptor.decrypt(customer.getEncryptedSsn());
}
}
build.gradle:
dependencies {
implementation 'org.springframework.security:spring-security-crypto'
}
Secure Error Handling with Spring Boot
Spring Boot's error handling can inadvertently expose sensitive information. Configure secure error responses using @RestControllerAdvice.
Secure Exception Handling
@RestControllerAdvice
public class SecureExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ProblemDetail handleGenericError(Exception ex) {
// Log full details internally for debugging
log.error("Payment processing error", ex);
// Return generic message to client
return ProblemDetail.forStatusAndDetail(
HttpStatus.INTERNAL_SERVER_ERROR,
"An error occurred processing your request"
);
}
@ExceptionHandler(PaymentNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ProblemDetail handleNotFound(PaymentNotFoundException ex) {
// Generic message - don't expose if resource exists
return ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND,
"Payment not found"
);
}
}
Production Error Configuration
# application-prod.yml
server:
error:
include-message: never # Don't expose exception messages
include-stacktrace: never # Never include stack traces
include-binding-errors: never # Don't expose validation details
Path Traversal Prevention in Spring Boot
For path traversal attack explanations, see Input Validation. This section shows Spring Boot-specific file handling patterns.
Secure File Service Pattern
@Service
public class FileService {
private static final Path BASE_DIRECTORY = Paths.get("/app/files").toAbsolutePath();
public byte[] getFile(String filename) throws IOException {
// Validate filename doesn't contain path separators
if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) {
throw new SecurityException("Invalid filename");
}
// Build path and normalize
Path filePath = BASE_DIRECTORY.resolve(filename).normalize();
// Verify final path is still within BASE_DIRECTORY
if (!filePath.startsWith(BASE_DIRECTORY)) {
throw new SecurityException("Path traversal attempt detected");
}
// Additional check: file must exist and be a regular file
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
throw new FileNotFoundException("File not found");
}
return Files.readAllBytes(filePath);
}
}
What's happening:
- Reject filenames with
.,/,\(path separators) normalize()resolves.and..in path- Verify final path still starts with base directory
- Check file exists and is a regular file (not directory)
Mass Assignment Prevention with DTOs
Mass assignment vulnerabilities occur when binding request data directly to entities. For mass assignment attack explanations, see Input Validation. Spring Boot prevents this using DTOs (Data Transfer Objects).
DTO Pattern
// Request DTO - only fields user can set
public record CreateUserRequest(
@NotBlank String name,
@Email String email
// No isAdmin field - user can't set it
) {}
// Service sets internal fields
@Service
@RequiredArgsConstructor
public class UserService {
public User createUser(CreateUserRequest request) {
var user = new User();
user.setId(UUID.randomUUID().toString());
user.setName(request.name());
user.setEmail(request.email());
user.setIsAdmin(false); // Controlled internally, not from user input
return userRepository.save(user);
}
// Only admins can promote users
@PreAuthorize("hasRole('ADMIN')")
public User promoteToAdmin(String userId) {
var user = userRepository.findById(userId)
.orElseThrow(() -> new NotFoundException("User not found"));
user.setIsAdmin(true);
return userRepository.save(user);
}
}
Spring Security Configuration
Spring Security provides comprehensive security features including authentication, authorization, and protection against common attacks.
SecurityFilterChain Configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self'")
)
.frameOptions(frame -> frame.deny())
// X-XSS-Protection header omitted: removed from Spring Security 6.1+
// and dropped by modern browsers. Rely on Content-Security-Policy instead.
)
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**", "/actuator/health").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
// For JWT/OAuth2 REST APIs: use STATELESS (no server-side sessions)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
}
Key Security Headers:
- CSRF Token: Prevents cross-site request forgery (see Input Validation - CSRF)
- Content Security Policy: Mitigates XSS attacks
- X-Frame-Options: Prevents clickjacking
Summary
Spring Boot Security Checklist:
- Jakarta Bean Validation: Use
@Validwith@NotNull,@Size,@Patternannotations - Spring Data JPA: Automatically prevents SQL injection with parameterized queries
- JdbcTemplate: Use
?placeholders or named parameters, never string concatenation - JSON Serialization: Spring Boot automatically escapes output
- DTOs: Prevent mass assignment by using dedicated request/response objects
- Spring Security Crypto: Use
TextEncryptorfor sensitive data encryption - Error Handling: Configure
@RestControllerAdviceto hide internal details - SecurityFilterChain: Enable CSRF, CSP, frame options
- Production Config: Disable stack traces and error details in
application-prod.yml
- Input Validation - Validation concepts, SQL injection, XSS, CSRF
- Authentication - OAuth 2.0, JWT, MFA
- Authorization - RBAC, ABAC, access control
- Data Protection - Encryption and sensitive data
- Security Testing - Testing security controls
- Spring Boot API Design - Validation in REST APIs