Spring Security Resolution: Personalized Authentication and RememberMe Implementation

Spring Security Resolution (3) - Personalized Authentication and RememberMe Implementation

When learning Spring Cloud, when encountering the related content of authorized service oauth, he always knows half of it, so he decided to study and collate the content, principle and design of Spring Security, Spring Security Oauth2 and other permissions, authentication-related content. This series of articles is written to enhance the impression and understanding in the process of learning. If there is any infringement, please let me know.

Project environment:

  • JDK1.8
  • Spring boot 2.x
  • Spring Security 5.x

Personalized authentication

(1) Configuration login

_In the process of authorization and authentication, we all use the default login page of Security (/ login), so how do we implement a login page if we want to customize it? In fact, it's very simple. We create a new FormAuthentication Config configuration class and then implement the following settings in the configure(HttpSecurity http) method:

        http.formLogin()
                //You can set custom login pages or (login) interfaces
                // Note 1: Generally speaking, when the interface is set to (login) interface, it will be configured to be accessible without permission, so anonymous filter will be used, which means that the authentication process will not be taken, so we generally do not set the interface address directly.
                // Note 2: The address you configure here must be configured to have no permission to access, otherwise there will be a problem of redirection all the time (because after having no permission, you will redirect to the login page url that you configure here)
                .loginPage(securityProperties.getLogin().getLoginPage())
                //.loginPage("/loginRequire")
                // Specify the URL of the authentication credentials (default is / login).
                // Note 1: The modified url here means that the UsernamePassword Authentication Filter will validate the url here
                // Note 2: The interface address set by loginPage is different from that set by loginPage, but once loginPage sets the access interface url, the configuration here will be meaningless.
                // Note 3: The Url set here has default unauthorized access
                .loginProcessingUrl(securityProperties.getLogin().getLoginUrl())
                //Setting up successful and failed processors, respectively
                .successHandler(customAuthenticationSuccessHandler)
                .failureHandler(customAuthenticationFailureHandler);

_Finally, in Spring Security Config's configure (Http Security http) method, call formAuthentication Config. configure (http).

As you can see, loginPage() sets the login page or interface, and loginProcessing Url () sets the interface address (must be Post) that UsernamePassword Authentication Filter matches. (Students who have seen the authorization process should know that its default is / login.) Here are the following points worth noting:

  • loginPage() The address configured here (whether interface url or login page) must be configured as unauthorized access, otherwise there will be a constant redirection problem (because unauthorized access will redirect to the login page url configured here).
  • loginPage() is generally not directly set to the (login) interface, because the interface will be configured to access without permission (of course, the login page also needs to configure access without permission), so it will go anonymous filter, which means it will not go through the authentication process, so we generally do not directly set to the interface address.
  • loginProcessingUrl() The modified url here means that the UsernamePassword Authentication Filter will verify the url here
  • loginProcessingUrl() The Url set here is accessed by default without permission, which is different from the interface address set by loginPage. Once loginPage sets the interface url, the configuration here will be meaningless.
  • successHandler() and failureHandler set up the Authentication Success Processor and the Authentication Failure Processor respectively. (If you are not impressed with the two processors, it is recommended to review the authorization process.)

(2) Configuration of successful and failed processors

In the authorization process, we mentioned these two processors briefly. The default processors in Security are SavedRequest Aware Authentication Success Handler and Simple Url Authentication Failure Handler. This time, we customized these two processors, Custom Authentication Success Handler and Simple Url Authentication Failure Handler. S SavedRequestAware Authentication Success Handler) Overrides the onAuthentication Success () method:

@Component("customAuthenticationSuccessHandler")
@Slf4j
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    @Autowired
    private SecurityProperties securityProperties;

    private RequestCache requestCache = new HttpSessionRequestCache();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        logger.info("Successful login");
        // If loginSuccessUrl is set, it always jumps to the set address.
        // If not, try to jump to the address you visited before login. If the address you visited before login is empty, jump to the root path of the site.
        if (!StringUtils.isEmpty(securityProperties.getLogin().getLoginSuccessUrl())) {
            requestCache.removeRequest(request, response);
            setAlwaysUseDefaultTargetUrl(true);
            setDefaultTargetUrl(securityProperties.getLogin().getLoginSuccessUrl());
        }
        super.onAuthenticationSuccess(request, response, authentication);
    }

}

Rewrite the onAuthentication Failure () method with Custom Authentication Failure Handler (extends SimpleUrl Authentication Failure Handler):

@Component("customAuthenticationFailureHandler")
@Slf4j
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {

        logger.info("Logon failure");
        if (StringUtils.isEmpty(securityProperties.getLogin().getLoginErrorUrl())){

            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));

        } else {
            // Login Failure Page with Jump Settings
            redirectStrategy.sendRedirect(request,response,securityProperties.getLogin().getLoginErrorUrl());
        }

    }
}

(3) Customized landing page

