Spring Security Default Security Filter Chain
From parent article
Setting Up Spring Security
To get started with Spring Security, add the following dependency to pom.xml
<dependencies>
...
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
...
</dependencies>
After adding the dependency, run the following command to start your application:
./mvnw spring-boot:run
Once the application starts, Spring will secure the application with the default security filter chain .
When your application start up it will log your Default Security Filter Chain along with the generated password.
By default, when you access any endpoint, you will be presented with a login page.
The default username is user, and the password is generated and logged during application startup.
To understand how the default security setup works, let’s take a look at the default security filters in the chain.
Default Security Filter Chain
Security Filters
The FilterChainProxy stores the SecurityFilterChain in a list called additionalFilters, a counter named currentPosition is used to manage where along the chain the request is being processed. In the image above, the currentPosition is 3 so the request is being processed in the SecurityContextHolderFilter.
We will focus on
[Optional] You can read more about the other security filters here:
Username Password Authentication Flow
Retrieving Security Context
When an incoming request enters the Security Filter Chain and reaches the SecurityContextHolderFilter it will call the SecurityContextRepository to retrieve any existing SecurityContext associated with the Cookie JSESSIONID.
JSESSIONID is a cookie used to track user sessions. When a user logs in, their authenticated state is represented by an Authentication object, which is encapsulated in a SecurityContext stored in memory by the SecurityContextRepository. For subsequent requests in the same session, this Authentication object is retrieved sparing the user the extra work of re-authenticating.
Since the user hasn’t been authenticated in this session, an empty Security Context is created stored in in the SecurityContextHolder.
The SecurityContextHolder is a thread local store for the SecurityContext, meaning that the SecurityContext and its Authentication object is global to the current request meaning that it can be access anywhere during the request lifecycle and is isolated from other requests.
Getting Redirected to Login Page
When the request reaches the ExceptionTranslationFilter, which redirects unauthenticated requests to the login page.
The login page is a JSP page generated by the DefaultLoginPageGenerating filter.
A JSP page is a server generated page that dynamically creates HTML content to display in a web browser.
Authentication with the UsernamePasswordAuthenticationFilter
When you sign into the login page, it will make a POST request to the /login endpoint with the user’s username and password.
The POST request to the /login endpoint will be intercepted by the UsernamePasswordAuthenticationFilter.
The filter will extract the username and password from the request and created an unauthenticated UsernamePasswordAuthenticationToken(implementation of Authentication) and pass the token to the authenticate method of the AuthenticationManager.
Authentication Architecture
Unimplemented
领英推荐
When an Authentication object is processed by the AuthenticationManager's authenticate method, it delegates the authentication to an appropriate AuthenticationProvider. The AuthenticationProvider extracts the username and password from the Authentication object, retrieves the corresponding UserDetails (which includes the stored password), and encodes the provided password using a PasswordEncoder. The authentication is successful if the encoded password matches the stored password in the UserDetails.
The default configuration for Spring Security, the AuthenticationManager is implemented as ProviderManager, the PasswordEncoder is implemented as DelegatingPasswordEncoder and the UserDetailsService is implemented as InMemoryUserDetailsManager.
The ProviderManager will decide which which implementation of AuthenticationProvider to delegate authentication to by looping over all available providers. It selects the provider that supports the UsernamePasswordAuthenticationToken which is the implementation of the Authentication object that is passed from the UsernamePasswordAuthentication filter.
Since the abstract class AbstractDetailsAuthenticationProvider supports UsernamePasswordAuthenticationToken the ProviderManager will delegate authentication to the DaoAuthenticationProvider.
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware {
....
@Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
...
}
We will authenticate the Authentication token encoding the password and compared it with the encoded password in the UserDetails object retrieved from the datastore.
When the request has been authenticated an authenticated Authentication object will be created using the username and authorities from the UserDetails object, for safety consideration we will not store the password.
Once the SecurityContextHolder contains the authenticated Authentication with the proper authorities it has access to the relevant controller.
Subsequent Requests of Authenticated Session
When the response being returned to the client reaches the SecurityContextPersistenceFilter, the SecurityContext is stored by the SecurityContextRepository and then cleared from the SecurityContextHolder. This is important because:
When a subsequent request is made within the same session, the SecurityContextHolderFilter retrieves the stored authenticated SecurityContext from the SecurityContextRepository using the JSESSIONID.
Once the authenticated Authentication object is loaded into the SecurityContextHolder, the request is granted access to the relevant controller.
Basic Authentication Flow
Consider an incoming request with the following authorization header.
Authorization: Basic <encoded token>
The <encoded token> is the Base64 encoded string of <username>:<password>
Similar to the Username Password Authentication flow, an incoming request reaches the SecurityContextHolderFilter which retrieves the SecurityContext of the current session from SecurityContextRepository, since the user has not previously been authenticated an empty SecurityContext is stored in the SecurityContextHolder.
Since the request contains the Authorization header with the Basic scheme, it will trigger the BasicAuthenticationFilter.
The filter will extract the username and password from the encoded token, create an unauthenticated Authentication token, and delegate it to the ProviderManager to perform authentication, similar to the process used in the Username Password Authentication flow.
Once the request has been authenticated it will have access to the controller.
Similar to the Username Password Authentication when the response reach the SecurityContextHolderFilter, the SecurityContextRepository will store the SecurityContext for subsequent request in the same session and clear the SecurityContext in the SecurityContextHolder.
A subsequent request will have access to the authenticated SecurityContext, eliminating the need for re-authentication.
Drawback to the Username Password and Basic Authentication flow
We will no longer use the default security filter chain because it is tightly coupled with the Spring MVC framework, which assumes a server-generated frontend closely integrated with backend REST APIs. This setup relies on server-managed sessions (JSESSIONID) to maintain authentication state, making it unsuitable for hosting REST APIs in a modern web application where the backend is decoupled from the frontend, particularly when using frameworks like React or Vue.
Another significant drawback of the default security filter chain emerges when scaling the application across multiple servers. Since JSESSIONID is stored in memory on a single server, a user who logs in on one server might be routed to another server by a load balancer. If the session is managed in-memory, the new server won’t recognize the user's session, causing authentication failures. This issue highlights the limitations of relying on server-managed sessions in a distributed environment, underscoring the need for a more stateless, scalable approach to authentication.