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.
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 currentSecurityContext.!(authentication instanceof AnonymousAuthenticationToken): This is a crucial check. Spring Security creates anAnonymousAuthenticationTokenfor unauthenticated requests. We must explicitly ignore these to ensure we only generate keys for genuinely logged-in users.authentication.getName(): This method on theAuthenticationprincipal returns aStringrepresenting the user, which is perfect for our rate-limiting key.return null: Just like with ourApiKeyResolver, we returnnullif a key cannot be resolved. This allows theRateLimitingInterceptorto 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
UserIdResolverthat extracts an authenticated user's name from theSecurityContext. - You learned to use
SecurityContextHolder.getContext().getAuthentication()to access the current user's identity. - You understand the importance of checking for
AnonymousAuthenticationTokento differentiate between anonymous traffic and logged-in users. - This new resolver integrates seamlessly into our existing
RateLimitKeyResolverstrategy, 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).