Step 2: Configuration Fundamentals – Spring Security Series

⚙️ Step 2: Configuration Fundamentals

🎥 YouTube Video: Coming Soon – Subscribe to JavaiOS Channel


From Default to Advanced Spring Security Setup 🔑

This guide walks through the essential steps for configuring Spring Security, starting from basic default setup and progressing to advanced, production-ready configurations. Understanding these fundamentals is crucial before implementing custom authentication providers and JWT token handling.


📋 Default Spring Security Setup

Basic Auto-Configuration

When you add Spring Security to your project, it provides sensible defaults:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

What you get automatically:

  • All endpoints secured by default
  • Form-based authentication
  • Basic HTTP authentication
  • Default user: user
  • Generated password in console logs
  • CSRF protection enabled
  • Session management

Default Security Filter Chain

Spring Boot automatically creates a SecurityFilterChain equivalent to:

@Configuration
@EnableWebSecurity
public class DefaultSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .formLogin(withDefaults())
            .httpBasic(withDefaults())
            .build();
    }
}

🚀 Transitioning to Custom Configuration

Step 1: Create Custom Security Configuration

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .build();
    }
}

Step 2: Custom UserDetailsService

@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: " + username));

        return org.springframework.security.core.userdetails.User
            .withUsername(user.getUsername())
            .password(user.getPassword())
            .authorities(user.getRoles())
            .accountExpired(false)
            .accountLocked(false)
            .credentialsExpired(false)
            .disabled(false)
            .build();
    }
}

Step 3: Password Encoder Configuration

@Configuration
public class PasswordConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
}

⚡ Advanced Configuration with JWT

Complete Security Configuration

@Configuration
@EnableWebSecurity
public class AdvancedSecurityConfig {

    @Autowired
    private CustomUserDetailsService userDetailsService;
    
    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    
    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(
                    "/auth/login", 
                    "/auth/register", 
                    "/public/**",
                    "/v3/api-docs/**",
                    "/swagger-ui/**"
                ).permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authenticationProvider(authenticationProvider())
            .addFilterBefore(jwtRequestFilter, 
                UsernamePasswordAuthenticationFilter.class)
            .build();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = 
            new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
}

JWT Authentication Filter

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
            HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        final String requestTokenHeader = request.getHeader("Authorization");

        String username = null;
        String jwtToken = null;

        // JWT Token is in the form "Bearer token"
        if (requestTokenHeader != null && 
            requestTokenHeader.startsWith("Bearer ")) {
            jwtToken = requestTokenHeader.substring(7);
            try {
                username = jwtUtil.getUsernameFromToken(jwtToken);
            } catch (IllegalArgumentException e) {
                logger.error("Unable to get JWT Token");
            } catch (ExpiredJwtException e) {
                logger.error("JWT Token has expired");
            }
        }

        // Validate token
        if (username != null && 
            SecurityContextHolder.getContext().getAuthentication() == null) {

            UserDetails userDetails = 
                this.userDetailsService.loadUserByUsername(username);

            if (jwtUtil.validateToken(jwtToken, userDetails)) {
                UsernamePasswordAuthenticationToken authToken = 
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                        
                authToken.setDetails(
                    new WebAuthenticationDetailsSource().buildDetails(request));
                    
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        chain.doFilter(request, response);
    }
}

🔧 Configuration Best Practices

1. Security Filter Order

Understanding filter execution order is crucial:

// Correct filter ordering
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(customAuthFilter, BasicAuthenticationFilter.class)

2. Exception Handling

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, 
            HttpServletResponse response,
            AuthenticationException authException) throws IOException {
            
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, 
            "Unauthorized: " + authException.getMessage());
    }
}

3. CORS Configuration

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOriginPatterns(Arrays.asList("*"));
    configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
    configuration.setAllowedHeaders(Arrays.asList("*"));
    configuration.setAllowCredentials(true);
    
    UrlBasedCorsConfigurationSource source = 
        new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

📊 Configuration Comparison

Feature Default Configuration Advanced Configuration
Authentication Form login + Basic Auth JWT + Custom Provider
Session Management Session-based Stateless (JWT)
User Store In-memory Database via UserDetailsService
Password Encoding Default delegating encoder BCrypt with custom strength
CSRF Protection Enabled Disabled (stateless)
Exception Handling Default error pages Custom JSON responses

🔍 Common Configuration Issues

Issue 1: Filter Order Problems

Problem: JWT filter not executing before authentication

// ❌ Wrong
.addFilterAfter(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)

// ✅ Correct  
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)

Issue 2: Circular Bean Dependencies

Problem: AuthenticationManager circular dependency

// ❌ Can cause circular dependency
@Autowired
private AuthenticationManager authenticationManager;

// ✅ Better approach
@Bean
public AuthenticationManager authenticationManager(
        AuthenticationConfiguration config) throws Exception {
    return config.getAuthenticationManager();
}

Issue 3: CORS Issues with JWT

Problem: Preflight requests failing

// ✅ Add CORS configuration
.cors(cors -> cors.configurationSource(corsConfigurationSource()))

✅ Configuration Checklist

Security Configuration:

  • ✅ CSRF disabled for stateless JWT
  • ✅ Session policy set to STATELESS
  • ✅ Public endpoints configured with permitAll()
  • ✅ Protected endpoints require authentication
  • ✅ Custom authentication provider registered

JWT Configuration:

  • ✅ JWT filter added before username/password filter
  • ✅ Custom authentication entry point configured
  • ✅ Token validation logic implemented
  • ✅ Security context set properly

Additional Security:

  • ✅ CORS configured for frontend integration
  • ✅ Password encoder with appropriate strength
  • ✅ Exception handling for authentication failures
  • ✅ Proper filter ordering maintained

🚀 Continue Your Security Journey

Part of the AdventureTube technical blog series supporting the JavaiOS YouTube channel.

Leave a Comment

Your email address will not be published. Required fields are marked *