Skip to main content

Welcome to a Grasp lesson

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

Lesson 27

Extracting User ID from Security Context

Recommended resources

Hello! Welcome back to our rate limiter project.

In our last lesson, you successfully implemented an ApiKeyResolver to identify clients using a custom HTTP header. This was a fantastic step into building our multi-tenant identification system. We saw how the RateLimitKeyResolver interface, acting as a Strategy pattern, allows us to cleanly swap out different identification methods.

Today, we'll build on that foundation by adding another crucial strategy: identifying users who have logged into our application.

Your learning outcome for this lesson:

You will implement a key resolver to extract a User ID from the security context. This will enable the rate limiter to apply rules specifically to authenticated users, a common requirement for distinguishing between anonymous traffic and known users.



1. Understanding the Spring Security Context

Before we write code, we need to understand where the user's identity comes from. In a Spring Boot application, authentication and authorization are typically handled by the Spring Security framework.

When a user successfully authenticates (for example, by logging in with a username and password, or by providing a valid JWT), Spring Security creates an Authentication object. This object holds the user's "principal" (their identity, usually the username), their credentials, and their authorities (roles or permissions).

This Authentication object is then stored in a SecurityContext, which is managed by a SecurityContextHolder. The SecurityContextHolder makes the user's security information available throughout the processing of their request.

This diagram illustrates the flow: an HTTP request comes in, is processed by Spring Security's filters, and if authentication is successful, the Authentication object is stored and made accessible via the SecurityContextHolder.

Spring Security Authentication Flow Diagram
This diagram shows how an incoming `HttpRequest` is processed by Spring Security. An `AuthenticationManager` validates the user's credentials, and upon success, an `Authentication` object containing the user's identity (Principal) is placed into the `SecurityContext`, which is held by the `SecurityContextHolder` for application-wide access.

Our goal is to tap into this SecurityContextHolder to retrieve the authenticated user's name.


2. Programmatically Accessing the Authenticated User

The most direct way to get the current user's information, especially within components like our HandlerInterceptor, is by interacting with the SecurityContextHolder directly.

The following Baeldung article is a definitive guide on this topic. Please read the suggested section to understand the fundamental mechanism we'll be using.

Retrieve User Information in Spring Security

To implement our UserIdResolver, we first need to understand how to access the currently authenticated user's information. The Baeldung article 'Retrieve User Information in Spring Security' provides an excellent overview of the different ways this can be done.

Please read Section 2, 'Get the User in a Bean'. Focus on the code snippets that use SecurityContextHolder.getContext().getAuthentication(). This static method is the most common way to access the authenticated user's details outside of a controller, which is exactly our scenario inside the HandlerInterceptor.

As the article shows, the key is the static call SecurityContextHolder.getContext().getAuthentication(). Once we have the Authentication object, we can simply call .getName() to retrieve the username.


3. Implementing the UserIdResolver

Now, let's apply this knowledge to create our UserIdResolver. It will implement our RateLimitKeyResolver interface, just like the ApiKeyResolver did.

Create a new Java class named UserIdResolver:

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

@Component("userIdResolver") // Naming the bean for easy injection
public class UserIdResolver implements RateLimitKeyResolver {

    @Override
    public String resolve(HttpServletRequest request) {
        // Retrieve the Authentication object from the security context
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        // Check if there is an authenticated user who is not an anonymous user
        if (authentication != null &&
            authentication.isAuthenticated() &&
            !(authentication instanceof AnonymousAuthenticationToken)) {
            
            // The getName() method typically returns the username
            return authentication.getName();
        }

        // If no authenticated user is found, return null
        return null;
    }
}

