Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 67

Endpoint Security Rules

Recommended resources

In our last session, you saw how effortlessly Spring Security locks down an entire application by default. While powerful, this "all or nothing" approach is rarely what we need. Real-world applications require more nuanced control: some pages must be public, others accessible only to logged-in users, and some restricted to specific roles like administrators.

This lesson is about taking that control. We will move beyond the default settings and learn how to configure your own security rules to protect specific endpoints. You'll define a custom SecurityFilterChain, the central component for tailoring Spring Security to your application's precise needs. By the end of this lesson, you will be able to make certain APIs public, protect others, and implement role-based access control.

Overriding the Default: Your First SecurityFilterChain

To move beyond the default behavior, you need to tell Spring Security exactly how you want it to behave. You do this by creating a special configuration class and defining a Java Bean of type SecurityFilterChain. Once Spring detects your custom SecurityFilterChain bean, it steps back and deactivates its own default auto-configuration. You are now in command.

A security configuration class is a standard Java class annotated with @Configuration to mark it as a source of bean definitions and @EnableWebSecurity to enable Spring Security's web security support.

Inside this class, you'll define a method annotated with @Bean that returns a SecurityFilterChain. This method takes an HttpSecurity object as a parameter, which acts as a builder for constructing your security rules. We will use the modern "Lambda DSL" (Domain-Specific Language) to configure it, which provides a clean and readable way to define complex rules.

The unlogged.io article gives a great overview of why we need to move beyond the default setup and what a custom configuration looks like.

Securing Spring Boot 3 Applications with ...

This article highlights the limitations of the default setup and introduces the core components for building a custom security configuration.

First, read the section outlining the limitations of the default configuration. This reinforces why today's lesson is so important. Next, skim through the introduction to custom security configuration. Don't worry about the UserDetails or UserService parts yet; we will cover those in the next lesson. Finally, jump to the code block for the SecurityConfig.java class and focus on the securityFilterChain bean. It shows the basic structure we are about to build: httpSecurity.csrf(...).authorizeHttpRequests(...).sessionManagement(...).httpBasic(...) We will focus primarily on the authorizeHttpRequests part today.


Authorizing Requests: requestMatchers, permitAll, and authenticated

The core of our configuration happens inside the authorizeHttpRequests() method. This is where we define which users can access which endpoints. The general pattern is: "For a request that matches this pattern, apply this rule."

