Spring Reference: Spring Security¶
Overview¶
Spring Security provides authentication, authorization, and protection against common attacks.
Basic Configuration¶
Security Filter Chain (Spring Security 6.x)¶
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**", "/health").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/api/**").authenticated()
.anyRequest().denyAll()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
User Details Service¶
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword()) // Already encoded
.roles(user.getRoles().toArray(String[]::new))
.accountExpired(!user.isActive())
.accountLocked(user.isLocked())
.credentialsExpired(false)
.disabled(!user.isEnabled())
.build();
}
}
In-Memory Users (Testing)¶
@Bean
public UserDetailsService userDetailsService() {
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder().encode("admin123"))
.roles("ADMIN", "USER")
.build();
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder().encode("user123"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(admin, user);
}
Authentication¶
Authentication Manager¶
@Configuration
public class SecurityConfig {
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public DaoAuthenticationProvider authenticationProvider(
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return provider;
}
}
Custom Authentication Provider¶
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final LdapService ldapService;
private final UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
// Custom authentication logic (e.g., LDAP)
if (!ldapService.authenticate(username, password)) {
throw new BadCredentialsException("Invalid credentials");
}
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
return new UsernamePasswordAuthenticationToken(
userDetails, password, userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
JWT Authentication¶
JWT Configuration¶
@Configuration
@EnableWebSecurity
public class JwtSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
JwtAuthenticationFilter jwtFilter) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // Stateless, no CSRF needed
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
JWT Filter¶
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
String username = jwtService.extractUsername(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(token, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource()
.buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
JWT Service¶
@Service
public class JwtService {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration:3600000}") // 1 hour default
private long expiration;
public String generateToken(UserDetails userDetails) {
return generateToken(Map.of(), userDetails);
}
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return Jwts.builder()
.setClaims(extraClaims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public boolean isTokenValid(String token, UserDetails userDetails) {
String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractClaim(token, Claims::getExpiration).before(new Date());
}
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
return claimsResolver.apply(claims);
}
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}
Auth Controller¶
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@PostMapping("/login")
public AuthResponse login(@RequestBody @Valid LoginRequest request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.username(), request.password()));
UserDetails userDetails = userDetailsService.loadUserByUsername(request.username());
String token = jwtService.generateToken(userDetails);
return new AuthResponse(token);
}
@PostMapping("/refresh")
public AuthResponse refresh(@RequestHeader("Authorization") String authHeader) {
String oldToken = authHeader.substring(7);
String username = jwtService.extractUsername(oldToken);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String newToken = jwtService.generateToken(userDetails);
return new AuthResponse(newToken);
}
}
OAuth2 / OpenID Connect¶
OAuth2 Resource Server (JWT)¶
@Configuration
@EnableWebSecurity
public class OAuth2ResourceServerConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
)
.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter =
new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthoritiesClaimName("roles");
authoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}
}
# application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com
# OR
jwk-set-uri: https://auth.example.com/.well-known/jwks.json
OAuth2 Client¶
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope: openid, profile, email
github:
client-id: ${GITHUB_CLIENT_ID}
client-secret: ${GITHUB_CLIENT_SECRET}
scope: read:user, user:email
@Configuration
@EnableWebSecurity
public class OAuth2ClientConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService())
)
)
.build();
}
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> customOAuth2UserService() {
return new CustomOAuth2UserService();
}
}
Method Security¶
Enable Method Security¶
@Configuration
@EnableMethodSecurity(
prePostEnabled = true, // @PreAuthorize, @PostAuthorize
securedEnabled = true, // @Secured
jsr250Enabled = true // @RolesAllowed
)
public class MethodSecurityConfig {
}
Annotations¶
@Service
public class OrderService {
// Role-based
@PreAuthorize("hasRole('ADMIN')")
public void deleteOrder(Long id) { }
// Multiple roles
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
public void updateOrder(Order order) { }
// Permission-based
@PreAuthorize("hasAuthority('ORDER_READ')")
public Order getOrder(Long id) { }
// SpEL expressions
@PreAuthorize("hasRole('ADMIN') or #order.customerId == authentication.principal.id")
public void modifyOrder(Order order) { }
// Parameter access
@PreAuthorize("#id == authentication.principal.id or hasRole('ADMIN')")
public UserProfile getProfile(@Param("id") Long id) { }
// Post-authorize (filter return value)
@PostAuthorize("returnObject.customerId == authentication.principal.id")
public Order findOrder(Long id) { }
// Filter collections
@PostFilter("filterObject.owner == authentication.principal.username")
public List<Document> getDocuments() { }
@PreFilter("filterObject.status != 'DELETED'")
public void processOrders(List<Order> orders) { }
// Legacy annotations
@Secured("ROLE_ADMIN")
public void adminOnly() { }
@RolesAllowed({"ADMIN", "MANAGER"})
public void managersAndAdmins() { }
}
Custom Permission Evaluator¶
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication auth, Object targetDomainObject,
Object permission) {
if (targetDomainObject instanceof Order order) {
String perm = (String) permission;
String username = auth.getName();
return switch (perm) {
case "READ" -> canReadOrder(order, username);
case "WRITE" -> canWriteOrder(order, username);
case "DELETE" -> canDeleteOrder(order, username);
default -> false;
};
}
return false;
}
@Override
public boolean hasPermission(Authentication auth, Serializable targetId,
String targetType, Object permission) {
// Load object by ID and check permission
return false;
}
}
// Usage
@PreAuthorize("hasPermission(#order, 'WRITE')")
public void updateOrder(Order order) { }
CORS Configuration¶
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://example.com", "https://app.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setExposedHeaders(List.of("Authorization", "X-Request-Id"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
}
// In SecurityFilterChain
http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
CSRF Protection¶
@Configuration
public class CsrfConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// For SPAs with cookie-based CSRF
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
)
// Ignore CSRF for specific paths
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/webhooks/**")
)
.build();
}
}
// Disable for stateless APIs (use JWT instead)
http.csrf(csrf -> csrf.disable())
Security Context¶
@RestController
public class ProfileController {
@GetMapping("/profile")
public UserProfile getProfile() {
// Get current authenticated user
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
// Or inject directly
return getProfile(auth.getName());
}
@GetMapping("/profile2")
public UserProfile getProfile2(@AuthenticationPrincipal UserDetails userDetails) {
return getProfile(userDetails.getUsername());
}
// Custom principal
@GetMapping("/profile3")
public UserProfile getProfile3(@AuthenticationPrincipal CustomUserPrincipal principal) {
return getProfile(principal.getId());
}
}
Async Security Context¶
@Configuration
public class AsyncSecurityConfig {
@Bean
public DelegatingSecurityContextAsyncTaskExecutor taskExecutor() {
return new DelegatingSecurityContextAsyncTaskExecutor(
new SimpleAsyncTaskExecutor());
}
}
// Or enable globally
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
return new DelegatingSecurityContextExecutorService(
Executors.newCachedThreadPool());
}
}
Security Headers¶
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self'")
)
.frameOptions(frame -> frame.deny())
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)
)
.xssProtection(xss -> xss.disable()) // Modern browsers have this built-in
)
.build();
}
Testing Security¶
@WebMvcTest(OrderController.class)
@Import(SecurityConfig.class)
class OrderControllerSecurityTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private OrderService orderService;
@Test
void shouldDenyUnauthenticated() throws Exception {
mockMvc.perform(get("/api/orders"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "USER")
void shouldAllowAuthenticatedUser() throws Exception {
when(orderService.findAll()).thenReturn(List.of());
mockMvc.perform(get("/api/orders"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "ADMIN")
void adminCanDelete() throws Exception {
mockMvc.perform(delete("/api/orders/1"))
.andExpect(status().isNoContent());
}
@Test
@WithMockUser(roles = "USER")
void userCannotDelete() throws Exception {
mockMvc.perform(delete("/api/orders/1"))
.andExpect(status().isForbidden());
}
@Test
@WithUserDetails("[email protected]") // Load from UserDetailsService
void withRealUser() throws Exception {
// Uses actual user from database/service
}
}
// Custom annotation for common test users
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(username = "admin", roles = {"ADMIN", "USER"})
public @interface WithAdmin {}
Common Interview Questions¶
- How does Spring Security filter chain work?
- Series of filters processing each request
- Authentication filter validates credentials
-
Authorization filter checks permissions
-
JWT vs Session authentication?
- JWT: Stateless, scalable, contains claims
-
Session: Server-side state, easier to invalidate
-
@PreAuthorize vs @Secured?
- @PreAuthorize: SpEL expressions, more flexible
-
@Secured: Simple role check, less powerful
-
How to handle CORS?
- Configure CorsConfigurationSource
-
Register with security filter chain
-
OAuth2 Resource Server vs Client?
- Resource Server: Validates tokens (API)
- Client: Obtains tokens (web app)
- *