Complete guide to Spring Security: learn how to secure Spring applications with authentication, authorization, OAuth2, JWT, and more. Master Spring Security's comprehensive security framework for enterprise applications.
Table of Contents
1. What is Spring Security?
Spring Security is a powerful and highly customizable authentication and access-control framework for Java applications. It provides comprehensive security services for Spring-based applications, including authentication, authorization, protection against common attacks, and integration with OAuth2 and JWT.
Spring Security provides:
- Authentication and authorization mechanisms
- Protection against common vulnerabilities (CSRF, XSS, etc.)
- Session management
- Method-level security
- OAuth2 and JWT support
- LDAP integration
- Remember-me functionality
- Password encoding
The following diagram illustrates the Spring Security architecture:
2. Why Use Spring Security?
- Comprehensive Security: Provides authentication, authorization, and protection against common attacks
- Spring Integration: Seamlessly integrates with Spring Framework and Spring Boot
- Flexible Configuration: Supports both Java configuration and XML configuration
- Multiple Authentication Methods: Supports form-based, HTTP Basic, OAuth2, JWT, and more
- Method-Level Security: Secure individual methods with annotations
- OAuth2 Support: Built-in support for OAuth2 resource servers and clients
- JWT Support: Full support for JSON Web Tokens
- CSRF Protection: Built-in protection against Cross-Site Request Forgery
- Session Management: Advanced session management and security
3. Core Concepts
3.1 Authentication
Authentication is the process of verifying who a user is. Spring Security supports various authentication mechanisms.
The authentication flow is illustrated below:
3.1.1 In-Memory Authentication
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic();
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("admin")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
3.1.2 Database Authentication
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/register").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/home")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.authorities(user.getRoles())
.build();
}
}
3.2 Authorization
Authorization is the process of determining what a user is allowed to do. Spring Security provides multiple ways to configure authorization.
3.2.1 URL-Based Authorization
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.requestMatchers(HttpMethod.GET, "/api/**").hasRole("USER")
.requestMatchers(HttpMethod.POST, "/api/**").hasRole("ADMIN")
.anyRequest().authenticated()
);
return http.build();
}
}
3.2.2 Method-Level Security
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
// Configuration
}
@Service
public class UserService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {
// Only admins can delete users
}
@PreAuthorize("hasRole('USER') and #userId == authentication.principal.id")
public User getUser(Long userId) {
// Users can only view their own profile
}
@PreAuthorize("hasPermission(#user, 'WRITE')")
public void updateUser(User user) {
// Custom permission check
}
@Secured("ROLE_ADMIN")
public List<User> getAllUsers() {
// Only admins can see all users
}
@RolesAllowed({"USER", "ADMIN"})
public void updateProfile(User user) {
// Users and admins can update profiles
}
}
3.3 Security Filter Chain
Spring Security uses a filter chain to process security-related concerns. Each filter handles a specific security aspect.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(jwtDecoder())
)
);
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri("https://example.com/.well-known/jwks.json")
.build();
}
}
4. Configuration
Spring Security can be configured using Java configuration or XML. Modern applications use Java configuration.
4.1 Basic Security Configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/home", "/register").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.failureUrl("/login?error=true")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.permitAll()
)
.rememberMe(remember -> remember
.tokenValiditySeconds(86400)
.key("remember-me-key")
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
4.2 CORS Configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
5. OAuth2
OAuth2 is an authorization framework that enables applications to obtain limited access to user accounts. Spring Security provides comprehensive OAuth2 support.
5.1 OAuth2 Concepts
OAuth2 involves several key concepts and roles:
The OAuth2 flow is illustrated below:
- Resource Owner: The user who owns the resource
- Client: The application requesting access
- Authorization Server: Issues access tokens
- Resource Server: Hosts protected resources
- Access Token: Credential used to access resources
- Refresh Token: Used to obtain new access tokens
5.2 OAuth2 Resource Server
A resource server validates access tokens and serves protected resources.
5.2.1 JWT Resource Server Configuration
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(jwtDecoder())
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/**").authenticated()
);
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri("https://auth.example.com/.well-known/jwks.json")
.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter =
new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthorityPrefix("ROLE_");
authoritiesConverter.setAuthoritiesClaimName("roles");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}
}
5.2.2 Opaque Token Resource Server
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(opaqueToken -> opaqueToken
.introspectionUri("https://auth.example.com/oauth2/introspect")
.introspectionClientCredentials("client-id", "client-secret")
)
);
return http.build();
}
}
5.3 OAuth2 Client
An OAuth2 client application requests authorization and uses access tokens to access resources.
5.3.1 OAuth2 Client Configuration
@Configuration
@EnableWebSecurity
public class OAuth2ClientConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService())
)
.successHandler(oauth2AuthenticationSuccessHandler())
)
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(
googleClientRegistration(),
githubClientRegistration()
);
}
private ClientRegistration googleClientRegistration() {
return ClientRegistration.withRegistrationId("google")
.clientId("google-client-id")
.clientSecret("google-client-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
.scope("openid", "profile", "email")
.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
.tokenUri("https://www.googleapis.com/oauth2/v4/token")
.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
.userNameAttributeName(IdTokenClaimNames.SUB)
.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
.clientName("Google")
.build();
}
private ClientRegistration githubClientRegistration() {
return ClientRegistration.withRegistrationId("github")
.clientId("github-client-id")
.clientSecret("github-client-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
.scope("read:user")
.authorizationUri("https://github.com/login/oauth/authorize")
.tokenUri("https://github.com/login/oauth/access_token")
.userInfoUri("https://api.github.com/user")
.userNameAttributeName("id")
.clientName("GitHub")
.build();
}
}
5.3.2 OAuth2 Client Properties
# application.yml
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
provider:
google:
authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
token-uri: https://www.googleapis.com/oauth2/v4/token
user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo
user-name-attribute: sub
github:
authorization-uri: https://github.com/login/oauth/authorize
token-uri: https://github.com/login/oauth/access_token
user-info-uri: https://api.github.com/user
user-name-attribute: id
6. JWT (JSON Web Tokens)
JSON Web Tokens (JWT) are a compact, URL-safe means of representing claims to be transferred between parties. Spring Security provides full JWT support.
6.1 JWT Structure
A JWT consists of three parts: header, payload, and signature, separated by dots.
// JWT Structure: header.payload.signature
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
6.2 JWT Configuration
@Configuration
@EnableWebSecurity
public class JwtSecurityConfig {
@Value("${jwt.secret}")
private String jwtSecret;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter(jwtTokenProvider());
}
@Bean
public JwtTokenProvider jwtTokenProvider() {
return new JwtTokenProvider(jwtSecret);
}
}
@Component
public class JwtTokenProvider {
private final String secret;
private final long validityInMilliseconds = 3600000; // 1 hour
public JwtTokenProvider(String secret) {
this.secret = secret;
}
public String createToken(String username, List<String> roles) {
Claims claims = Jwts.claims().setSubject(username);
claims.put("roles", roles);
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(validity)
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(getUsername(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
public String getUsername(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody().getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
}
7. Real-World Examples
7.1 Example 1: REST API Security
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class RestApiSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.decoder(jwtDecoder()))
);
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri("https://auth.example.com/.well-known/jwks.json")
.build();
}
}
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/me")
@PreAuthorize("hasRole('USER')")
public ResponseEntity<User> getCurrentUser(Authentication authentication) {
String username = authentication.getName();
// Return current user
return ResponseEntity.ok(userService.findByUsername(username));
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
7.2 Example 2: OAuth2 Login
@Configuration
@EnableWebSecurity
public class OAuth2LoginConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService())
)
.successHandler(oauth2AuthenticationSuccessHandler())
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/error").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> customOAuth2UserService() {
return new CustomOAuth2UserService();
}
@Bean
public AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler() {
return new OAuth2AuthenticationSuccessHandler();
}
}
@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
@Autowired
private UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = new DefaultOAuth2UserService().loadUser(userRequest);
String email = oauth2User.getAttribute("email");
User user = userRepository.findByEmail(email)
.orElseGet(() -> createNewUser(oauth2User));
return new CustomOAuth2User(user, oauth2User.getAttributes());
}
private User createNewUser(OAuth2User oauth2User) {
User user = new User();
user.setEmail(oauth2User.getAttribute("email"));
user.setName(oauth2User.getAttribute("name"));
user.setProvider(Provider.GOOGLE);
return userRepository.save(user);
}
}
7.3 Example 3: Method Security
@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// Configuration
}
@Service
public class OrderService {
@PreAuthorize("hasRole('USER')")
public Order createOrder(Order order) {
// Only authenticated users can create orders
return orderRepository.save(order);
}
@PreAuthorize("hasRole('ADMIN') or #order.userId == authentication.principal.id")
public Order updateOrder(Long orderId, Order order) {
// Admins or order owners can update
return orderRepository.save(order);
}
@PreAuthorize("hasRole('ADMIN')")
@PostAuthorize("returnObject.userId == authentication.principal.id or hasRole('ADMIN')")
public Order getOrder(Long orderId) {
// Users can only see their own orders, admins can see all
return orderRepository.findById(orderId).orElseThrow();
}
@PreFilter("filterObject.userId == authentication.principal.id or hasRole('ADMIN')")
public List<Order> processOrders(List<Order> orders) {
// Filter orders before processing
return orders.stream()
.map(this::processOrder)
.collect(Collectors.toList());
}
}
8. Best Practices
8.1 Password Security
// Always use strong password encoding
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // Use strength 12 or higher
}
// Never store plain text passwords
// Always hash passwords before storing
String encodedPassword = passwordEncoder.encode(rawPassword);
// Verify passwords securely
boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);
8.2 Token Security
// Use short-lived access tokens
long accessTokenValidity = 3600000; // 1 hour
// Use refresh tokens for long-term sessions
long refreshTokenValidity = 86400000; // 24 hours
// Always validate tokens on resource server
// Use HTTPS for token transmission
// Store tokens securely (httpOnly cookies preferred)
8.3 CSRF Protection
// Enable CSRF for state-changing operations
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
// Disable CSRF only for stateless APIs
http.csrf(csrf -> csrf.disable()); // Only for stateless APIs
8.4 Security Headers
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'")
)
.frameOptions(frame -> frame.deny())
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubdomains(true)
)
);
return http.build();
}
9. Advanced Concepts
9.1 Custom Authentication Provider
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails user = userDetailsService.loadUserByUsername(username);
if (passwordEncoder.matches(password, user.getPassword())) {
return new UsernamePasswordAuthenticationToken(
user, password, user.getAuthorities()
);
} else {
throw new BadCredentialsException("Invalid credentials");
}
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication);
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public AuthenticationProvider authenticationProvider() {
return customAuthenticationProvider;
}
}
9.2 Custom Access Decision Voter
public class CustomAccessDecisionVoter implements AccessDecisionVoter<Object> {
@Override
public boolean supports(ConfigAttribute attribute) {
return attribute.getAttribute().startsWith("CUSTOM_");
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
@Override
public int vote(Authentication authentication, Object object,
Collection<ConfigAttribute> attributes) {
// Custom voting logic
return ACCESS_GRANTED;
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<?>> decisionVoters = Arrays.asList(
new RoleVoter(),
new AuthenticatedVoter(),
new CustomAccessDecisionVoter()
);
return new UnanimousBased(decisionVoters);
}
}
9.3 Security Event Publishing
@Component
public class SecurityEventListener {
@EventListener
public void handleAuthenticationSuccess(AuthenticationSuccessEvent event) {
// Log successful authentication
log.info("User {} authenticated successfully",
event.getAuthentication().getName());
}
@EventListener
public void handleAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
// Log failed authentication
log.warn("Authentication failed for user: {}",
event.getAuthentication().getName());
}
@EventListener
public void handleAuthorizationFailure(AuthorizationFailureEvent event) {
// Log authorization failure
log.warn("Authorization failed for user: {}",
event.getAuthentication().getName());
}
}
0 Comments