Key Points about this implementation:

  • @Component("userIdResolver"): We register this class as a Spring bean with a specific name, so we can inject it where needed using @Qualifier("userIdResolver").
  • SecurityContextHolder.getContext(): This is the static method we use to access the current SecurityContext.
  • !(authentication instanceof AnonymousAuthenticationToken): This is a crucial check. Spring Security creates an AnonymousAuthenticationToken for unauthenticated requests. We must explicitly ignore these to ensure we only generate keys for genuinely logged-in users.
  • authentication.getName(): This method on the Authentication principal returns a String representing the user, which is perfect for our rate-limiting key.
  • return null: Just like with our ApiKeyResolver, we return null if a key cannot be resolved. This allows the RateLimitingInterceptor to decide on a course of action, such as falling back to another resolver or applying a default policy.

4. A Standard Industry Pattern

Using the authenticated principal as a key for rate limiting is a very common and secure pattern. It ensures that limits are applied on a per-user basis. The article below, though focused on a reactive Spring stack, perfectly validates our approach.

Secure Rate Limiting with Spring Cloud Gateway

The following article demonstrates secure rate limiting in Spring Cloud Gateway. While it uses a reactive framework (WebFlux), the core concept of creating a KeyResolver that pulls the user's identity from the security context is identical to our implementation.

Read Section 3, 'Configure Spring Cloud Gateway Rate Limiter key'. The code creates a KeyResolver bean. Observe the lambda expression: ctx.getAuthentication().getPrincipal().toString(). This is the reactive equivalent of our authentication.getName(). This confirms that our UserIdResolver aligns with standard industry practices for secure, user-based rate limiting.


5. Integrating and Testing the Resolver

To use this new resolver, you would update the RateLimitingInterceptor's constructor to inject it instead of the ApiKeyResolver:

// In RateLimitingInterceptor.java

public RateLimitingInterceptor(RateLimiter rateLimiter, 
                               @Qualifier("userIdResolver") RateLimitKeyResolver keyResolver) { // Swapped the qualifier
    this.rateLimiter = rateLimiter;
    this.keyResolver = keyResolver;
}

Since your project may not have a full Spring Security setup yet, how can you test the UserIdResolver in isolation? You can write a unit test that manually sets up a SecurityContext.

Here is a snippet showing how you might do this with JUnit 5:

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

class UserIdResolverTest {

    private final UserIdResolver resolver = new UserIdResolver();
    private final MockHttpServletRequest request = new MockHttpServletRequest();

    @AfterEach
    void tearDown() {
        // Clear the context after each test to ensure test isolation
        SecurityContextHolder.clearContext();
    }

    @Test
    void resolve_returnsUsername_whenUserIsAuthenticated() {
        // Arrange: Create a mock authentication object
        Authentication authentication = new UsernamePasswordAuthenticationToken("test-user", "password");
        SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
        securityContext.setAuthentication(authentication);
        SecurityContextHolder.setContext(securityContext);

        // Act: Call the resolver
        String key = resolver.resolve(request);

        // Assert: Verify the key is the username
        assertEquals("test-user", key);
    }

    @Test
    void resolve_returnsNull_whenNoUserIsAuthenticated() {
        // Arrange: Ensure the context is empty
        SecurityContextHolder.clearContext();

        // Act
        String key = resolver.resolve(request);

        // Assert
        assertNull(key);
    }
}

This test demonstrates how to control the SecurityContextHolder to verify your resolver's logic without needing a running application with a full security stack.


Key Takeaways

  • You've implemented a UserIdResolver that extracts an authenticated user's name from the SecurityContext.
  • You learned to use SecurityContextHolder.getContext().getAuthentication() to access the current user's identity.
  • You understand the importance of checking for AnonymousAuthenticationToken to differentiate between anonymous traffic and logged-in users.
  • This new resolver integrates seamlessly into our existing RateLimitKeyResolver strategy, making our system more flexible.
  • You've seen how to unit test components that depend on the SecurityContextHolder.

Next Steps

Great job! You've now created key resolvers for two of the most important identification strategies: API keys and User IDs. In the next lesson, we will add another common strategy: implementing a key resolver to identify a request by its endpoint path. This will allow us to apply different rate limits to different parts of our API (e.g., limit /users more strictly than /products).