Spring Security
Authentication, authorization, and security configuration with Spring Security including JWT, OAuth2, and method-level security
You are an expert in Spring Security for building secure Java applications with Spring Boot. You design security as an integral architectural layer, applying defense in depth and the principle of least privilege across authentication, authorization, and transport security. ## Key Points - **Use BCrypt** for password hashing — it is intentionally slow and resistant to brute-force attacks. Never store plaintext passwords. - **Keep JWT expiry short** (15-60 minutes) and implement refresh tokens for long-lived sessions. - **Store secrets externally** — JWT signing keys and OAuth2 client secrets belong in environment variables or a secrets manager, not in `application.yml`. - **Apply the principle of least privilege** — deny by default and explicitly permit only required endpoints. - **Enable CSRF protection** for browser-based sessions. Only disable it for stateless JWT APIs. - **Use `@PreAuthorize`** for fine-grained access control at the method level rather than only relying on URL-based rules. - **Validate all input** — security is defense in depth. Use `@Valid` on request bodies alongside authentication and authorization. - **Forgetting to hash passwords** — calling `passwordEncoder.encode()` on registration but comparing raw passwords on login. Let Spring Security handle comparison through the `PasswordEncoder` bean. - **Leaking security details in error responses** — returning "user not found" vs "wrong password" lets attackers enumerate valid accounts. Return a generic "invalid credentials" message. - **Not invalidating JWTs on logout** — JWTs are stateless, so they remain valid until expiry. Implement a token blacklist or use short-lived tokens with refresh token rotation. - **Disabling CSRF globally** when only the API portion is stateless — keep CSRF enabled for any endpoints serving browser sessions.
skilldb get java-spring-skills/Spring SecurityFull skill: 325 linesSpring Security — Java/Spring Boot
You are an expert in Spring Security for building secure Java applications with Spring Boot. You design security as an integral architectural layer, applying defense in depth and the principle of least privilege across authentication, authorization, and transport security.
Core Philosophy
Security is not a feature to be added after the application works. It is a cross-cutting architectural concern that shapes how requests are processed, how data is accessed, and how errors are communicated. Spring Security's filter chain model enforces this perspective: every request passes through security checks before it reaches application code. When security is treated as an afterthought, the result is a patchwork of ad-hoc checks scattered across controllers, easily bypassed and impossible to audit. When security is designed into the architecture from the start, it becomes consistent, testable, and maintainable.
The principle of least privilege should be the default posture. Deny everything, then explicitly permit what is needed. Spring Security's authorizeHttpRequests DSL reads naturally from most specific to most general, and the final rule should be anyRequest().authenticated() or anyRequest().denyAll(). A permissive default that grants access unless explicitly denied is an invitation for privilege escalation bugs every time a new endpoint is added. The security configuration should be reviewed with the same rigor as the data model, because a misconfigured matcher can expose sensitive operations to unauthenticated users.
Authentication and authorization are separate concerns that must not be conflated. Authentication answers "who are you?" -- verifying credentials, validating tokens, establishing identity. Authorization answers "are you allowed to do this?" -- checking roles, permissions, and ownership. Mixing these concerns produces code where a JWT filter also checks admin status, or a policy check assumes a specific authentication mechanism. Keep them cleanly separated: the filter chain handles authentication, @PreAuthorize and method security handle authorization, and policies express fine-grained access rules.
Overview
Spring Security is the standard framework for securing Spring-based applications. It provides authentication (verifying identity), authorization (controlling access), and protection against common exploits like CSRF, session fixation, and clickjacking. Spring Boot auto-configures sensible defaults, and the security filter chain architecture allows precise customization.
Core Concepts
Security Filter Chain
Every HTTP request passes through a chain of security filters. Spring Security 6+ uses a component-based configuration model:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // Disable for stateless APIs
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
UserDetailsService
The bridge between your user store and Spring Security:
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getEmail())
.password(user.getPasswordHash())
.roles(user.getRoles().stream().map(Role::getName).toArray(String[]::new))
.accountLocked(!user.isActive())
.build();
}
}
JWT Authentication
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration-ms:3600000}")
private long expirationMs;
public String generateToken(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
Date now = new Date();
Date expiry = new Date(now.getTime() + expirationMs);
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(now)
.setExpiration(expiry)
.signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256)
.compact();
}
public String extractUsername(String token) {
return Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
.build()
.parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
}
JWT Filter
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtTokenProvider tokenProvider, UserDetailsService userDetailsService) {
this.tokenProvider = tokenProvider;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
if (tokenProvider.validateToken(token)) {
String username = tokenProvider.extractUsername(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
Implementation Patterns
Authentication Controller
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider tokenProvider;
private final UserService userService;
public AuthController(AuthenticationManager authenticationManager,
JwtTokenProvider tokenProvider, UserService userService) {
this.authenticationManager = authenticationManager;
this.tokenProvider = tokenProvider;
this.userService = userService;
}
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()));
String token = tokenProvider.generateToken(authentication);
return ResponseEntity.ok(new AuthResponse(token));
}
@PostMapping("/register")
public ResponseEntity<Void> register(@Valid @RequestBody RegisterRequest request) {
userService.register(request);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
Method-Level Security
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
}
@Service
public class DocumentService {
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public List<Document> getUserDocuments(Long userId) {
// Only admins or the user themselves can access
return documentRepository.findByUserId(userId);
}
@PreAuthorize("hasAuthority('DOCUMENT_DELETE')")
public void deleteDocument(Long documentId) {
documentRepository.deleteById(documentId);
}
@PostAuthorize("returnObject.owner == authentication.name")
public Document getDocument(Long id) {
return documentRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Document", id));
}
}
OAuth2 Resource Server
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com/realms/myapp
@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter()))
);
return http.build();
}
private JwtAuthenticationConverter jwtAuthConverter() {
JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
converter.setAuthoritiesClaimName("roles");
converter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter authConverter = new JwtAuthenticationConverter();
authConverter.setJwtGrantedAuthoritiesConverter(converter);
return authConverter;
}
CORS Configuration
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://myapp.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
Best Practices
- Use BCrypt for password hashing — it is intentionally slow and resistant to brute-force attacks. Never store plaintext passwords.
- Keep JWT expiry short (15-60 minutes) and implement refresh tokens for long-lived sessions.
- Store secrets externally — JWT signing keys and OAuth2 client secrets belong in environment variables or a secrets manager, not in
application.yml. - Apply the principle of least privilege — deny by default and explicitly permit only required endpoints.
- Enable CSRF protection for browser-based sessions. Only disable it for stateless JWT APIs.
- Use
@PreAuthorizefor fine-grained access control at the method level rather than only relying on URL-based rules. - Validate all input — security is defense in depth. Use
@Validon request bodies alongside authentication and authorization.
Common Pitfalls
- Ordering filter chains incorrectly — a
permitAll()rule before a more specificauthenticated()rule causes the permissive rule to match first. Order matchers from most specific to least specific. - Forgetting to hash passwords — calling
passwordEncoder.encode()on registration but comparing raw passwords on login. Let Spring Security handle comparison through thePasswordEncoderbean. - Leaking security details in error responses — returning "user not found" vs "wrong password" lets attackers enumerate valid accounts. Return a generic "invalid credentials" message.
- Not invalidating JWTs on logout — JWTs are stateless, so they remain valid until expiry. Implement a token blacklist or use short-lived tokens with refresh token rotation.
- Disabling CSRF globally when only the API portion is stateless — keep CSRF enabled for any endpoints serving browser sessions.
Anti-Patterns
-
Security by obscurity — relying on undocumented URL paths or non-standard header names instead of proper authentication and authorization. Every endpoint must be explicitly secured regardless of how "hidden" it appears. Attackers use automated scanners, not guesswork.
-
The permissive catch-all — ending the authorization chain with
.anyRequest().permitAll()because "we will lock it down later." Every new endpoint added after this configuration is publicly accessible by default. Use.anyRequest().denyAll()or.anyRequest().authenticated()as the final rule. -
Logging sensitive credentials — including raw passwords, tokens, or API keys in log statements during authentication debugging. Even in development, this creates a habit that leaks into production logs and violates compliance requirements. Log only non-sensitive metadata like usernames and authentication outcomes.
-
Monolithic security configuration — a single 200-line
SecurityFilterChainbean that handles API authentication, admin panel sessions, actuator access, and OAuth2 flows. Split into multiple filter chains with@OrderandsecurityMatcherso each concern is isolated, testable, and independently modifiable. -
JWT without rotation or revocation — issuing long-lived JWTs with no mechanism for revocation and no key rotation strategy. When a token is compromised or a user's access should be revoked, there is no way to invalidate outstanding tokens. Use short-lived access tokens with refresh token rotation, and implement a token blacklist for immediate revocation needs.
Install this skill directly: skilldb add java-spring-skills
Related Skills
Spring Actuator
Application monitoring, health checks, metrics, and observability with Spring Boot Actuator and Micrometer
Spring Batch
Batch processing with Spring Batch including jobs, steps, chunk processing, readers, writers, and job scheduling
Spring Boot Basics
Core Spring Boot concepts including auto-configuration, starters, dependency injection, and application lifecycle
Spring Cloud
Microservices architecture with Spring Cloud including service discovery, API gateway, circuit breakers, and distributed configuration
Spring Data Jpa
Data persistence with Spring Data JPA including repositories, entity mapping, queries, and transaction management
Spring Testing
Testing patterns for Spring Boot applications including unit tests, integration tests, sliced tests, and test containers