No more descriptions are given here. Paste the code directly:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Sign in</title>
</head>
<body>
<h2>Login page</h2>
<form action="/loginUp" method="post">  
    <table>
        <tr>
            <td>User name:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>Password:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td colspan='2'><input name="remember-me" type="checkbox" value="true"/>Remember me</td>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit">Sign in</button>
            </td>
        </tr>
    </table>
</form>
</body>
</html>

Note that the requested address here is the one configured by loginProcessingUrl().

(4) Testing and Verification

_There is no result map, as long as we understand the process of the result, we can do this:
localhost:8080 - > Click Test to Verify Security Privilege Control - > Jump to our custom login page / loginUp.html. After login - > configure loginSuccessUrl, jump to loginSuccess.html; otherwise jump directly to the / get_user/test interface to return the results. The whole process involves our customized login page and the customized login success/failure processor.

2. RememberMe (Remember Me) Functional Analysis

(1) RememberMe Function Implementation Configuration

First of all, we add rememberMe configuration, and then look at the phenomenon:

1. Create persistent_logins tables to store token and user-related information:

create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null);

2. Add rememberMe configuration information

    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // If the token table does not exist, you can initialize the persistent_logins (ddl is in the db directory) table with the following statement; if it exists, please comment out this statement, otherwise an error will be reported.
        //tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }
    
     @Override
    protected void configure(HttpSecurity http) throws Exception {

        formAuthenticationConfig.configure(http);
        http.   ....
                .and()
                // Turning on the remember me feature means that RememberMeAuthentication Filter will get token information from Cookie
                .rememberMe()
                // Setting token Repository, where jdbcTokenRepository Impl is used by default, means that we will read the user information represented by token from the database
                .tokenRepository(persistentTokenRepository())
                // Setting up userDetails Service, as in the authentication process, RememberMe has a dedicated RememberMe Authentication Provider, which means that you need to use UserDetails Service to load UserDetails information
                .userDetailsService(userDetailsService)
                // Set the effective time of rememberMe, which is set by configuration
                .tokenValiditySeconds(securityProperties.getLogin().getRememberMeSeconds())
                .and()
                .csrf().disable(); // Closing csrf Cross-Station (Domain) Attack Prevention and Control
    }

The following configuration is explained here:

  • rememberMe() turns on the remember me function, which means that RememberMe Authentication Filter will get token information from Cookie
  • tokenRepository() configures token's acquisition policy, which is configured to read from the database
  • UserDetails Service () Configure UserDetaisService (If you are not familiar with this object, it is recommended to review the authentication process)
  • TokenValidity Seconds () sets the effective time of rememberMe, which is set by configuration

Another important configuration is on the login page, where name="remember-me" is required, and rememberMe is used to turn on remermberMe functionality by verifying this configuration.

<input name="remember-me" type="checkbox" value="true"/>Remember me</td>

_The results of the operation should be as follows: Enter the landing page - > Check and remember me to log in - > After success, check the persistent_logins table and find a data - > Restart the project - > Revisit the pages that need to be logged in, and find that you can access them without login - > Delete the persistent_logins table data. Wait for the valid time of token settings to expire, and then refresh the page to find the jump to the landing page.

(2) RembemberMe Implementation of Source Code Parsing

_First, let's look at the successfulAuthentication() method internal source code of UsernamePassword Authentication Filer (Abstract Authentication Processing Filter):

protected void successfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {
        
        // 1. Set Authentication Object that has been authenticated successfully into SecurityContext
        SecurityContextHolder.getContext().setAuthentication(authResult);
        
        // 2 Call RememberMe related service processing
        rememberMeServices.loginSuccess(request, response, authResult);

        // Fire event
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                    authResult, this.getClass()));
        }
        //3 Call Successful Processor
        successHandler.onAuthenticationSuccess(request, response, authResult);
    }

We found a line of code that we focused on this time: rememberMeServices.loginSuccess(request, response, authResult). Look at the internal source code of this method:

@Override
    public final void loginSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication successfulAuthentication) {
        // Here's how to determine if the user checked to remember me.
        if (!rememberMeRequested(request, parameter)) {
            logger.debug("Remember-me login not requested.");
            return;
        }

        onLoginSuccess(request, response, successfulAuthentication);
    }

Use rememberMeRequested() to determine if rememberMeRequested() has been checked.
The onLoginSuccess() method eventually calls the onLoginSuccess() method of PersistentTokenBasedRememberMeServices, and the source code of the method is as follows:

protected void onLoginSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication successfulAuthentication) {
        // 1 Get Account Name
        String username = successfulAuthentication.getName();
        
        // 2 Create Persistent RememberMeToken object
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
                username, generateSeriesData(), generateTokenData(), new Date());
        try {
            // 3 Store persistent RememberMeToken information through token Repository
            tokenRepository.createNewToken(persistentToken);
            // 4 Add persistent RememberMeToken information to Cookie
            addCookie(persistentToken, request, response);
        }
        catch (Exception e) {
            logger.error("Failed to save persistent token ", e);
        }
    }

