- Published on
7 - Spring Security tutorial part two
- Authors

- Name
- Samreach YAN
Table of Contents
- Docker Compose for PostgreSQL
- CSRF Protection
- CORS Configuration
- Multi-Factor Authentication
- Social Login (OAuth 2.0 Providers)
- Rate Limiting
- Security Headers
- Actuator Security
- Testing Security
- Production Considerations
- Troubleshooting
- Best Practices
- Learning Resources
Prerequisites
- Java 17+
- Spring Boot 3.x
- Basic Spring Security knowledge
- Docker (for PostgreSQL setup)
1. Docker Compose for PostgreSQL
Set up a PostgreSQL database using Docker Compose for persistent storage.
version: '3.8'
services:
postgres:
image: postgres:16
environment:
POSTGRES_DB: spring_security_db
POSTGRES_USER: user
POSTGRES_PASSWORD: password
ports:
- '5432:5432'
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- app-network
volumes:
postgres_data:
networks:
app-network:
driver: bridge
Run docker-compose up -d to start the PostgreSQL container.
Configure the database connection in application.yml:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/spring_security_db
username: user
password: password
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
server:
port: 8080
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
management:
endpoints:
web:
exposure:
include: health,info,metrics
2. CSRF Protection
Spring Security enables CSRF protection by default. For APIs, you may disable it for non-browser clients, but keep it enabled for browser-based clients.
Configure CSRF in the security configuration:
package com.example.springsecurityadvanced.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**", "/swagger-ui/**", "/api-docs/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.permitAll()
);
return http.build();
}
}
The CookieCsrfTokenRepository stores the CSRF token in a cookie, accessible to frontend applications.
3. CORS Configuration
Enable CORS for specific origins to allow cross-origin requests.
Add CORS configuration in SecurityConfig.java:
package com.example.springsecurityadvanced.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**", "/swagger-ui/**", "/api-docs/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.permitAll()
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("http://localhost:3000");
configuration.addAllowedMethod("*");
configuration.addAllowedHeader("*");
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
4. Multi-Factor Authentication (MFA)
Implement MFA using TOTP (Time-Based One-Time Password) with a QR code for authenticator apps.
Add dependencies for TOTP:
<!-- Add to existing pom.xml -->
<dependency>
<groupId>dev.samstevens.totp</groupId>
<artifactId>totp</artifactId>
<version>1.7.1</version>
</dependency>
Create entities for users and MFA:
package com.example.springsecurityadvanced.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "users")
public class User {
@Id
private String username;
private String password;
private String roles;
private boolean mfaEnabled;
private String mfaSecret;
// Getters and setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getRoles() { return roles; }
public void setRoles(String roles) { this.roles = roles; }
public boolean isMfaEnabled() { return mfaEnabled; }
public void setMfaEnabled(boolean mfaEnabled) { this.mfaEnabled = mfaEnabled; }
public String getMfaSecret() { return mfaSecret; }
public void setMfaSecret(String mfaSecret) { this.mfaSecret = mfaSecret; }
}
Create a service for MFA:
package com.example.springsecurityadvanced.service;
import dev.samstevens.totp.code.*;
import dev.samstevens.totp.exceptions.QrGenerationException;
import dev.samstevens.totp.qr.QrData;
import dev.samstevens.totp.qr.ZxingPngQrGenerator;
import dev.samstevens.totp.secret.SecretGenerator;
import dev.samstevens.totp.time.SystemTimeProvider;
import org.springframework.stereotype.Service;
@Service
public class MfaService {
private final SecretGenerator secretGenerator;
private final ZxingPngQrGenerator qrGenerator;
public MfaService(SecretGenerator secretGenerator, ZxingPngQrGenerator qrGenerator) {
this.secretGenerator = secretGenerator;
this.qrGenerator = qrGenerator;
}
public String generateSecret() {
return secretGenerator.generate();
}
public byte[] generateQrCode(String username, String secret) throws QrGenerationException {
QrData data = new QrData.Builder()
.label(username)
.secret(secret)
.issuer("SpringSecurityAdvanced")
.algorithm(HashingAlgorithm.SHA1)
.digits(6)
.period(30)
.build();
return qrGenerator.generate(data);
}
public boolean verifyCode(String secret, String code) {
CodeVerifier verifier = new DefaultCodeVerifier(
new DefaultCodeGenerator(),
new SystemTimeProvider()
);
return verifier.isValidCode(secret, code);
}
}
Update SecurityConfig.java to include MFA:
package com.example.springsecurityadvanced.config;
import com.example.springsecurityadvanced.service.MfaService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final MfaService mfaService;
private final UserDetailsService userDetailsService;
public SecurityConfig(MfaService mfaService, UserDetailsService userDetailsService) {
this.mfaService = mfaService;
this.userDetailsService = userDetailsService;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**", "/swagger-ui/**", "/api-docs/**", "/mfa/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.permitAll()
)
.authenticationProvider(authenticationProvider());
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("http://localhost:3000");
configuration.addAllowedMethod("*");
configuration.addAllowedHeader("*");
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
}
Create a controller for MFA:
package com.example.springsecurityadvanced.controller;
import com.example.springsecurityadvanced.entity.User;
import com.example.springsecurityadvanced.service.MfaService;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/mfa")
public class MfaController {
private final MfaService mfaService;
private final UserRepository userRepository;
public MfaController(MfaService mfaService, UserRepository userRepository) {
this.mfaService = mfaService;
this.userRepository = userRepository;
}
@GetMapping("/setup")
public ResponseEntity<byte[]> setupMfa() throws Exception {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
String secret = mfaService.generateSecret();
user.setMfaSecret(secret);
user.setMfaEnabled(true);
userRepository.save(user);
byte[] qrCode = mfaService.generateQrCode(username, secret);
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_PNG)
.body(qrCode);
}
@PostMapping("/verify")
public ResponseEntity<?> verifyMfa(@RequestParam String code) {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
if (mfaService.verifyCode(user.getMfaSecret(), code)) {
return ResponseEntity.ok("MFA verified successfully");
}
return ResponseEntity.badRequest().body("Invalid MFA code");
}
}
5. Social Login (OAuth 2.0 Providers)
Configure OAuth 2.0 for Google login.
Add OAuth 2.0 configuration in application.yml:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/spring_security_db
username: user
password: password
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
security:
oauth2:
client:
registration:
google:
client-id: your-google-client-id
client-secret: your-google-client-secret
scope: profile,email
server:
port: 8080
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
management:
endpoints:
web:
exposure:
include: health,info,metrics
Update SecurityConfig.java to include OAuth 2.0:
package com.example.springsecurityadvanced.config;
import com.example.springsecurityadvanced.service.MfaService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final MfaService mfaService;
private final UserDetailsService userDetailsService;
public SecurityConfig(MfaService mfaService, UserDetailsService userDetailsService) {
this.mfaService = mfaService;
this.userDetailsService = userDetailsService;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**", "/swagger-ui/**", "/api-docs/**", "/mfa/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.permitAll()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/home", true)
)
.authenticationProvider(authenticationProvider());
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("http://localhost:3000");
configuration.addAllowedMethod("*");
configuration.addAllowedHeader("*");
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
}
6. Rate Limiting
Implement rate limiting using Bucket4j.
Add dependency:
<!-- Add to existing pom.xml -->
<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-core</artifactId>
<version>8.2.0</version>
</dependency>
Create a rate limiting filter:
package com.example.springsecurityadvanced.config;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Refill;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class RateLimitFilter extends OncePerRequestFilter {
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
private Bucket createNewBucket() {
Refill refill = Refill.intervally(10, Duration.ofMinutes(1));
Bandwidth limit = Bandwidth.classic(10, refill);
return Bucket.builder().addLimit(limit).build();
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String clientIp = request.getRemoteAddr();
Bucket bucket = buckets.computeIfAbsent(clientIp, k -> createNewBucket());
if (bucket.tryConsume(1)) {
filterChain.doFilter(request, response);
} else {
response.setStatus(429);
response.getWriter().write("Too Many Requests");
}
}
}
Register the filter:
package com.example.springsecurityadvanced.config;
import com.example.springsecurityadvanced.service.MfaService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final MfaService mfaService;
private final UserDetailsService userDetailsService;
private final RateLimitFilter rateLimitFilter;
public SecurityConfig(MfaService mfaService, UserDetailsService userDetailsService, RateLimitFilter rateLimitFilter) {
this.mfaService = mfaService;
this.userDetailsService = userDetailsService;
this.rateLimitFilter = rateLimitFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.addFilterBefore(rateLimitFilter, SessionManagementFilter.class)
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**", "/swagger-ui/**", "/api-docs/**", "/mfa/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.permitAll()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/home", true)
)
.authenticationProvider(authenticationProvider());
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("http://localhost:3000");
configuration.addAllowedMethod("*");
configuration.addAllowedHeader("*");
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
}
7. Security Headers
Add security headers to protect against common attacks.
package com.example.springsecurityadvanced.config;
import com.example.springsecurityadvanced.service.MfaService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final MfaService mfaService;
private final UserDetailsService userDetailsService;
private final RateLimitFilter rateLimitFilter;
public SecurityConfig(MfaService mfaService, UserDetailsService userDetailsService, RateLimitFilter rateLimitFilter) {
this.mfaService = mfaService;
this.userDetailsService = userDetailsService;
this.rateLimitFilter = rateLimitFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.addFilterBefore(rateLimitFilter, SessionManagementFilter.class)
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'")
)
.xssProtection(xss -> xss
.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)
)
.frameOptions(frame -> frame
.deny()
)
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.preload(true)
.maxAgeInSeconds(31536000)
)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**", "/swagger-ui/**", "/api-docs/**", "/mfa/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.permitAll()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/home", true)
)
.authenticationProvider(authenticationProvider());
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("http://localhost:3000");
configuration.addAllowedMethod("*");
configuration.addAllowedHeader("*");
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
}
8. Actuator Security
Secure Spring Boot Actuator endpoints.
Update application.yml:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/spring_security_db
username: user
password: password
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
security:
oauth2:
client:
registration:
google:
client-id: your-google-client-id
client-secret: your-google-client-secret
scope: profile,email
server:
port: 8080
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: when_authorized
metrics:
enabled: true
Secure actuator endpoints in SecurityConfig.java:
package com.example.springsecurityadvanced.config;
import com.example.springsecurityadvanced.service.MfaService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final MfaService mfaService;
private final UserDetailsService userDetailsService;
private final RateLimitFilter rateLimitFilter;
public SecurityConfig(MfaService mfaService, UserDetailsService userDetailsService, RateLimitFilter rateLimitFilter) {
this.mfaService = mfaService;
this.userDetailsService = userDetailsService;
this.rateLimitFilter = rateLimitFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.addFilterBefore(rateLimitFilter, SessionManagementFilter.class)
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'")
)
.xssProtection(xss -> xss
.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)
)
.frameOptions(frame -> frame
.deny()
)
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.preload(true)
.maxAgeInSeconds(31536000)
)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**", "/swagger-ui/**", "/api-docs/**", "/mfa/**").permitAll()
.requestMatchers("/actuator/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.permitAll()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/home", true)
)
.authenticationProvider(authenticationProvider());
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("http://localhost:3000");
configuration.addAllowedMethod("*");
configuration.addAllowedHeader("*");
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
}
9. Testing Security
Write tests to verify security configurations.
package com.example.springsecurityadvanced.config;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
public class SecurityConfigTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testPublicEndpoint() throws Exception {
mockMvc.perform(get("/api/public/test"))
.andExpect(status().isOk());
}
@Test
public void testSecuredEndpointWithoutAuth() throws Exception {
mockMvc.perform(get("/api/secured/test"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "USER")
public void testSecuredEndpointWithAuth() throws Exception {
mockMvc.perform(get("/api/secured/test"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
public void testAdminEndpointWithUserRole() throws Exception {
mockMvc.perform(get("/actuator/health"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(roles = "ADMIN")
public void testAdminEndpointWithAdminRole() throws Exception {
mockMvc.perform(get("/actuator/health"))
.andExpect(status().isOk());
}
}
Create test endpoints:
package com.example.springsecurityadvanced.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("/api/public/test")
public String publicTest() {
return "Public endpoint";
}
@GetMapping("/api/secured/test")
public String securedTest() {
return "Secured endpoint";
}
}
10. Production Considerations
- HTTPS: Always use HTTPS in production. Configure SSL in
application.ymlor use a reverse proxy like Nginx. - Password Storage: Use BCrypt for password hashing (already configured).
- Session Management: Use stateless JWT for APIs or configure session timeouts.
- Monitoring: Enable logging for security events.
- Environment Variables: Store sensitive data (e.g., OAuth client secrets) in environment variables.
- Backup: Regularly back up the PostgreSQL database.
- Updates: Keep dependencies and Spring Boot updated.
11. Troubleshooting
- CSRF Issues: Ensure the frontend includes the CSRF token in requests. Check cookie settings.
- CORS Errors: Verify allowed origins and methods in
CorsConfiguration. - MFA Failures: Confirm the TOTP code is synchronized with the server time.
- OAuth 2.0 Errors: Check client ID/secret and redirect URIs in the provider’s console.
- Rate Limiting: Adjust bucket size and refill rate based on traffic.
- Actuator Access: Ensure users have the
ADMINrole for actuator endpoints.
12. Best Practices
- Always use HTTPS in production
- Never store plain text passwords - use strong password encoders
- Implement proper session management - timeout, concurrency control
- Use the principle of least privilege for authorization
- Keep dependencies updated to address security vulnerabilities
- Regularly audit security configurations
- Implement proper error handling - don't leak sensitive information
- Use security headers as shown in section 7
- Monitor and log security events
- Regularly test security with automated tests and penetration testing
13. Learning Resources
Official Documentation:
Books:
- "Spring Security in Action" by Laurentiu Spilca
- "Securing Spring Boot Applications" by Steven C. Saliman
Online Courses:
Tools:
- OWASP ZAP for security testing
- Burp Suite for web security testing
Communities:
This comprehensive tutorial provides complete working examples for advanced Spring Security features. Remember to adapt the configurations to your specific requirements and always follow security best practices in production environments.