Spring Security is an authentication framework officially recommended by Spring. Its power is well known, but its complexity is notorious. Spring Security abstracts almost all components, so that it is impossible to understand intuitively. This article will discuss the architecture of Spring Security in a simple way. For other complete details, please refer to Spring Security’s official documents.
Table of Contents
Spring Security and Servlet
Before understanding the architecture of Spring Security, let us take a look at how Spring Security works with Servlet. The following figure shows the flow of Servlet and Spring Security when an HTTP request comes in.
On the left side of the figure, a client application sends an HTTP request, enters the FilterChain, and finally reaches Servlet. In Spring, this servlet will be DispatchServlet .
Each filter in FilterChain will call next filter by Filter.doFilter()
, as shown in the following code:
public class FilterX implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) { // Do something before filterChain.doFilter(request, response, filterChain); // Do something after } }
DelegatingFilterProxy
Spring provides a filter called DelegatingFilterProxy. It is a bridge between Servlet and Spring’s ApplicationContext. Spring Security places DelegatingFilterProxy within Servlet’s FilterChain, obtains FilterChainProxy bean from ApplicationContext in DelegatingFilterProxy.doFilter(), and calls FilterChainProxy.doFilter()
, as shown below.
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) { Filter delegate = getFilterBean("FilterChainProxy"); delegate.doFilter(request, response, filterChain); }
FilterChainProxy & SecurityFilterChain
FilterChainProxy is provided by Spring Security. It contains a List<SecurityFilterChain>, and these SecurityFilterChain are all beans. When an HTTP request enters FilterChainProxy, it calls SecurityFilterChain.matches(request)
for each SecurityFilterChain. Then, it calls SecurityFilterChain.getFilters()
to the one that returns true in order to get the List<SecurityFilterChain> within it.
But, it obtains a List<Filter> but not a FilterChain, so use VirtualFilterChain to make List<Filter> like FilterChain. The last is the call VirtualFilterChain.doFilter()
.
The the process is roughly as follows:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) { List<Filter> filters = this.getFilters(); if (filters == null) { filterChain.doFilter(request, response); return; } VirtualFilterChain vfc = new VirtualFilterChain(request, filterChain, filters); vfc.doFilter(request, response); } private List<Filter> getFilters(HttpServletRequest request) { for (SecurityFilterChain chain : this.filterChains) { if (chain.matches(request)) { return chain.getFilters(); } } return null; }
The FilterChainProxy contains a List<SecurityFilterChain> instead of a single SecurityFilterChain, mainly to allow it to select a specific SecurityFilterChain according to HTTP requests.
Security Filters
SecurityFilterChain.getFilters()
returns List<Filter>. These filters are all provided by Spring Security, so we call these filters Security Filters.
Spring Security has a predefined set of SecurityFilterChain. We will introduce the very last two Security Filters in this preset SecurityChain – ExceptionTranslationFilter and FilterSecurityInterceptor.
Spring Security Authentication Flow
ExceptionTranslationFilter
The following figure shows the logic of ExceptionTranslationFilter.
This figure is roughly equivalent to the following code:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) { try { filterChain.doFilter(request, response); } catch (AccessDeniedException | AuthenticationException e) { if (!isAuthenticated || e instanceof AuthenticationException) { startAuthentication(request, response, filterChain, e); } else { accessDeniedHandler.handle(request, response, e); } } } protected void startAuthentication( HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) { SecurityContextHolder.getContext().setAuthentication(null); requestCache.saveRequest(request, response); authenticationEntryPoint.commence(request, response, reason); }
Simply put, ExceptionTranslationFilter directly calls its next Security Filter that is FilterSecurityInterceptor and catch exceptions. If no exception is thrown, ExceptionTranslationFilter is equivalent to doing nothing.
If an exception is thrown out, and it is an AuthenticationException, the authentication process will start. So it will first clear authentications in SecurityContext, and call authenticationE
n tryPoint.commence()
to request credentials from clients.
If it is an AccessDeniedException, it calls accessDeniedHandler.handle()
.
Finally, it will return directly, and the HTTP request will not go to DispatchServlet.
AuthenticationEntryPoint
AuthenticationEntryPoint is called at the end of startAuthentication()
. So it is designed to request credentials from clients. Let’s take a look at a few examples.
BasicAuthenticationEntryPoint
BasicAuthenticationEntryPoint is used in the case of Basic Authentication . It writes WWW-Authenticate in response so that clients can do authentication in the next HTTP request.
public class BasicAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean { public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) { response.addHeader("WWW-Authenticate", "Basic realm=\"" + realmName + "\""); response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); } }
LoginUrlAuthenticationEntryPoint
LoginUrlAuthenticationEntryPoint is used in the case of Login Form. It redirects to login page and let clients enter an username and password to login.
public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean { public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) { response.sendRedirect(loginUrl); } }
Http403ForbiddenEntryPoint
Http403ForbiddenEntryPoint is used when you do not want to request credentials from clients, but only want to directly return an error. If you are using Bearer token, this may be what you want. And if your API is RESTful, you will want to return JSON strings.
public class Http403ForbiddenEntryPoint implements AuthenticationEntryPoint { public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied"); } }
AccessDeniedHandler
AccessDeniedHandler is used to handle situations when HTTP request is rejected, such as being rejected due to insufficient permissions.
AccessDeniedHandlerImpl will redirect to the 403 page or write to response directly. If your API is RESTful, it will return JSON strings.
public class AccessDeniedHandlerImpl implements AccessDeniedHandler { public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) { if (errorPage != null) { request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException); response.setStatus(HttpStatus.FORBIDDEN.value()); RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage); dispatcher.forward(request, response); } else { response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase()); } } }
SecurityContextHolder
SecurityContextHolder is used to store authenticated Authentication objects. Its structure is roughly as shown in the figure below.
Except SecurityContextHolder is a class, everything else is an interface. The following figure shows their relationship in UML.
Before we discuss each interface, let’s first understand how to use SecurityContextHolder. The usage is very straightforward, that is, directly set an authenticated Authentication to SecurityContextHolder. It is declared as static, so after setting, the entire application can get the authenticated Authentication through SecurityContextHolder.getContext().getAuthentication(). If it returns null, it means that it has not been verified.
Authentication authentication = new UsernamePasswordAuthenticationToken(request.username, request.password); Authentication authenticated = authenticationManagerBuilder.object.authenticate(authentication); SecurityContextHolder.getContext().setAuthentication(authenticated);
SecurityContextHolderStrategy
SecurityContextHolderStrategy is used to determine how to store SecurityContext.
For example, GlobalSecurityContextHolderStrategy allows the entire application to share a SecurityContext.
class GlobalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { private static SecurityContext contextHolder; }
The ThreadLocalSecurityContextHolderStrategy allows each thread to have its own copy of SecurityContext. And this is also the default SecurityContextHolderStrategy by Spring Security. So in Spring Boot, every HTTP request is processed by a thread. That is to say, every HTTP request has its own SecurityContext.
class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>(); }
SecurityContext
SecurityContext mainly contains an authenticated Authentication object.
Authentication
Authentication mainly includes Principal, Credentials, and Authorities. Principal and Credentials could have different definitions because of different kind of authentications, so they will be declared as Object.
For example, Principal is often an username, and Credentials is a password. If tokens are used, then Credentials may be a token.
GrantedAuthority
GrantedAuthority is used in authorization phase. Permissions are defined by strings, and all strings have a prefix of ROLE_.
For example, the code below sets GET /greet to have ROLE_ADMIN permission. When calling .hasRole(), ROLE_ should be removed from the string inside.
@Configuration @EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) { http .authorizeRequests() .antMatchers("/login").permitAll() .antMatchers("/greet").hasRole("ADMIN") .anyRequest().authenticated() } }
Therefore, when an Authentication is generated, a ROLE_ADMIN permission must be given so that this HTTP request can access GET /greet. As shown in the code below:
List<GrantedAuthority> authorities = new ArrayList(); authorities.add(SimpleGrantedAuthority("ROLE_ADMIN")); Authentication authentication = new UsernamePasswordAuthenticationToken(request.username, request.password, authorities); Authentication authenticated = authenticationManagerBuilder.object.authenticate(authentication); SecurityContextHolder.getContext().setAuthentication(authenticated);
AuthenticationManager
AuthenticationManager is used to verify Authentication objects. An authenticated Authentication will be set to SecurityContextHolder. ProviderManager is the most commonly used AuthenticationManager implementation. It has a List<AuthenticationProvider>, and each AuthenticationProvider has an opportunity to verify the authentication passed in. If there is no AuthenticationProvider that can verify this Authentication, it will use its parent to verify. If this Authentication cannot be verified, the ProviderManager will throw an AuthenticationException.
DaoAuthenticationProvider is an implementation of AuthenticationProvider. It is used to verify Username/Password mode.
In addition, if you want to get default AuthenticationManager instance in application, you must use AuthenticationManagerBuilder because AuthenticationManagerBuilder is a Bean. The following code shows how to use AuthenticationManagerBuilder.
@Autowire private AuthenticationManagerBuilder authenticationManagerBuilder; Authentication authentication = new UsernamePasswordAuthenticationToken(username, password); Authentication authenticated = authenticationManagerBuilder.getObject().authenticate(authentication); SecurityContextHolder.getContext().setAuthentication(authenticated);
After passing authentication flow, the next step is authorization flow. FilterSecurityInterceptor is used to deal with it.
FilterSecurityInterceptor
The of FilterSecurityInterceptor is as follows:
The logic of the above figure is roughly as the following code:
FilterInvocation fi = new FilterInvocation(request, response, filterChain); Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(fi); Authentication authenticated = SecurityContextHolder.getContext().getAuthentication(); try { accessDecisionManager.decide(authenticated, fi, attributes); } catch (AccessDeniedException e) { throw e; }
FilterSecurityInterceptor obtains Collection<ConfigAttribute> according to an incoming HTTP request. Then, pass Collection<ConfigAttribute> and an authenticated Authentication to AccessDecisionManager to determine whether to authorize this Authentication to access resources.
For example, in the following code, set GET /greet to require ROLE_ADMIN permission.
@Configuration @EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) { http .authorizeRequests() .antMatchers("/login").permitAll() .antMatchers("/greet").hasRole("ADMIN") .anyRequest().authenticated() } }
However, the authorities in Authentication obtained by FilterSecurityInterceptor only contains ROLE_USER.
List<GrantedAuthority> authorities = new ArrayList(); authorities.add(SimpleGrantedAuthority("ROLE_USER")); Authentication authentication = new UsernamePasswordAuthenticationToken(request.username, request.password, authorities); Authentication authenticated =
In this way, according to GET /greet resource, the obtained Collection<ConfigAttribute> will have a ConfigAttribute in which is hasRole(‘ROLE_ADMIN’). Then, accessDecisionManager.decide() will find that GET /greet resource requires hasRole(‘ROLE_ADMIN’) permission, but the authorities in the obtained Authentication only contains ROLE_USER permission, so AccessDeniedException will be thrown.
Conclusion
Spring Security provides a highly scalable architecture. You can customize a set of verification process according requirements. However, because of excessive abstraction, the complexity has greatly increased. Hope that after reading this chapter, you can have a some understanding of Spring Security.