java5 min read

Spring Security Tutorial: Learn Authentication from Scratch (2026)

Spring Security Tutorial: Learn Authentication from Scratch (2026)

Published:  |  Category: Java  |  Reading time: ~15 min
Spring Security Tutorial: Learn Authentication from Scratch (2026)

Spring Security provides comprehensive authentication, authorization, and protection against common attacks like CSRF and session fixation. I have integrated it into dozens of applications, from simple single-page apps to multi-tenant SaaS platforms with OAuth2 federation. Its filter chain architecture, while intimidating at first, becomes intuitive once you understand how each filter handles a specific security concern.

This tutorial covers authentication providers, security filter chains, method-level security, OAuth2 resource server configuration, CORS, and CSRF protection.

Security Filter Chain Architecture

Spring Security operates through a chain of filters. Each filter checks a specific condition: BasicAuthenticationFilter processes Basic auth headers, UsernamePasswordAuthenticationFilter handles form logins, and ExceptionTranslationFilter translates security exceptions to HTTP responses. The order of filters is fixed but you can add custom filters at specific positions.

The SecurityFilterChain bean defines which paths a filter chain applies to and what rules they enforce. You can declare multiple chains for different URL patterns — for example, one chain for public API endpoints and another for administrative routes.

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/**")
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
        return http.build();
    }
}

Authentication Providers

AuthenticationProvider is where the actual credential validation happens. DaoAuthenticationProvider delegates to a UserDetailsService to load user details and uses a PasswordEncoder to verify passwords. For token-based systems, implement a JwtAuthenticationProvider that validates JWT signatures and extracts claims.

BCryptPasswordEncoder is the default password encoder and remains the safest choice for password hashing in 2026. Avoid MD5, SHA-1, or plain-text encoders. If migrating legacy hashes, use a delegating password encoder that tries multiple formats.

@Service
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email) 
            throws UsernameNotFoundException {
        return userRepository.findByEmail(email)
            .map(user -> User.builder()
                .username(user.getEmail())
                .password(user.getPassword())
                .roles(user.getRole().name())
                .build())
            .orElseThrow(() -> new UsernameNotFoundException("User not found"));
    }
}

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

Method-Level Security

@PreAuthorize and @PostAuthorize evaluate SpEL expressions before or after method execution. Use @PreAuthorize for access control on service methods: @PreAuthorize("hasRole('ADMIN') or #order.customerId == authentication.principal.id"). Method-level security works with any bean in the Spring context.

@Secured is a simpler alternative that accepts role names as strings. @RolesAllowed is the JSR-250 equivalent. For fine-grained permissions, combine Spring Security ACL or write a custom PermissionEvaluator.

@Service
public class OrderService {
    @PreAuthorize("hasPermission(#orderId, 'com.example.Order', 'READ')")
    public OrderResponse getOrder(Long orderId) {
        return orderRepository.findById(orderId)
            .map(OrderResponse::from)
            .orElseThrow();
    }

    @PostFilter("filterObject.customerId == authentication.principal.id")
    public List getMyOrders() {
        return orderRepository.findAll().stream()
            .map(OrderResponse::from)
            .toList();
    }
}

OAuth2 Resource Server Configuration

Spring Security supports OAuth2 bearer tokens out of the box. Configure the issuer URI and Spring Security automatically fetches the JWKS key set to validate token signatures. You can customize the JwtDecoder for additional claims validation or audience checking.

For token introspection (opaque tokens), configure the introspection endpoint and client credentials. The NimbusJwtDecoder handles JWK set caching, reducing HTTP calls to the authorization server.

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com
          jwk-set-uri: https://auth.example.com/.well-known/jwks.json

@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
        .jwsAlgorithm(SignatureAlgorithm.RS256)
        .build();
}

CORS and CSRF Protection

Cross-Origin Resource Sharing (CORS) policies restrict which origins can access your API. Configure allowedOrigins, allowedMethods, and allowedHeaders in the CorsConfigurationSource. For public APIs, allow specific origins rather than using wildcards in production.

CSRF protection is enabled by default for state-changing operations in Spring Security. For stateless REST APIs using bearer tokens or Basic auth, disable CSRF — it serves no purpose when the server does not rely on session cookies. For traditional MVC applications with server-side rendering, keep CSRF enabled.

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("https://app.example.com"));
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    config.setAllowedHeaders(List.of("*"));
    config.setExposedHeaders(List.of("X-Request-Id"));
    
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", config);
    return source;
}

// Stateless API: disable CSRF
http.csrf(AbstractHttpConfigurer::disable);

Testing Security Configuration

Spring Security provides testing support via @WithMockUser, @WithAnonymousUser, and SecurityMockMvcRequestPostProcessors. Use @WithMockUser to simulate authenticated requests with specific roles and authorities. For OAuth2 resource servers, use @WithMockJwt or mock the JwtDecoder.

Test both happy paths (authenticated, authorized) and failure paths (unauthenticated, insufficient permissions). Security tests should be part of every controller test suite — a misconfigured security rule can silently expose sensitive data.

@WebMvcTest(OrderController.class)
@Import(SecurityConfig.class)
class OrderControllerSecurityTest {
    @Autowired private MockMvc mockMvc;
    @MockBean private OrderService orderService;

    @Test
    @WithAnonymousUser
    void rejectsUnauthenticatedAccess() throws Exception {
        mockMvc.perform(get("/api/orders"))
            .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(roles = "ADMIN")
    void allowsAdminAccess() throws Exception {
        mockMvc.perform(get("/api/admin/reports"))
            .andExpect(status().isOk());
    }

    @Test
    @WithMockUser(roles = "USER")
    void blocksUserFromAdminEndpoints() throws Exception {
        mockMvc.perform(get("/api/admin/reports"))
            .andExpect(status().isForbidden());
    }
}

Frequently Asked Questions

Should I store JWT tokens in localStorage or cookies?

Use httpOnly secure cookies with SameSite=Strict for the access token to prevent XSS-based token theft. Store the refresh token in a separate cookie with stricter path restrictions. Avoid localStorage for tokens in browser-based applications.

How do I implement password reset securely?

Generate a cryptographically random token (not a JWT), store its hash in the database with an expiration, and email it as a link. Validate the token, prompt for new password, enforce strength rules, invalidate all existing sessions after reset.

What is the difference between hasRole and hasAuthority?

hasRole adds the ROLE_ prefix automatically (hasRole('ADMIN') checks for ROLE_ADMIN). hasAuthority checks for the exact string you provide. Use hasRole for roles and hasAuthority for fine-grained permissions like 'ORDER_WRITE'.

How do I handle multiple authentication mechanisms?

Define multiple SecurityFilterChain beans with different securityMatcher patterns. For example, one chain for API endpoints with JWT, another for admin pages with form login, and a third for actuator endpoints with IP allowlisting.

Originally published on Ayodhyyya. Last updated June 1, 2026.