Spring Security Interview Guide: Authentication, JWT & OAuth2

·13 min read
spring-securityjavaauthenticationjwtoauth2interview-preparation

Security is where enterprise Java gets serious. Spring Security is the de facto standard for securing Spring applications, and interviewers expect backend developers to understand it deeply.

This guide covers authentication, authorization, JWT, OAuth2, and security best practices—the knowledge that comes up in Java backend interviews.


Security Fundamentals

Before diving into Spring Security specifics, understand the core concepts.

Authentication vs Authorization

ConceptQuestionSpring Security Component
AuthenticationWho are you?AuthenticationManager
AuthorizationWhat can you do?AccessDecisionManager

Authentication verifies identity through credentials:

  • Username and password
  • JWT tokens
  • OAuth2 tokens
  • Certificates

Authorization checks permissions after authentication:

  • Role-based (ROLE_ADMIN, ROLE_USER)
  • Permission-based (READ_PRIVILEGE, WRITE_PRIVILEGE)
  • Resource-based (own resources only)

The Security Filter Chain

Spring Security is built on servlet filters. Every request passes through a chain of security filters.

Request → Filter Chain → Servlet (Controller)

┌─────────────────────────────────────────────────────────┐
│                  Security Filter Chain                   │
├─────────────────────────────────────────────────────────┤
│ 1. SecurityContextPersistenceFilter                      │
│    └── Loads/saves SecurityContext from session          │
│                                                          │
│ 2. UsernamePasswordAuthenticationFilter                  │
│    └── Processes login form submissions                  │
│                                                          │
│ 3. BasicAuthenticationFilter                             │
│    └── Processes HTTP Basic authentication               │
│                                                          │
│ 4. BearerTokenAuthenticationFilter                       │
│    └── Processes JWT/OAuth2 bearer tokens                │
│                                                          │
│ 5. ExceptionTranslationFilter                            │
│    └── Handles security exceptions                       │
│                                                          │
│ 6. FilterSecurityInterceptor                             │
│    └── Enforces authorization rules                      │
└─────────────────────────────────────────────────────────┘

SecurityContext and Principal

// Get the current authenticated user
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 
// Get username
String username = authentication.getName();
 
// Get authorities (roles/permissions)
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
 
// Get principal (usually UserDetails)
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
    UserDetails user = (UserDetails) principal;
    String username = user.getUsername();
}
 
// In a controller, inject directly
@GetMapping("/profile")
public ResponseEntity<User> getProfile(@AuthenticationPrincipal UserDetails user) {
    return ResponseEntity.ok(userService.findByUsername(user.getUsername()));
}

Authentication

Spring Security supports multiple authentication mechanisms. Here's how to implement the most common ones.

Basic Configuration (Spring Boot 3.x)

@Configuration
@EnableWebSecurity
public class SecurityConfig {
 
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**", "/auth/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard")
                .permitAll()
            )
            .logout(logout -> logout
                .logoutSuccessUrl("/login?logout")
                .permitAll()
            );
 
        return http.build();
    }
 
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

UserDetailsService

Load user data from your database.

@Service
public class CustomUserDetailsService implements UserDetailsService {
 
    private final UserRepository userRepository;
 
    public CustomUserDetailsService(UserRepository userRepository) {
        this.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(new String[0]))
            .build();
    }
}

Custom UserDetails

For more user information in the security context.

public class CustomUserDetails implements UserDetails {
 
    private final User user;
 
    public CustomUserDetails(User user) {
        this.user = user;
    }
 
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return user.getRoles().stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
            .collect(Collectors.toList());
    }
 
    @Override
    public String getPassword() {
        return user.getPassword();
    }
 
    @Override
    public String getUsername() {
        return user.getUsername();
    }
 
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
 
    @Override
    public boolean isAccountNonLocked() {
        return !user.isLocked();
    }
 
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
 
    @Override
    public boolean isEnabled() {
        return user.isEnabled();
    }
 
    // Custom methods
    public Long getId() {
        return user.getId();
    }
 
    public String getEmail() {
        return user.getEmail();
    }
}

