Spring Security
Why Spring Security?
Every production API needs authentication (who are you?) and authorization (what can you do?). Spring Security provides a comprehensive, customizable security framework integrated with Spring Boot.
Getting Started
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
By default, Boot generates a random password for the user account. Replace this immediately with proper configuration.
Basic Security Configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health", "/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
}
User Details Service
@Service
public class AppUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public AppUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
AppUser user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException(email));
return User.builder()
.username(user.getEmail())
.password(user.getPasswordHash())
.roles(user.getRole().name())
.build();
}
}
Store passwords hashed with BCrypt — never plain text.
JWT Authentication
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
@Service
public class JwtService {
@Value("${app.jwt.secret}")
private String secret;
public String generateToken(String username, List<String> roles) {
return Jwts.builder()
.subject(username)
.claim("roles", roles)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + 86400000))
.signWith(Keys.hmacShaKeyFor(secret.getBytes()))
.compact();
}
public String extractUsername(String token) {
return parseClaims(token).getSubject();
}
public boolean isValid(String token) {
try {
parseClaims(token);
return true;
} catch (JwtException e) {
return false;
}
}
private Claims parseClaims(String token) {
return Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(secret.getBytes()))
.build()
.parseSignedClaims(token)
.getPayload();
}
}
JWT Filter
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
if (jwtService.isValid(token)) {
String username = jwtService.extractUsername(token);
UserDetails user = userDetailsService.loadUserByUsername(username);
var auth = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
chain.doFilter(request, response);
}
}
Register the filter before UsernamePasswordAuthenticationFilter in the security chain.
Method-Level Security
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {}
@Service
public class OrderService {
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public Order getOrder(Long orderId, Long userId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new ResourceNotFoundException("Order", orderId));
}
@PostAuthorize("returnObject.owner == authentication.name")
public Document getDocument(Long id) {
return documentRepository.findById(id).orElseThrow();
}
}
Fine-grained authorization at the service layer — not just URL patterns.
OAuth2 / OIDC (Social Login)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID}
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET}
spring.security.oauth2.client.registration.google.scope=openid,profile,email
Spring Security handles the OAuth2 redirect flow. For resource servers validating tokens from an identity provider (Keycloak, Auth0), use spring-boot-starter-oauth2-resource-server.
CORS Configuration
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://app.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
CSRF Protection
- Session-based apps — keep CSRF enabled (default)
- Stateless JWT APIs — disable CSRF (no cookies)
- SPAs with cookies — use CSRF tokens or SameSite cookies
Security Headers
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))
.frameOptions(HeadersConfigurer.FrameOptionsConfig::deny)
.xssProtection(Customizer.withDefaults())
);
Rate Limiting
Spring Security does not include rate limiting — add Bucket4j or a gateway (NGINX, API Gateway) in front:
@Component
public class RateLimitFilter extends OncePerRequestFilter {
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String key = request.getRemoteAddr();
Bucket bucket = buckets.computeIfAbsent(key, k -> Bucket.builder()
.addLimit(Bandwidth.classic(100, Refill.greedy(100, Duration.ofMinutes(1))))
.build());
if (bucket.tryConsume(1)) {
chain.doFilter(request, response);
} else {
response.setStatus(429);
}
}
}
Testing Security
@SpringBootTest
@AutoConfigureMockMvc
class SecurityIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(roles = "ADMIN")
void adminEndpointAccessible() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
@Test
void protectedEndpointRequiresAuth() throws Exception {
mockMvc.perform(get("/api/orders"))
.andExpect(status().isUnauthorized());
}
}
Production Checklist
- BCrypt password hashing (cost factor ≥ 12)
- JWT secrets in environment variables, rotated periodically
- HTTPS enforced — no credentials over HTTP
- Role-based access on both URL and method levels
- CORS restricted to known origins
- Rate limiting on auth endpoints
- Security headers configured
- Dependency scanning (
./mvnw dependency-check:check) - Audit logging for authentication events
Spring Security is powerful but requires deliberate configuration — default-deny policies and defense in depth protect production applications.