The key methods we'll use are:

  • requestMatchers(String... antPatterns): This method selects endpoints based on URL patterns. It uses Ant-style path matching, where * matches one segment and ** matches zero or more segments. For example, /api/users/* matches /api/users/1 but not /api/users/1/posts. /api/users/** would match both.
  • permitAll(): This rule allows any user (authenticated or not) to access the matched endpoint. It's perfect for home pages, login pages, or public registration endpoints.
  • authenticated(): This rule requires the user to be authenticated to access the matched endpoint. It doesn't care about roles, only that the user has successfully logged in.
  • anyRequest(): This is a powerful catch-all that matches any request that hasn't been matched by a preceding requestMatchers() rule. It's crucial for establishing a default security posture.

The Baeldung article below provides a crystal-clear, code-driven example of putting these pieces together.

Spring Security – Configuring Different URLs

This article from Baeldung is an excellent practical guide to configuring security for different URLs.

Please read section 4, "Set Up the Security Configuration". Focus on the structure of the securityFilterChain bean. You'll see how authorizeHttpRequests is used to build a chain of rules. Notice the logic: Match /products/** and permit all access. Match /customers/** and apply a role-based rule (we'll get to this shortly). For anyRequest not yet matched, require authentication. This ordering is important: Spring Security evaluates matchers in the order they are declared. Therefore, you should always place more specific rules before more general ones, with anyRequest() coming last.


Implementing Role-Based Access Control (RBAC)

Requiring authentication is a good start, but most applications need finer-grained control. For instance, any authenticated user might be able to view their own profile, but only an administrator should be able to delete a user account. This is role-based access control.

Your experience with JWTs in your MERN project has already introduced you to the concept of including role information in a token. Spring Security provides a seamless way to enforce rules based on this information using methods like:

  • hasRole("ADMIN"): The user must have the specified role.
  • hasAnyRole("ADMIN", "MANAGER"): The user must have at least one of the specified roles.

A crucial convention to remember is that Spring Security automatically prefixes role names with ROLE_ when performing the check. So, if you use hasRole("ADMIN"), it will check if the authenticated user has an authority named ROLE_ADMIN.

The following video demonstrates how to layer role-based rules into your SecurityFilterChain.

Spring boot 3 & Spring security 6 - Roles and Permissions Based Authorization Explained!

This detailed video from Ali Bouali covers configuring security based on both roles and permissions. We will concentrate on the role-based configuration part.

Watch the segment from here. The instructor adds rules to the SecurityFilterChain bean. Notice how he first secures the /api/v1/management/** path using hasAnyRole(ADMIN.name(), MANAGER.name()). This is exactly the pattern for restricting an entire section of your API to certain roles.


Hands-On: Customizing Security for group-manager

Let's apply these concepts to your project. We will create a SecurityConfig class, define some in-memory users with different roles for testing, and then create a set of rules to protect your user-related endpoints.

1. Create the SecurityConfig Class

In your project, create a new package named config. Inside this package, create a new Java class called SecurityConfig.

package com.example.groupmanager.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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // We will add our beans here
}

2. Define In-Memory Users for Testing

Before defining rules, we need users with roles to test against. In the next lesson, we'll load users from the database. For now, we can create a simple UserDetailsService bean that provides hardcoded, in-memory users. This is a common practice for development and testing.

Add the following bean to your SecurityConfig class. We are creating a "user" with the role USER and an "admin" with the role ADMIN. Note that we are using a no-op password encoder for simplicity; in a real application, you would use a strong hashing algorithm like BCrypt.

// Add this method inside the SecurityConfig class

@Bean
public UserDetailsService userDetailsService() {
    UserDetails user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build();
    UserDetails admin = User.withDefaultPasswordEncoder()
            .username("admin")
            .password("password")
            .roles("ADMIN")
            .build();
    return new InMemoryUserDetailsManager(user, admin);
}

Note: withDefaultPasswordEncoder() is deprecated and only suitable for samples. We will replace this with a proper PasswordEncoder bean in a future lesson.

3. Define the SecurityFilterChain Bean

Now for the main event. Add the SecurityFilterChain bean to your SecurityConfig. We will configure the following rules for our /users API:

  • GET /users: Allow anyone to see the list of users.
  • GET /users/{id}: Allow any authenticated user (both USER and ADMIN) to view a single user.
  • POST /users: Only allow an ADMIN to create a new user.
  • PUT /users/{id}: Only allow an ADMIN to update a user.
  • DELETE /users/{id}: Only allow an ADMIN to delete a user.
// Add this method inside the SecurityConfig class

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authorize -> authorize
            .requestMatchers("/users").permitAll() // Allow public access to GET /users
            .requestMatchers("/users/*").authenticated() // Require authentication for GET /users/{id}
            .requestMatchers("/users").hasRole("ADMIN") // Require ADMIN role for POST, PUT, DELETE
            .anyRequest().authenticated()
        )
        .httpBasic(); // Enable HTTP Basic authentication

    return http.build();
}

Wait, there's a problem here! How do we distinguish between GET /users and POST /users? The code above is too simplistic.

You're right to be skeptical. The requestMatchers we wrote doesn't account for the HTTP method. A better approach is to specify the method directly. Let's refine the rules.

import org.springframework.http.HttpMethod; // Add this import at the top of your file

// Replace the previous securityFilterChain bean with this one

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf.disable()) // Disabling CSRF for stateless REST APIs
        .authorizeHttpRequests(authorize -> authorize
            .requestMatchers(HttpMethod.GET, "/users", "/users/**").permitAll()
            .requestMatchers(HttpMethod.POST, "/users").hasRole("ADMIN")
            .requestMatchers(HttpMethod.PUT, "/users/**").hasRole("ADMIN")
            .requestMatchers(HttpMethod.DELETE, "/users/**").hasRole("ADMIN")
            .anyRequest().authenticated()
        )
        .httpBasic(); // Use HTTP Basic for authentication with tools like Postman

    return http.build();
}

This version is much more precise. We've also disabled CSRF (Cross-Site Request Forgery) protection, which is a common and safe practice for stateless REST APIs that are not called from browser forms.

4. Test Your New Rules

Restart your application. The login form will disappear because we are now using httpBasic. You will need to use a tool like Postman to test the endpoints. For each request, go to the "Authorization" tab, select "Basic Auth", and enter the username and password.

  • Test Case 1: Unauthenticated
    • GET http://localhost:8080/users -> Should return 200 OK with the user list.
    • POST http://localhost:8080/users -> Should return 401 Unauthorized.
  • Test Case 2: Authenticated as user/password
    • GET http://localhost:8080/users/1 -> Should return 200 OK with user details.
    • POST http://localhost:8080/users -> Should return 403 Forbidden (user does not have the ADMIN role).
  • Test Case 3: Authenticated as admin/password
    • POST http://localhost:8080/users -> Should return 201 Created (or whatever your controller returns).
    • DELETE http://localhost:8080/users/1 -> Should return 200 OK or 204 No Content.

How It Works: A Look Under the Hood

You've successfully configured the rules, but what is happening when a request is processed?

Spring Security uses a chain of filters to process incoming requests. When you define a SecurityFilterChain bean, you are building your own custom chain. A key component in this process is the DelegatingFilterProxy, which is registered with the servlet container. It intercepts every request and delegates it to the appropriate SecurityFilterChain that matches the request's URL.

This diagram illustrates how Spring Security can manage multiple filter chains. The `DelegatingFilterProxy` acts as a router, sending a request to the `SecurityFilterChain` whose pattern (`/api/**` or `/**`) matches the request URL. In our case, we have one chain that handles all requests.

Once the request enters our filter chain, it eventually reaches the AuthorizationFilter. This filter is responsible for enforcing the rules you just wrote.

This sequence diagram shows the authorization process. The `AuthorizationFilter` retrieves the current user's `Authentication` object from the `SecurityContext`. It then passes this, along with the request, to an `AuthorizationManager`. This manager checks your configured rules (`hasRole('ADMIN')`, `permitAll()`, etc.) and returns an `AuthorizationDecision`. If the decision is `true`, the filter chain continues; otherwise, an "Access Denied" response is sent.

Conclusion

In this lesson, you graduated from using Spring Security's default configuration to defining your own precise, endpoint-specific security rules. You have taken a massive step toward building a truly secure and professional-grade backend application.

Key Takeaways:

  • You can take full control of web security by defining a @Bean of type SecurityFilterChain in a class annotated with @Configuration and @EnableWebSecurity.
  • The modern Lambda DSL provides a fluent API via the HttpSecurity object to configure rules.
  • You can specify rules for URL patterns using requestMatchers() and combine them with HTTP methods for greater precision.
  • Access control is managed with methods like permitAll(), authenticated(), hasRole(), and hasAnyRole().
  • Rule order matters: specific matchers must come before general ones, with anyRequest() as the final fallback.

We used in-memory users today for convenience. While great for testing, a real application needs to authenticate against a persistent user store. In our next lesson, you will learn how to bridge Spring Security with Spring Data JPA by implementing a custom UserDetailsService to load user data directly from your MySQL database.