Password Encoding

Never store plain-text passwords.

@Bean
public PasswordEncoder passwordEncoder() {
    // BCrypt is the recommended default
    return new BCryptPasswordEncoder();
}
 
// Usage in registration
public User registerUser(RegisterRequest request) {
    User user = new User();
    user.setUsername(request.getUsername());
    user.setPassword(passwordEncoder.encode(request.getPassword()));
    return userRepository.save(user);
}
 
// Password encoders available
BCryptPasswordEncoder       // Recommended, adaptive
Argon2PasswordEncoder       // Memory-hard, most secure
SCryptPasswordEncoder       // Memory-hard alternative
Pbkdf2PasswordEncoder       // NIST recommended

Authentication Provider

For custom authentication logic.

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
 
    private final UserDetailsService userDetailsService;
    private final 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())) {
            throw new BadCredentialsException("Invalid password");
        }
 
        // Additional checks
        if (!user.isEnabled()) {
            throw new DisabledException("Account is disabled");
        }
 
        return new UsernamePasswordAuthenticationToken(
            user, password, user.getAuthorities()
        );
    }
 
    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

Authorization

Control what authenticated users can access.

URL-Based Authorization

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth
        // Public endpoints
        .requestMatchers("/", "/public/**", "/auth/**").permitAll()
 
        // Static resources
        .requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
 
        // Role-based
        .requestMatchers("/admin/**").hasRole("ADMIN")
        .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
 
        // Authority-based (more granular)
        .requestMatchers(HttpMethod.DELETE, "/api/**").hasAuthority("DELETE_PRIVILEGE")
 
        // SpEL expressions
        .requestMatchers("/api/users/{id}/**")
            .access((authentication, context) -> {
                Long userId = Long.parseLong(context.getVariables().get("id"));
                // Custom logic
                return new AuthorizationDecision(
                    hasAccess(authentication.get(), userId)
                );
            })
 
        // Everything else requires authentication
        .anyRequest().authenticated()
    );
 
    return http.build();
}

Method Security

Enable with @EnableMethodSecurity:

@Configuration
@EnableMethodSecurity(
    prePostEnabled = true,  // @PreAuthorize, @PostAuthorize
    securedEnabled = true,  // @Secured
    jsr250Enabled = true    // @RolesAllowed
)
public class MethodSecurityConfig {
}

@PreAuthorize - Before method execution:

@Service
public class UserService {
 
    // Simple role check
    @PreAuthorize("hasRole('ADMIN')")
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
 
    // Multiple roles
    @PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
    public void updateUser(User user) {
        userRepository.save(user);
    }
 
    // SpEL with method parameters
    @PreAuthorize("#userId == principal.id or hasRole('ADMIN')")
    public User getUser(Long userId) {
        return userRepository.findById(userId).orElseThrow();
    }
 
    // Complex expressions
    @PreAuthorize("hasRole('ADMIN') and #user.department == principal.department")
    public void promoteUser(User user) {
        // ...
    }
 
    // Custom method
    @PreAuthorize("@securityService.canAccessResource(#resourceId)")
    public Resource getResource(Long resourceId) {
        return resourceRepository.findById(resourceId).orElseThrow();
    }
}

@PostAuthorize - After method execution:

// Check result after method executes
@PostAuthorize("returnObject.owner == principal.username or hasRole('ADMIN')")
public Document getDocument(Long id) {
    return documentRepository.findById(id).orElseThrow();
}

@Secured and @RolesAllowed:

// Spring's @Secured
@Secured("ROLE_ADMIN")
public void adminOnly() { }
 
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
public void adminOrManager() { }
 
// JSR-250 @RolesAllowed
@RolesAllowed("ADMIN")
public void adminOnly() { }

Role vs Authority

// Roles are authorities with ROLE_ prefix
// hasRole("ADMIN") checks for ROLE_ADMIN authority
 
