Understanding the Core of Spring Security





Spring Security Setup → From Zero to JWT (Spring Boot 3.2+) | www.codegigs.app





Spring Security Setup → From Zero to JWT (Spring Boot 3.2+)

I spent the first two years of my career scared of Spring Security. You copy a config from Stack Overflow, it works, and you pray nobody touches it. Then Spring Boot 3 came out, deprecated WebSecurityConfigurerAdapter, and broke everything.

If you’re staring at a 403 Forbidden error right now, you’re not alone. There’s a thread on r/SpringBoot from last month with 50+ comments of people raging about the exact same migration issues.

At www.codegigs.app, I’ve helped thousands of developers fix this. Forget the theory for a minute. Here is the actual code you need to get a secure API running in 2025.

The Core: SecurityFilterChain (Goodbye Adapters)

In the old days, we extended a class. Now, we define a Bean. This is the entry point for everything.

Think of this filter chain like airport security. If you don’t have a ticket (Token), you don’t get past the first guard. If you have a ticket but it’s for the wrong flight (Role), you get stopped at the gate.

// SecurityConfig.java
// Spring Boot 3.2+, Spring Security 6.2+
package app.codegigs.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthFilter;
    private final AuthenticationProvider authenticationProvider;

    // Constructor injection
    public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter, 
                          AuthenticationProvider authenticationProvider) {
        this.jwtAuthFilter = jwtAuthFilter;
        this.authenticationProvider = authenticationProvider;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // Disable CSRF for stateless APIs
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll() // Public endpoints
                .requestMatchers("/api/admin/**").hasRole("ADMIN") // Admin only
                .anyRequest().authenticated() // Everything else needs a token
            )
            .sessionManagement(sess -> sess
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // No cookies!
            )
            .authenticationProvider(authenticationProvider)
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

What just happened?

Lines 29-33 are where most people mess up. In Spring Boot 3, it’s authorizeHttpRequests, not authorizeRequests. Use the wrong one, and your app might start but your rules will be ignored.

Also, notice line 40. We are shoving our custom JWT filter before the standard password filter. If you get the order wrong, Spring tries to look for a session ID, finds nothing, and kicks you out before your JWT logic even runs.

Database Auth: The Real UserDetailsService

In-memory authentication is fine for tutorials, but please don’t ship it. You need to load users from your actual database.

Spring provides an interface called UserDetailsService. It has one job: take a username, and find the user.

// ApplicationConfig.java
@Bean
public UserDetailsService userDetailsService() {
    return username -> repository.findByEmail(username)
        .orElseThrow(() -> new UsernameNotFoundException("User not found"));
}

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

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

This is the production pattern we teach at www.codegigs.app. By defining the `AuthenticationProvider` bean manually, we tell Spring exactly how to check passwords (BCrypt) and where to find users.

The JWT Filter (Where the Magic Happens)

This is the part that usually scares people. It’s actually just a loop that runs on every request.

// JwtAuthenticationFilter.java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(
            @NonNull HttpServletRequest request,
            @NonNull HttpServletResponse response,
            @NonNull FilterChain filterChain
    ) throws ServletException, IOException {
        
        final String authHeader = request.getHeader("Authorization");
        final String jwt;
        final String userEmail;

        // 1. Check if token exists
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        jwt = authHeader.substring(7);
        userEmail = jwtService.extractUsername(jwt);

        // 2. Check if user is not already authenticated
        if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
            
            // 3. Validate and set context
            if (jwtService.isTokenValid(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                );
                authToken.setDetails(
                        new WebAuthenticationDetailsSource().buildDetails(request)
                );
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        filterChain.doFilter(request, response);
    }
}
The “Silent Failure” Trap: See line 21? If the header is missing, we just call filterChain.doFilter and return. We don’t throw an error. Why? Because this request might be for a public endpoint (like /login). If we threw an error here, nobody could ever log in!

Why Am I Still Getting 403?

You set everything up, hit your endpoint, and… 403 Forbidden. I’ve been there. Check these three things immediately:

  1. The “ROLE_” Prefix: In your database, your role is probably “ADMIN”. But Spring Security expects “ROLE_ADMIN”. You can handle this in your `UserDetails` implementation or just fix your database data. A Stack Overflow user wasted days on this recently.
  2. Method Security: If you use @PreAuthorize, you must add @EnableMethodSecurity to your config class. It’s not on by default anymore.
  3. CSRF: If you are testing with Postman, make sure CSRF is disabled (as shown in the first code block).

If you’re struggling with Method Security specifically, check out our deep dive on Protecting Application Logic.

Conclusion

Spring Security is powerful, but it doesn’t have to be complicated. Start with the `SecurityFilterChain`, wire up your database, and stick a JWT filter in front.

That’s the foundation. Once you have this working, you can start looking at more advanced topics like API Keys or OAuth2.

Want to Master the Rest?

This was just Part 1. In the full course, we build a complete banking API with 2FA, Email Verification, and Rate Limiting.

Start the Spring Security Master Class



[Spring Security Fix: 401 vs 403 for JWT Authentication](https://www.youtube.com/watch?v=ucx6wo6dp98)

This video is relevant because it specifically addresses the common confusion between 401 and 403 errors when configuring JWT authentication in Spring Boot, which directly complements the troubleshooting section of the article.

http://googleusercontent.com/youtube_content/0

Leave a Comment

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

Scroll to Top