Using Spring Seucurity to implement login authentication and authorization management is a large part of the project, and a relatively difficult part. This project has improved on the original project by replacing the deprecated FilterSecurityInterceptor authorization API with the new AuthorizationFilter authorization API recommended by the new version, and by taking into account the concurrent security of authorization during coding. In addition, the project has integrated Spring Session to provide cluster session support, improved the authorisation of anonymously accessible interfaces, added the ability to disable roles, and made some code optimisations. This is a summary of the main frameworks used in the project, for more information on the frameworks please see the official Spring Security documentation.

Authentication

Authentication process

The project uses login authentication with a username and password form. This section summarises how the project’s login authentication works in Spring Security. First, we look at how the user is asked to log in.

Spring Security Authentication Process

The above diagram is built on the SecurityFilterChain.

  1. First, a user makes an unauthenticated request to its unauthorized resource (/private).
  2. Spring Security’s FilterSecurityInterceptor indicates that the unauthenticated request has been rejected by throwing an AccessDeniedException; (AuthorizationFilter replaces FilterSecurityInterceptor)
  3. Since the user is not authenticated, ExceptionTranslationFilter starts Start Authentication and redirects the request to AuthenticationEntryPoint, where the user is asked to redirect to the landing page.
  4. The browser requests to go to the login page to which it was redirected.
  5. The front end renders the login page. (This project has separate front and back ends, no LoginController)

When the username and password are submitted, UsernamePasswordAuthenticationFilter will authenticate the username and password. Next, we look at how the user logs in.

SecurityFilterChain

The above diagram is built on the SecurityFilterChain.

  1. When a user submits their username and password, the UsernamePasswordAuthenticationFilter takes the username and password extracted from the HttpServletRequest instance and creates a UsernamePasswordAuthenticationToken, which is an Authentication type.
  2. Next, the UsernamePasswordAuthenticationToken is passed into the AuthenticationManager instance to be authenticated.
  3. Failure if authentication fails.
    1. SecurityContextHolder is emptied.
    2. RememberMeServices.loginFail is called. (This project does not use remember me authentication)
    3. AuthenticationFailureHandler is called, entering the authentication failure handler.
  4. Success if authentication is successful.
    1. SessionAuthenticationStrategy is notified of new logins. (Not used in this project)
    2. Authentication is set on SecurityContextHolder.
    3. RememberMeServices.loginSuccess is called.
    4. ApplicationEventPublisher publishes InteractiveAuthenticationSuccessEvent event. (Not used in this project)
    5. AuthenticationSuccessHandler is called to enter the authentication success handler.

Finally, let’s understand what happens to the AuthenticationManager authentication process. The implementation of AuthenticationManager is ProviderManager, and AuthenticationProvider is the authentication provider delegated to it. The DaoAuthenticationProvider implements the AuthenticationProvider which uses the UserDetailsService and the PasswordEncoder to authenticate a username and password. The diagram below explains how the AuthenticationProvider works.

How AuthenticationProvider works

  1. UsernamePasswordAuthenticationFilter passes UsernamePasswordAuthenticationToken to the AuthenticationManager, which is implemented by the ProviderManager.
  2. The ProviderManager is configured to use an AuthenticationProvider of type DaoAuthenticationProvider.
  3. The DaoAuthenticationProvider looks up UserDetails from the UserDetailsService.
  4. The DaoAuthenticationProvider uses the PasswordEncoder to verify the password on the UserDetails returned in the previous step.
  5. When authentication is successful, the Authentication returned is of type UsernamePasswordAuthenticationToken and there is a principal that is the UserDetails returned by the configured UserDetailsService. Ultimately, the returned UsernamePasswordAuthenticationToken is set on the SecurityContextHolder by the UsernamePasswordAuthenticationFilter.

Session Management

In addition to the original Spring Security session management project, this project integrates Spring Session to provide clustered session support. The implementation of the SessionRegistry interface is a registry for Spring Security sessions, and with the HttpSessionEventPublisher exposed as a Spring bean to publish session lifecycle events, the SessionRegistry interface can be used to fetch user session information through the SessionRegistry interface. The SpringSessionBackedSessionRegistry is Spring Session’s custom implementation of the SessionRegistry interface and can be used to retrieve session information for a cluster from Spring Session.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Bean
@SuppressWarnings({"unchecked", "rawtypes"})
public SessionRegistry sessionRegistry(FindByIndexNameSessionRepository sessionRepository) {
    return new SpringSessionBackedSessionRegistry(sessionRepository);
}

@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
}

The SpringSessionBackedSessionRegistry has a limitation in that it does not support the getAllPrincipals() method, i.e. it cannot retrieve all session principals. However, the backend administration of this project implements the ability to display a list of online users and needs to retrieve all session principals. In the implementation of the getAllSessions() method of the SpringSessionBackedSessionRegistry, I found that it looks for all sessions of a session subject by username, so I only need to save the usernames of the online users to retrieve all the online sessions using the username collection.

1
2
3
4
5
6
7
@Override
public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
    // Find all sessions of a session subject by username
    Collection<S> sessions = this.sessionRepository.findByPrincipalName(name(principal)).values();
    // Omitted...
    return infos;
}

The following diagram explains how to get session information from the SpringSessionBackedSessionRegistry by saving the username.

How to get session information from the SpringSessionBackedSessionRegistry by saving the username

  1. The user logs in successfully and enters the AuthenticationSuccessHandler’s authentication success handler.
  2. The user session is destroyed, causing the SessionEventListener to receive the user session destruction event.
  3. When a user logs in successfully, the user name is saved in Redis; when the user session is destroyed, the user name is removed from Redis if the user has no other online sessions.
  4. The SpringSessionBackedSessionRegistry gets the usernames of all online users from Redis and queries the session information with the usernames.
  5. The session information is presented to the administrator.

Third Party Authentication

This project incorporates Spring Security to implement some third-party authentication features that can reduce user registration costs and improve the user experience. Please see the third party website for more information on how to integrate third party authentication. Only the core code of the Spring Security integration is presented here. AbstractLoginStrategy is an abstract third-party login strategy template, where the program obtains the third-party login information, constructs UsernamePasswordAuthenticationToken, and hands it over to Spring Security’s context SecurityContext to manage, and the user is then registered and logged in.

Authorization

Authorization process

This project has been improved to use the new version of the recommended AuthorizationFilter instead of the FilterSecurityInterceptor to implement authorization. AuthorizationFilter uses the simplified AuthorizationManager API instead of metadata sources, configuration properties, decision managers and voters. This simplifies reuse and customisation. The diagram below explains how authorization is performed by the AuthorizationManager.

How AuthorizationManager performs authorization

The Authentication implementation used in this project is UsernamePasswordAuthenticationToken with a custom AuthorizationManager implementation

  1. First, AuthorizationFilter gets an Authentication from SecurityContextHolder. It wraps it in a Supplier to delay the lookup.
  2. Next, it passes Supplier<Authentication> and HttpServletRequest to the AuthorizationManager.
  3. If the authorization is denied, an AccessDeniedException is thrown. In this case, ExceptionTranslationFilter handles the AccessDeniedException.
  4. If access is allowed, AuthorizationFilter continues with FilterChain, allowing the application to process normally.

Authorization Core

The core of this project’s improved authorization core is a custom BlogAuthorizationManager implementation class that determines whether all requests are allowed or not, the key elements of this class are described in detail here.

As with the original design, the improved project still uses the locally cached authorization base resourceRoleList. The @PostConstruct annotated loadResourceRoleList() method will retrieve the authorization credentials from the database and write them to the cache when the application is started, and if there are no changes, the credentials will remain in memory, while if the credentials change, an external application will call updateAuthorizationCredentials() method to clear the cache until a new request for authorization is made and the cache is empty, then loadResourceRoleList() is called to read the data from the database and update it to the cache. This is also the most common caching strategy.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
public class BlogAuthorizationManagerImpl implements BlogAuthorizationManager {
    // ...
    private static List<ResourceRoleDTO> resourceRoleList;

    @Resource
    private RoleMapper roleMapper;

    private static void clearResourceRoleList() {
        // ...
    }

    @PostConstruct
    private void loadResourceRoleList() {
        resourceRoleList = roleMapper.listResourceRoles();
    }

    @Override
    public void updateAuthorizationCredentials() {
        clearResourceRoleList();
    }   
    // ...
}

authorization is an important feature, and with the use of local caching, it was easy for me to think about thread safety. In a project of the magnitude of a personal blog, the issue of thread safety is basically non-existent, but if such a design is used in a production environment, thread safety must be considered, so I have rewritten the design to consider thread safety in Demo.

In a production environment, what could be the problem if thread safety is not taken into account? If the authorization basis changes and the cache is cleared, there will be a lot of requests for authorization, and each request will query the database, putting a lot of pressure on the database, when in fact it only needs to be queried once. If the request is important and needs to be updated in real time, the other requesting threads will not be aware of the change in authorization basis after the update, which may result in a false authorization.

There are certainly more problems than the two mentioned above, but the vast majority of these problems can be solved just as well as they can by locking. You cannot use normal locks, which only allow one thread to read or write at a time, which would greatly reduce throughput. authorization is usually based on a read-more-write less scenario and is best suited to using read-write locks. This project has been improved to use fair read/write locks and volatile variables to mark the availability of the cache, ensuring a thread-safe authorization process.

The reason for using fair locks is that if the request to be authorised is important and cannot be mis-authorised, non-fair locks may make previous authorization basis update requests later than later authorization requests, or even make authorization basis update requests late for processing, resulting in mis-authorization. The use of fair locks comes at the cost of reduced system throughput, but this side effect is nothing compared to the impact of a mis-authorization.

The volatile variable invalid is used to ensure visibility of the resourceRoleList cache between multiple threads, so that if one thread clears or updates the cache, the other threads will be aware of the change in time, avoiding problems such as mis-authorization or duplicate database queries caused by using expired cache data.

The clearResourceRoleList() method and the getAvailableAuthorities() method are central to thread safety. The former clears the cache with a write lock, while the latter uses double retrieval in conjunction with the volatile variable invalid, and also uses the lock degradation mechanism of ReentrantReadWriteLock. Here the double-ended search minimises the problem of repeated queries to the database, and the lock degradation mechanism ensures that the thread updating the cache can read the cached data. If the lock degradation mechanism is not used, and the thread updating the cache releases the write lock after the update is complete, and then some thread acquires the write lock and clears the cache, the thread that originally updated the cache will not be able to read the cache data, resulting in a mis-authorization.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@Component
public class BlogAuthorizationManagerImpl implements BlogAuthorizationManager {
    // ...
    private static final ReadWriteLock READ_WRITE_LOCK = new ReentrantReadWriteLock(true);
    private static final Lock READ_LOCK = READ_WRITE_LOCK.readLock();
    private static final Lock WRITE_LOCK = READ_WRITE_LOCK.writeLock();

    private static volatile boolean invalid;

    private static List<ResourceRoleDTO> resourceRoleList;
    // ...
    private static void clearResourceRoleList() {
        WRITE_LOCK.lock();
        try {
            resourceRoleList = null;
            invalid = true;
        } finally {
            WRITE_LOCK.unlock();
        }
    }

    @PostConstruct
    private void loadResourceRoleList() {
        // ...
    }

    @Override
    public void updateAuthorizationCredentials() {
        clearResourceRoleList();
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication,
                                       RequestAuthorizationContext requestContext) {
        // ...
            // Get a list of available permissions
            Set<SimpleGrantedAuthority> availableSet
                    = getAvailableAuthorities(requestContext);
        // ...
    }

    private Set<SimpleGrantedAuthority> getAvailableAuthorities
            (RequestAuthorizationContext requestContext) {
        READ_LOCK.lock();
        if (invalid) {
            // Must release read lock before acquiring write lock.
            READ_LOCK.unlock();
            WRITE_LOCK.lock();
            // Recheck state because another thread might have
            // acquired write lock and changed state before we did.
            try {
                if (invalid) {
                    loadResourceRoleList();
                    invalid = false;
                }
                // Downgrade by acquiring read lock before releasing write lock
                READ_LOCK.lock();
            } finally {
                // Unlock write, still hold read
                WRITE_LOCK.unlock();
            }
        }
        // Read safely
        Set<SimpleGrantedAuthority> availableAuthorities;
        try {
            availableAuthorities = processResourceRoleList(requestContext);
        } finally {
            READ_LOCK.unlock();
        }
        return availableAuthorities;
    }

    private Set<SimpleGrantedAuthority> processResourceRoleList
            (RequestAuthorizationContext requestContext) {
        // ...
    }

}

The BlogAuthorizationManager implementation class also uses the multi-threaded ThreadLocal variable ANONYMOUS to refine the authorization of anonymously accessible resources. This variable is first set to FALSE by default in the check() method, indicating that anonymous access is not available by default, and then set to TRUE in the processResourceRoleList() method if anonymous access is determined to be available, and the request is then authorised in the check() method. One caveat to using the ThreadLocal variable is that its remove() method should be called to clean it up after use, otherwise it may cause a memory leak.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Component
public class BlogAuthorizationManagerImpl implements BlogAuthorizationManager {

    private static final ThreadLocal<Boolean> ANONYMOUS = new ThreadLocal<>();
    // ...
    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication,
                                       RequestAuthorizationContext requestContext) {
        boolean authorized;
        ANONYMOUS.set(FALSE);
        try {
            // ...
            authorized = ANONYMOUS.get()
                    || grantedList.stream().anyMatch(availableSet::contains);
        } finally {
            ANONYMOUS.remove();
        }
        return new AuthorizationDecision(authorized);
    }
    // ...
    private Set<SimpleGrantedAuthority> processResourceRoleList
            (RequestAuthorizationContext requestContext) {
        // ...
                if (TRUE_OF_INT.equals(resource.getIsAnonymous())) {
                    ANONYMOUS.set(TRUE);
                }
        // ...
    }

}

Reference: https://insightblog.cn/articles/62