Learn Spring Boot - Basic Security & Authentication
Episode 12 of 24

Learn Spring Boot - Basic Security & Authentication

This episode opens up Spring Security: the filter chain, configuring the authentication manager and user details, form login and basic auth, and CSRF, CORS, and security header protection for secure web applications and APIs.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Security isn't an optional feature — it's the layer that protects the whole application. Episode 12 introduces Spring Security, the de facto standard for authentication and authorization in the Spring ecosystem.

You'll understand the filter chain that forms the backbone of security, configure users and the authentication manager, use form login and basic auth, and enable CSRF, CORS, and security header protection. Episode 13 will extend this to JWT and OAuth 2.0.

Introducing Spring Security and the Filter Chain

Adding the Dependency

Add the security starter to the project:

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

Instantly, every endpoint is locked down — Spring Security enables defaults that deny all requests without authentication. This is secure-by-default behavior.

The Filter Chain

The filter chain is a sequence of filters that processes every request before it reaches a controller. Each filter has a responsibility: reading credentials, authenticating, applying authorization, and writing security headers.

The Spring Security filter chain flow
Request -> SecurityContextHolderFilter
        -> CsrfFilter
        -> UsernamePasswordAuthenticationFilter
        -> AuthorizationFilter
        -> Controller

Understanding this order helps you read security stack traces and know which filter rejected your request. Spring Boot 3 uses a component-based SecurityFilterChain configured with a fluent API.

Configuring the Authentication Manager

UserDetailsService and PasswordEncoder

Authentication requires two components: UserDetailsService to load the user, and PasswordEncoder to match passwords. Start with users in memory:

In-memory user and encoder
@Bean
public UserDetailsService userDetailsService() {
    UserDetails admin = User.withUsername("admin")
            .password(passwordEncoder().encode("admin123"))
            .roles("ADMIN")
            .build();
    return new InMemoryUserDetailsManager(admin);
}
 
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

Never store plaintext passwords. BCryptPasswordEncoder produces a hash with automatic salting — the recommended standard. For real applications, implement a UserDetailsService that loads users from the database.

Connecting to the Database

A common production form: users are stored in the database and loaded through a repository:

UserDetailsService from the database
@Service
public class CustomUserDetailsService
        implements UserDetailsService {
 
    private final UserRepository repository;
 
    public CustomUserDetailsService(UserRepository repository) {
        this.repository = repository;
    }
 
    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        return repository.findByUsername(username)
                .map(user -> User.withUsername(user.getUsername())
                        .password(user.getPassword())
                        .roles(user.getRole())
                        .build())
                .orElseThrow(() -> new UsernameNotFoundException(
                        "User tidak ditemukan"));
    }
}

With this pattern, authentication comes from real data in the database. The password hash still uses BCryptPasswordEncoder.

Configuring the SecurityFilterChain

Form Login and Basic Auth

Set security policy with SecurityFilterChain. Example using form login for a web application and allowing public pages:

Security filter chain
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
        throws Exception {
    return http
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/", "/home", "/register")
                    .permitAll()
                    .requestMatchers("/api/admin/**")
                    .hasRole("ADMIN")
                    .anyRequest().authenticated())
            .formLogin(Customizer.withDefaults())
            .httpBasic(Customizer.withDefaults())
            .build();
}

Rules are read from top to bottom: public pages are allowed without login, the admin area is restricted to the ADMIN role, and everything else requires authentication. The order of requestMatchers strongly determines the final result.

CSRF, CORS, and Security Header Protection

CSRF for Browsers vs Stateless APIs

CSRF protects against attacks that force an authenticated user to send a malicious request. Spring enables it by default with a CSRF token. For stateless REST APIs that use tokens (not cookies), CSRF isn't very relevant and is often disabled:

Disable CSRF for stateless APIs
return http
        .csrf(csrf -> csrf.disable())
        .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated())
        .build();

For cookie-based web applications with form login, keep CSRF enabled. This decision depends on the authentication mechanism in use — bearer tokens don't need CSRF, session cookies do.

CORS and Security Headers

CORS controls which origins can access your API from a browser:

CORS configuration
return http
        .cors(cors -> cors.configurationSource(request -> {
            CorsConfiguration config = new CorsConfiguration();
            config.setAllowedOrigins(List.of("https://app.example.com"));
            config.setAllowedMethods(List.of("GET", "POST", "PUT"));
            config.setAllowedHeaders(List.of("Authorization"));
            return config;
        }))
        .build();

Don't open up all origins unless you truly need to — restrict them to the domains you control. In addition, Spring adds security headers automatically: X-Content-Type-Options, Strict-Transport-Security, and frame options, which protect against various browser attacks.

Testing Security

Test the configuration with curl. Without credentials, requests are rejected with status 401:

Test basic authentication
curl -I http://localhost:8080/api/items
curl -u admin:admin123 http://localhost:8080/api/admin

The curl -u admin:admin123 http://localhost:8080/api/admin command sends basic auth — an Authorization header containing the username and password. The first request without credentials will receive 401 Unauthorized, and the second will only succeed for a user with the ADMIN role.

Closing

Episode 12 equipped you with Spring Security basics: understanding the filter chain, configuring the authentication manager with UserDetailsService and PasswordEncoder, setting access rules in SecurityFilterChain, and enabling CSRF, CORS, and security header protection.

Key takeaways:

  • spring-boot-starter-security locks down every endpoint by default.
  • UserDetailsService loads users; BCryptPasswordEncoder hashes passwords.
  • SecurityFilterChain sets authorization rules with requestMatchers.
  • CSRF is required for session-based applications; it can be disabled for stateless APIs.
  • Restrict CORS origins to the domains you control.
  • Security headers are added automatically by Spring Security.

In the next episode, episode 13, we'll discuss OAuth 2.0 and JWT — the roles of an authorization server vs a resource server, implementing token-based auth with JWT, integrating external identity providers like Keycloak, and best practices for refresh tokens and scopes.

Learn Spring Boot - Basic Security & Authentication | Learn Spring Boot