// When creating authorities:
new SimpleGrantedAuthority("ROLE_ADMIN")  // For roles
new SimpleGrantedAuthority("READ_PRIVILEGE")  // For permissions
 
// In configuration:
.hasRole("ADMIN")           // Checks ROLE_ADMIN
.hasAuthority("ROLE_ADMIN") // Checks ROLE_ADMIN (explicit)
.hasAuthority("READ_PRIVILEGE") // Checks exact authority

JWT Authentication

Stateless authentication using JSON Web Tokens.

JWT Structure

Header.Payload.Signature

eyJhbGciOiJIUzI1NiJ9.           // Header (algorithm)
eyJzdWIiOiJ1c2VyMSIsInJvbGVzIjpbIlVTRVIiXX0.  // Payload (claims)
abc123signature                  // Signature

JWT Utility Class

@Component
public class JwtUtil {
 
    @Value("${jwt.secret}")
    private String secret;
 
    @Value("${jwt.expiration}")
    private long expiration;
 
    private SecretKey getSigningKey() {
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        return Keys.hmacShaKeyFor(keyBytes);
    }
 
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("roles", userDetails.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.toList()));
 
        return Jwts.builder()
            .setClaims(claims)
            .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 Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }
 
    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }
 
    private Claims extractAllClaims(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(getSigningKey())
            .build()
            .parseClaimsJws(token)
            .getBody();
    }
 
    public boolean validateToken(String token, UserDetails userDetails) {
        String username = extractUsername(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }
 
    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }
}

JWT Authentication Filter

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
 
    private final JwtUtil jwtUtil;
    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);
 
        try {
            String username = jwtUtil.extractUsername(token);
 
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
 
                if (jwtUtil.validateToken(token, userDetails)) {
                    UsernamePasswordAuthenticationToken authToken =
                        new UsernamePasswordAuthenticationToken(
                            userDetails,
                            null,
                            userDetails.getAuthorities()
                        );
 
                    authToken.setDetails(new WebAuthenticationDetailsSource()
                        .buildDetails(request));
 
                    SecurityContextHolder.getContext().setAuthentication(authToken);
                }
            }
        } catch (ExpiredJwtException e) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("Token expired");
            return;
        } catch (JwtException e) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("Invalid token");
            return;
        }
 
        filterChain.doFilter(request, response);
    }
}

JWT Security Configuration

@Configuration
@EnableWebSecurity
public class JwtSecurityConfig {
 
    private final JwtAuthenticationFilter jwtAuthFilter;
    private final UserDetailsService userDetailsService;
 
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // Disable CSRF for stateless API
            .csrf(csrf -> csrf.disable())
 
            // Stateless session
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
 
            // Authorization rules
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
 
            // Add JWT filter before username/password filter
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
 
        return http.build();
    }
 
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

Login Endpoint

@RestController
@RequestMapping("/auth")
public class AuthController {
 
    private final AuthenticationManager authenticationManager;
    private final JwtUtil jwtUtil;
    private final UserDetailsService userDetailsService;
 
    @PostMapping("/login")
    public ResponseEntity<AuthResponse> login(@RequestBody LoginRequest request) {
        authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                request.getUsername(),
                request.getPassword()
            )
        );
 
        UserDetails user = userDetailsService.loadUserByUsername(request.getUsername());
        String token = jwtUtil.generateToken(user);
 
        return ResponseEntity.ok(new AuthResponse(token));
    }
 
    @PostMapping("/refresh")
    public ResponseEntity<AuthResponse> refresh(@RequestHeader("Authorization") String authHeader) {
        String oldToken = authHeader.substring(7);
        String username = jwtUtil.extractUsername(oldToken);
        UserDetails user = userDetailsService.loadUserByUsername(username);
 
        if (jwtUtil.validateToken(oldToken, user)) {
            String newToken = jwtUtil.generateToken(user);
            return ResponseEntity.ok(new AuthResponse(newToken));
        }
 
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
    }
}

OAuth2 & OpenID Connect

Modern authentication for APIs and single sign-on.

OAuth2 Roles

