Skip to content

Spring Security

Spring Security provides authentication and authorization for Spring applications. It uses a filter chain that intercepts every request. Key concepts: SecurityFilterChain (defines security rules), UserDetailsService (loads user data), PasswordEncoder (bcrypt), JWT-based stateless auth, method-level security (@PreAuthorize), and CORS/CSRF configuration.


Security Filter Chain

Every HTTP request passes through a chain of security filters: SecurityContextPersistenceFilterCsrfFilterUsernamePasswordAuthenticationFilter (or custom JWT filter) → ExceptionTranslationFilterFilterSecurityInterceptor (authorization). Configure via SecurityFilterChain bean using the DSL builder pattern.

Deep Dive: Configuration (Spring Boot 3+)
Request → DelegatingFilterProxy → FilterChainProxy
          → SecurityContextPersistenceFilter
          → CsrfFilter
          → UsernamePasswordAuthenticationFilter (or JwtFilter)
          → ExceptionTranslationFilter
          → FilterSecurityInterceptor (authorization)
          → Controller
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @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()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/users/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated())
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

Authentication Flow

Flow: request with credentials → AuthenticationFilter creates token → AuthenticationManager delegates to AuthenticationProvider → uses UserDetailsService to load user → PasswordEncoder verifies password → on success, SecurityContextHolder stores authentication. Implement UserDetailsService to load users from your database.

Deep Dive: Custom UserDetailsService
@Service
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        User user = userRepository.findByEmail(username)
            .orElseThrow(() ->
                new UsernameNotFoundException("User not found: " + username));

        return org.springframework.security.core.userdetails.User.builder()
            .username(user.getEmail())
            .password(user.getPassword())  // Already encoded
            .roles(user.getRoles().stream()
                .map(Role::getName).toArray(String[]::new))
            .build();
    }
}

JWT Authentication

JWT flow: 1) Client sends credentials to /api/auth/login. 2) Server validates, generates JWT, returns it. 3) Client includes JWT in Authorization: Bearer <token> header. 4) Custom JwtAuthenticationFilter (extends OncePerRequestFilter) validates the token on each request and sets SecurityContext. Stateless — no server-side sessions.

Deep Dive: JWT Utility & Filter
@Component
public class JwtUtil {
    @Value("${jwt.secret}")
    private String secret;

    public String generateToken(UserDetails userDetails) {
        return Jwts.builder()
            .setSubject(userDetails.getUsername())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + 86400000))
            .signWith(getSigningKey(), SignatureAlgorithm.HS256)
            .compact();
    }

    public boolean isTokenValid(String token, UserDetails userDetails) {
        return extractUsername(token).equals(userDetails.getUsername())
                && !isTokenExpired(token);
    }
}
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String authHeader = request.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            chain.doFilter(request, response); return;
        }
        String token = authHeader.substring(7);
        String username = jwtUtil.extractUsername(token);

        if (username != null &&
                SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (jwtUtil.isTokenValid(token, userDetails)) {
                var authToken = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        chain.doFilter(request, response);
    }
}

Method-Level Security

Enable with @EnableMethodSecurity. Use @PreAuthorize to restrict method access based on roles or SpEL expressions. @PreAuthorize("hasRole('ADMIN')") checks roles. @PreAuthorize("#userId == authentication.principal.id") checks ownership. @PostAuthorize checks after the method returns.

Deep Dive: SpEL Examples
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig { }

@Service
public class UserService {
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteUser(Long id) { ... }

    @PreAuthorize("#userId == authentication.principal.id")
    public User getProfile(Long userId) { ... }

    @PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
    public void updateUser(Long userId, UserUpdateRequest request) { ... }

    @PostAuthorize("returnObject.owner == authentication.principal.username")
    public Document getDocument(Long docId) { ... }
}

CORS & CSRF Configuration

CORS (Cross-Origin Resource Sharing) — configure via WebMvcConfigurer.addCorsMappings() or in SecurityFilterChain. Specify allowed origins, methods, headers. CSRF (Cross-Site Request Forgery) — enabled by default for form-based apps. Disable for stateless JWT APIs (no sessions = no CSRF risk). Keep for session-based apps.

Deep Dive: Configuration
// CORS via WebMvcConfigurer
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("http://localhost:3000", "https://myapp.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE")
            .allowedHeaders("*")
            .allowCredentials(true);
    }
}

// CSRF — disable for stateless API
http.csrf(csrf -> csrf.disable());

// CSRF — enable with cookie token (for SPA + session)
http.csrf(csrf -> csrf
    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));

Common Interview Questions

Common Interview Questions
  • How does Spring Security work at a high level?
  • What is the Security Filter Chain?
  • How does authentication work in Spring Security?
  • What is UserDetailsService?
  • How do you implement JWT authentication?
  • What is the difference between authentication and authorization?
  • What is @PreAuthorize? Give examples.
  • What is CSRF? When should you disable it?
  • How do you configure CORS?
  • What is PasswordEncoder? Why use BCrypt?
  • What is SecurityContextHolder?