Spring Security Architecture Explained

Photo by Paulo Evangelista on Unsplash
Photo by Paulo Evangelista on Unsplash
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.

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.

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.

SpringSecurity And Servlet
SpringSecurity And Servlet

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);
}

DelegatingFilterProxy is where entering Spring from Servlet, while FilterChainProxy is where entering Spring Security from Spring, so to speak.

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.

ExceptionTranslationFilter
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 authenticationEtryPoint.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.

SecurityContextHolder
SecurityContextHolder

Except SecurityContextHolder is a class, everything else is an interface. The following figure shows their relationship in UML.

SecurityContext
SecurityContext

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.

AuthenticationManager
AuthenticationManager

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);

Spring Security Authorization Flow

After passing authentication flow, the next step is authorization flow. FilterSecurityInterceptor is used to deal with it.

FilterSecurityInterceptor

The of FilterSecurityInterceptor is as follows:

FilterSecurityInterceptor
FilterSecurityInterceptor

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like