Analyse the following source steps:

  • Get account information username
  • Pass in username to create PersistentRememberMeToken object
  • Storing persistent RememberMeToken information through token Repository
  • Add persistent RememberMeToken information to Cookie

_Token Repository here is what we configure the rememberMe function. After the above analysis, we see that rememberServices will create a token information and store it in the database (because we are configuring the Jdbc Token Repository Impl) and add the token information to the Cookie. At this point, we have seen some business processes before RememberMe implementation, then how to implement RememberMe later, I think you probably have a bottom line. Throw out the filter class RememberMeAuthentication Filter that we didn't mention in the previous authorization process. It's a filter between UsernamePassword Authentication Filter and Nonymous Authentication Filter. It's mainly responsible for getting to Cookie after the previous filter has not been authenticated successfully. Ken information is then retrieved by token Repository, User Details Servcie loads User Details information, and finally creates Authticaton (RememberMeAuthentication Token) information and calls Authentication Manager. Authentication () for authentication process.

RememberMeAuthenticationFilter

Let's look at the dofile method source code of RememberMeAuthentication Filter:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            //  1 Call rememberMeServices.autoLogin() to get Authtication information
            Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
                    response);

            if (rememberMeAuth != null) {
                // Attempt authenticaton via AuthenticationManager
                try {
                    // 2 Call authenticationManager.authenticate() authentication
                    rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
                    
                    ......
                    }

                }
                catch (AuthenticationException authenticationException) {
                .....
            }

            chain.doFilter(request, response);
        }

We focus on rememberMeServices.autoLogin(request,response) method implementation, viewer source code:

@Override
    public final Authentication autoLogin(HttpServletRequest request,
            HttpServletResponse response) {
        // 1 Get token information from Cookie
        String rememberMeCookie = extractRememberMeCookie(request);

        if (rememberMeCookie == null) {
            return null;
        }
        
        if (rememberMeCookie.length() == 0) {
            cancelCookie(request, response);
            return null;
        }

        UserDetails user = null;

        try {
            // 2 parsing token information
            String[] cookieTokens = decodeCookie(rememberMeCookie);
            // 3 Generating Uerdetails information through token information
            user = processAutoLoginCookie(cookieTokens, request, response);
            userDetailsChecker.check(user);

            logger.debug("Remember-me cookie accepted");
            // 4 Create Authentication with UserDetails Information 
            return createSuccessfulAuthentication(request, user);
        } 
        .....
    }

Internal implementation steps:

  • Get token information from Cookie and parse it
  • Generating UserDetails (processAutoLoginCookie() method by parsing token)
  • Generate Authentication through UserDetails (create Successful Authentication () create RememberMeAuthentication Token)

One of the most critical is how the processAutoLoginCookie() method generates the UserDetails object. Let's look at the source code implementation of this method:

protected UserDetails processAutoLoginCookie(String[] cookieTokens,
            HttpServletRequest request, HttpServletResponse response) {
        final String presentedSeries = cookieTokens[0];
        final String presentedToken = cookieTokens[1];
        // 1 Load database token information through token Repository
        PersistentRememberMeToken token = tokenRepository
                .getTokenForSeries(presentedSeries);

        PersistentRememberMeToken newToken = new PersistentRememberMeToken(
                token.getUsername(), token.getSeries(), generateTokenData(), new Date());
        // 2. Judging whether the token in the user's incoming token is consistent with that in the data, the inconsistency may have security problems.
        if (!presentedToken.equals(token.getTokenValue())) {
            tokenRepository.removeUserTokens(token.getUsername());
            throw new CookieTheftException(
                    messages.getMessage(
                            "PersistentTokenBasedRememberMeServices.cookieStolen",
                            "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
        }
        try {
            // 3 Update token and add it to Cookie
            tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
                    newToken.getDate());
            addCookie(newToken, request, response);
        }
        catch (Exception e) {
            throw new RememberMeAuthenticationException(
                    "Autologin failed due to data access problem");
        }
        // 4 Load UserDetails information through the UserDetails Service (). loadUserByUsername () method and return
        return getUserDetailsService().loadUserByUsername(token.getUsername());
    }

Let's look at its internal steps:

  • Loading database token information through token Repository
  • Judging whether the token in the user's incoming token is consistent with the token in the data, inconsistency may have security problems
  • Update token and add it to Cookie
  • Load UserDetails information through the UserDetails Service (). loadUserByUsername () method and return

Seeing this, I believe you can see why token Repository and User Details Service were configured when rememberMe was enabled.

Here I will not demonstrate the whole process of implementation, the old rules, the flow chart above:

_This article introduces personalized authentication and RememberMe code access to the security module in the code warehouse, project GitHub address: https://github.com/BUG9/spring-security

If you are interested in these, you are welcome to support star, follow, collect and forward!

Tags: Java Spring Database github JSON

Posted on Wed, 28 Aug 2019 07:55:56 -0700 by jrolands