RoleDescription
Resource OwnerThe user who owns the data
ClientApplication requesting access
Authorization ServerIssues tokens (Keycloak, Auth0, Okta)
Resource ServerAPI that validates tokens

Resource Server Configuration

# application.yml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com/realms/myrealm
          # Or specify JWK Set URI directly
          # jwk-set-uri: https://auth.example.com/.well-known/jwks.json
@Configuration
@EnableWebSecurity
public class OAuth2ResourceServerConfig {
 
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            );
 
        return http.build();
    }
 
    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
        authoritiesConverter.setAuthoritiesClaimName("roles");
        authoritiesConverter.setAuthorityPrefix("ROLE_");
 
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
        return converter;
    }
}

OAuth2 Client (Login with Google/GitHub)

# 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, user:email
@Configuration
@EnableWebSecurity
public class OAuth2LoginConfig {
 
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/login/**", "/error").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard")
                .userInfoEndpoint(userInfo -> userInfo
                    .userService(customOAuth2UserService())
                )
            );
 
        return http.build();
    }
 
    @Bean
    public OAuth2UserService<OAuth2UserRequest, OAuth2User> customOAuth2UserService() {
        return new CustomOAuth2UserService();
    }
}

Custom OAuth2 User Service

@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
 
    private final UserRepository userRepository;
 
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oauth2User = super.loadUser(userRequest);
 
        String provider = userRequest.getClientRegistration().getRegistrationId();
        String providerId = oauth2User.getAttribute("sub"); // or "id" for GitHub
        String email = oauth2User.getAttribute("email");
        String name = oauth2User.getAttribute("name");
 
        // Find or create user
        User user = userRepository.findByProviderAndProviderId(provider, providerId)
            .orElseGet(() -> {
                User newUser = new User();
                newUser.setProvider(provider);
                newUser.setProviderId(providerId);
                newUser.setEmail(email);
                newUser.setName(name);
                newUser.setRoles(Set.of("USER"));
                return userRepository.save(newUser);
            });
 
        return new CustomOAuth2User(user, oauth2User.getAttributes());
    }
}

Common Security Configurations

CORS Configuration

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .cors(cors -> cors.configurationSource(corsConfigurationSource()))
        // ... other config
    ;
 
    return http.build();
}
 
@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(List.of("https://example.com", "http://localhost:3000"));
    configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    configuration.setAllowedHeaders(List.of("*"));
    configuration.setAllowCredentials(true);
    configuration.setMaxAge(3600L);
 
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", configuration);
    return source;
}

CSRF Configuration

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        // For traditional web apps - CSRF enabled by default
        .csrf(csrf -> csrf
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .ignoringRequestMatchers("/api/webhooks/**") // Exclude webhooks
        )
 
        // For stateless APIs - disable CSRF
        // .csrf(csrf -> csrf.disable())
    ;
 
    return http.build();
}

Security Headers

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .headers(headers -> headers
            .contentSecurityPolicy(csp -> csp
                .policyDirectives("default-src 'self'; script-src 'self'")
            )
            .frameOptions(frame -> frame.deny())
            .xssProtection(xss -> xss.disable()) // Modern browsers have built-in protection
            .contentTypeOptions(Customizer.withDefaults()) // X-Content-Type-Options: nosniff
            .httpStrictTransportSecurity(hsts -> hsts
                .includeSubDomains(true)
                .maxAgeInSeconds(31536000)
            )
        );
 
    return http.build();
}

Session Management

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            // Session creation policy
            .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
 
            // Concurrent session control
            .maximumSessions(1)
            .maxSessionsPreventsLogin(false) // Kicks out previous session
 
            // Session fixation protection
            .sessionFixation().migrateSession()
 
            // Invalid session handling
            .invalidSessionUrl("/login?invalid")
        );
 
    return http.build();
}

Security Best Practices

Password Storage

// Always use strong password encoder
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12); // Work factor 12
}
 
// For upgrading legacy passwords
@Bean
public PasswordEncoder passwordEncoder() {
    String encodingId = "bcrypt";
    Map<String, PasswordEncoder> encoders = new HashMap<>();
    encoders.put("bcrypt", new BCryptPasswordEncoder());
    encoders.put("sha256", new StandardPasswordEncoder()); // Legacy
 
    return new DelegatingPasswordEncoder(encodingId, encoders);
}

Rate Limiting

@Component
public class RateLimitingFilter extends OncePerRequestFilter {
 
    private final RateLimiter rateLimiter;
 
    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
 
        String clientIp = request.getRemoteAddr();
 
        if (!rateLimiter.tryAcquire(clientIp)) {
            response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            response.getWriter().write("Rate limit exceeded");
            return;
        }
 
        filterChain.doFilter(request, response);
    }
}

Preventing Common Attacks

@Configuration
@EnableWebSecurity
public class SecurityConfig {
 
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // XSS Protection - use Content-Security-Policy
            .headers(headers -> headers
                .contentSecurityPolicy(csp -> csp
                    .policyDirectives("default-src 'self'; script-src 'self'")
                )
            )
 
            // SQL Injection - use parameterized queries (JPA does this)
 
            // CSRF - enabled by default for stateful apps
 
            // Clickjacking - frame options
            .headers(headers -> headers
                .frameOptions(frame -> frame.deny())
            )
 
            // Secure cookies
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
            );
 
        return http.build();
    }
}

Logging Security Events

@Component
public class AuthenticationEventListener {
 
    private static final Logger logger = LoggerFactory.getLogger(AuthenticationEventListener.class);
 
    @EventListener
    public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
        String username = event.getAuthentication().getName();
        logger.info("Successful login: {}", username);
    }
 
    @EventListener
    public void onAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
        String username = event.getAuthentication().getName();
        logger.warn("Failed login attempt: {} - Reason: {}",
            username, event.getException().getMessage());
    }
}

Testing Security

@SpringBootTest
@AutoConfigureMockMvc
class SecurityTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @Test
    void publicEndpoint_shouldBeAccessible() throws Exception {
        mockMvc.perform(get("/public/health"))
            .andExpect(status().isOk());
    }
 
    @Test
    void protectedEndpoint_shouldReturn401_whenNotAuthenticated() throws Exception {
        mockMvc.perform(get("/api/users"))
            .andExpect(status().isUnauthorized());
    }
 
    @Test
    @WithMockUser(username = "user", roles = "USER")
    void protectedEndpoint_shouldBeAccessible_whenAuthenticated() throws Exception {
        mockMvc.perform(get("/api/users"))
            .andExpect(status().isOk());
    }
 
    @Test
    @WithMockUser(username = "user", roles = "USER")
    void adminEndpoint_shouldReturn403_forRegularUser() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isForbidden());
    }
 
    @Test
    @WithMockUser(username = "admin", roles = "ADMIN")
    void adminEndpoint_shouldBeAccessible_forAdmin() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isOk());
    }
}

Quick Reference

Authentication vs Authorization:

  • Authentication = Who are you?
  • Authorization = What can you do?

Filter chain order:

  1. SecurityContextPersistenceFilter
  2. Authentication filters
  3. ExceptionTranslationFilter
  4. FilterSecurityInterceptor

Method security annotations:

  • @PreAuthorize - SpEL expressions, most flexible
  • @Secured - Simple role checks
  • @RolesAllowed - JSR-250 standard

JWT setup:

  1. Create JWT utility (generate, validate)
  2. Create JWT filter (extract, authenticate)
  3. Configure stateless session
  4. Disable CSRF

OAuth2 Resource Server:

  1. Add starter dependency
  2. Configure issuer-uri
  3. Customize JWT converter for roles

Related Articles


What's Next?

Spring Security is vast—this guide covers what matters for interviews. In practice, start with the simplest configuration that meets your needs, then add complexity as required.

Remember: security is not a feature, it's a requirement. Every endpoint should be consciously configured as public or protected. Default to authenticated access.

Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides