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.
The above diagram is built on the SecurityFilterChain
.
- First, a user makes an unauthenticated request to its unauthorized resource (/private).
- Spring Security’s
FilterSecurityInterceptor
indicates that the unauthenticated request has been rejected by throwing an AccessDeniedException; (AuthorizationFilter
replacesFilterSecurityInterceptor
) - Since the user is not authenticated,
ExceptionTranslationFilter
starts Start Authentication and redirects the request toAuthenticationEntryPoint
, where the user is asked to redirect to the landing page. - The browser requests to go to the login page to which it was redirected.
- 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.
The above diagram is built on the SecurityFilterChain
.
- When a user submits their username and password, the
UsernamePasswordAuthenticationFilter
takes the username and password extracted from theHttpServletRequest
instance and creates aUsernamePasswordAuthenticationToken
, which is anAuthentication
type. - Next, the
UsernamePasswordAuthenticationToken
is passed into theAuthenticationManager
instance to be authenticated. - Failure if authentication fails.
SecurityContextHolder
is emptied.RememberMeServices.loginFail
is called. (This project does not use remember me authentication)AuthenticationFailureHandler
is called, entering the authentication failure handler.
- Success if authentication is successful.
SessionAuthenticationStrategy
is notified of new logins. (Not used in this project)Authentication
is set onSecurityContextHolder
.RememberMeServices.loginSuccess
is called.ApplicationEventPublisher
publishesInteractiveAuthenticationSuccessEvent
event. (Not used in this project)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.
UsernamePasswordAuthenticationFilter
passesUsernamePasswordAuthenticationToken
to theAuthenticationManager
, which is implemented by theProviderManager
.- The
ProviderManager
is configured to use anAuthenticationProvider
of typeDaoAuthenticationProvider
. - The
DaoAuthenticationProvider
looks upUserDetails
from theUserDetailsService
. - The
DaoAuthenticationProvider
uses thePasswordEncoder
to verify the password on theUserDetails
returned in the previous step. - When authentication is successful, the
Authentication
returned is of typeUsernamePasswordAuthenticationToken
and there is aprincipal
that is theUserDetails
returned by the configuredUserDetailsService
. Ultimately, the returnedUsernamePasswordAuthenticationToken
is set on theSecurityContextHolder
by theUsernamePasswordAuthenticationFilter
.
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.
|
|
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.
|
|
The following diagram explains how to get session information from the SpringSessionBackedSessionRegistry
by saving the username.
- The user logs in successfully and enters the
AuthenticationSuccessHandler
’s authentication success handler. - The user session is destroyed, causing the
SessionEventListener
to receive the user session destruction event. - 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.
- The
SpringSessionBackedSessionRegistry
gets the usernames of all online users from Redis and queries the session information with the usernames. - 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
.
The
Authentication
implementation used in this project isUsernamePasswordAuthenticationToken
with a customAuthorizationManager
implementation
- First,
AuthorizationFilter
gets anAuthentication
fromSecurityContextHolder
. It wraps it in aSupplier
to delay the lookup. - Next, it passes
Supplier<Authentication>
andHttpServletRequest
to theAuthorizationManager
. - If the authorization is denied, an
AccessDeniedException
is thrown. In this case,ExceptionTranslationFilter
handles theAccessDeniedException
. - 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.
|
|
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.
|
|
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.
|
|
Reference:
https://insightblog.cn/articles/62