Spring Securtiy certification process (source code analysis)

When authenticating with Spring Security Framework, you may encounter this problem:

The username or password you entered is either empty or wrong, it's wrong information is Bad credentials.

So if you want to give the corresponding error message, depending on the circumstances how to do it?

This time we only understand Spring Securiy certification process in order to know how to modify the code.

Well, look at the following example, most people WebSecurityConfig the configure code similar to the following:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // TODO Auto-generated method stub
        http
        .authorizeRequests()
        .anyRequest().permitAll()
        .and()
        .formLogin().loginPage("/signin")
        .usernameParameter("username")
        .passwordParameter("password")
        .loginProcessingUrl("/signin")
        .and()
        .csrf().disable();
    }

I believe the above code we all know what that means: Any request for information are allowed, that is, does not require authentication.

Login page request is / signin, name attribute username and password parameters are the username, password. Login page request is a form of action / signin.

Of course, this action does not have to login page and request the same. Finally, that is to prohibit cross-site request forgery.

Login authentication code and contact should be greater from loginPage () to loginProcessingUrl () method inside.

Start with loginPage looks, the left mouse button and drag to cover loginPage, then right into the Open Declaration on the FormLoginConfigurer class.

There are two ways this class is worth noting: loginPage constructors and methods.

    public FormLoginConfigurer() {
        super(new UsernamePasswordAuthenticationFilter(), null);
        usernameParameter("username");
        passwordParameter("password");
    }

    public FormLoginConfigurer<H> loginPage(String loginPage) {
        return super.loginPage(loginPage);
    }

Constructor uses a user name and password authentication filter class, the look on certification and relationship.

loginPage method you can follow the steps on their own to see, and now look directly UsernamePasswordAuthenticationFilter class.

    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
    private boolean postOnly = true;

   public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }

   public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        if (username == null) {
            username = "";
        }
        if (password == null) {
            password = "";
        }
        username = username.trim();
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
        username, password);
    
setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); }

 This is just one part of the code, others can see themselves. Two strings and constructors defined in the class define the default login.

Log action request to the / login, user name and password are acquired with username, password property values ​​POST method.

Class parent GenericFilterBean InitializingBean parent class implements an interface, which is initialized to a Bean.

When you see attemptAuthentication, that he is certified method friends.

Here we directly see the new UsernamePasswordAuthenticationToken (username, password);

    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }

From here you can know that the user name and password exist principal, credentials inside.

Now we just need to remember login information exists in the authRequest. Under setDetails, although I am not interested for now.

    protected void setDetails(HttpServletRequest request,
            UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

It calls a buildDetails method is actually called :( traced can see)

    
/**
  * Records the remote address and will also set the session Id if a session already
  * exists (it won't create one).
  *
  * @param request that the authentication request was received from
  */
public WebAuthenticationDetails(HttpServletRequest request) { this.remoteAddress = request.getRemoteAddr(); HttpSession session = request.getSession(false); this.sessionId = (session != null) ? session.getId() : null; }

You can see from the source code comments, it is recorded remote address and sets a session ID, here we do not care of it.

Direct look at this one: return the this .getAuthenticationManager () the authenticate (AuthRequest);.

It is a call to implement the authenticate method of the class AuthenticationManager interface.

We could not find it from the source code used in which implementation class, said the Internet is ProviderManager class, we look at the class.

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
        InitializingBean {
  public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        boolean debug = logger.isDebugEnabled();

        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }

            if (debug) {
                logger.debug("Authentication attempt using "
                        + provider.getClass().getName());
            }

            try {
                result = provider.authenticate(authentication);

                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException e) {
                prepareException(e, authentication);
                // SEC-546: Avoid polling additional providers if auth failure is due to
                // invalid account status
                throw e;
            }
            catch (InternalAuthenticationServiceException e) {
                prepareException(e, authentication);
                throw e;
            }
            catch (AuthenticationException e) {
                lastException = e;
            }
        }

        if (result == null && parent != null) {
            // Allow the parent to try.
            try {
                result = parentResult = parent.authenticate(authentication);
            }
            catch (ProviderNotFoundException e) {
                // ignore as we will throw below if no other exception occurred prior to
                // calling parent and the parent
                // may throw ProviderNotFound even though a provider in the child already
                // handled the request
            }
            catch (AuthenticationException e) {
                lastException = parentException = e;
            }
        }

        if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
                // Authentication is complete. Remove credentials and other secret data
                // from authentication
                ((CredentialsContainer) result).eraseCredentials();
            }

            // If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
            // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
            if (parentResult == null) {
                eventPublisher.publishAuthenticationSuccess(result);
            }
            return result;
        }

        // Parent was null, or didn't authenticate (or throw an exception).

        if (lastException == null) {
            lastException = new ProviderNotFoundException(messages.getMessage(
                    "ProviderManager.providerNotFound",
                    new Object[] { toTest.getName() },
                    "No AuthenticationProvider found for {0}"));
        }

        // If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
        // This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
        if (parentException == null) {
            prepareException(lastException, authentication);
        }

        throw lastException;
    }
}

Here I only give this kind of statement and authenticate method, can be seen from the class declaration it will be initialized to a Bean, we could not find normal right.

authenticate method will traverse all AuthenticationProvider, then call the authenticate method of the provider.

If the authentication result is not empty, then the result will be saved, and erased authentication information and then return result.

Is empty, then the general is not provided AuthenticationProvider, it will be reported ProviderNotFoundException error.

Now let's authenticate method provider of next.

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new CustomAuthenticationProvider();
        provider.setMessageSource(messageSource);
        provider.setUserDetailsService(userService);
        provider.setPasswordEncoder(new BCryptPasswordEncoder());
        return provider;
    }

This is what I wrote a AuthenticationProvider, but I rewrote a class inherits DaoAuthenticationProvider.

Here we look at DaoAuthenticationProvider class :( this class there and did not find authenticate method, then start looking for its parent class)

Parent is AbstractUserDetailsAuthenticationProvider, it also implements InitializingBean interface is initialized to a Bean.

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                () -> messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only UsernamePasswordAuthenticationToken is supported"));

        // Determine username
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();

        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);

        if (user == null) {
            cacheWasUsed = false;

            try {
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (UsernameNotFoundException notFound) {
                logger.debug("User '" + username + "' not found");

                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "Bad credentials"));
                }
                else {
                    throw notFound;
                }
            }

            Assert.notNull(user,
                    "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (AuthenticationException exception) {
            if (cacheWasUsed) {
                // There was a problem, so try again after checking
                // we're using latest data (i.e. not from the cache)
                cacheWasUsed = false;
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            else {
                throw exception;
            }
        }

        postAuthenticationChecks.check(user);

        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;

        if (forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }

        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

 In this code we can know: If authentication.getPrincipal () is empty, then, username will be as NONE_PROVIDED.

Not empty words will be authentication.getPrincipal (), that is, user name, but this is not the type of type String, but can cast.

Code is authentication.getName (), substantially the same as that above, but the type is of type String.

Then define a user, first try to obtain user from the cache, then you did not get to get through retrieveUser.

RetrieveUser this class is an abstract method, we now look DaoAuthenticationProvider class method.

protected final UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        }
        catch (UsernameNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
            throw ex;
        }
        catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }

You can see from the code it is to get users to UserDetailsService method we wrote earlier.

Next we look at the code behind this part of the exception code, etc. We will look at.

try {
            preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }

These two codes are the user to check the first line of code is actually calling this part:

private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
        public void check(UserDetails user) {
            if (!user.isAccountNonLocked()) {
                logger.debug("User account is locked");

                throw new LockedException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.locked",
                        "User account is locked"));
            }

            if (!user.isEnabled()) {
                logger.debug("User account is disabled");

                throw new DisabledException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.disabled",
                        "User is disabled"));
            }

            if (!user.isAccountNonExpired()) {
                logger.debug("User account is expired");

                throw new AccountExpiredException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.expired",
                        "User account has expired"));
            }
        }
    }

You can not see the password check, just to check on the status of the user. So we do not care of it, look at a line of code:

@SuppressWarnings("deprecation")
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            logger.debug("Authentication failed: no credentials provided");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }

        String presentedPassword = authentication.getCredentials().toString();

        if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            logger.debug("Authentication failed: password does not match stored value");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
    }

Here's get the password operation: authentication.getCredentials ().

Then if the password is not empty, then on through passwordEncoder.matches (presentedPassword, userDetails.getPassword () to check whether the match.

If the match is successful, ah, this part is over, we go back to class AbstractUserDetailsAuthenticationProvider the authenticate method.

return createSuccessAuthentication(principalToReturn, authentication, user);

It returns a successful authentication methods to create a return value. Here we go on.

Now let's return to the error handling AbstractUserDetailsAuthenticationProvider class:

try {
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (UsernameNotFoundException notFound) {
                logger.debug("User '" + username + "' not found");

                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "Bad credentials"));
                }
                else {
                    throw notFound;
                }
            }

This is the wrong user can not find the cause, we look messages.getMessage ():

    public String getMessage(String code, String defaultMessage) {
        String msg = this.messageSource.getMessage(code, null, defaultMessage, getDefaultLocale());
        return (msg != null ? msg : "");
    }

Let's look at the inside of the getMessage ():

It is an interface class method: in accordance with the code string messageSource returned, if the code is not present, returns defaultMessage.

Since it is an interface class, then we look at its implementation class, back to messageSource, look at it:

public class SpringSecurityMessageSource extends ResourceBundleMessageSource {
    // ~ Constructors
    // ===================================================================================================

    public SpringSecurityMessageSource() {
        setBasename("org.springframework.security.messages");
    }

    // ~ Methods
    // ========================================================================================================

    public static MessageSourceAccessor getAccessor() {
        return new MessageSourceAccessor(new SpringSecurityMessageSource());
    }
}

The original source of this data is to find a path inside.

Other error handling is the same, is omitted here. How do we get the error message?

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // TODO Auto-generated method stub
        http
        .authorizeRequests()
        .anyRequest().permitAll()
        .and()
        .formLogin().loginPage("/signin")
        .usernameParameter("username")
        .passwordParameter("password")
        .loginProcessingUrl("/signin")
        .failureHandler(authenticationFailureHandler)
        .and()
        .csrf().disable();

FailureHandler not see that this is a login failure processor, coupled with just a look inside here Source: AbstractAuthenticationFilterConfigurer

    /**
     * Specifies the {@link AuthenticationFailureHandler} to use when authentication
     * fails. The default is redirecting to "/login?error" using
     * {@link SimpleUrlAuthenticationFailureHandler}
     *
     * @param authenticationFailureHandler the {@link AuthenticationFailureHandler} to use
     * when authentication fails.
     * @return the {@link FormLoginConfigurer} for additional customization
     */
    public final T failureHandler(
            AuthenticationFailureHandler authenticationFailureHandler) {
        this.failureUrl = null;
        this.failureHandler = authenticationFailureHandler;
        return getSelf();
    }

As can be seen from the comment processor failure default SimpleUrlAuthenticationFailureHandler:

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

        if (defaultFailureUrl == null) {
            logger.debug("No failure URL set, sending 401 Unauthorized error");

            response.sendError(HttpStatus.UNAUTHORIZED.value(),
                HttpStatus.UNAUTHORIZED.getReasonPhrase());
        }
        else {
            saveException(request, exception);

            if (forwardToDestination) {
                logger.debug("Forwarding to " + defaultFailureUrl);

                request.getRequestDispatcher(defaultFailureUrl)
                        .forward(request, response);
            }
            else {
                logger.debug("Redirecting to " + defaultFailureUrl);
                redirectStrategy.sendRedirect(request, response, defaultFailureUrl);
            }
        }
    }

Because the default is defaultFailureUrl / login? Error, it can be seen from AbstractAuthenticationFilterConfigurer class.

After the login fails, calls saveException (request, exception); save the error messages.

protected final void saveException(HttpServletRequest request,
            AuthenticationException exception) {
        if (forwardToDestination) {
            request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
        }
        else {
            HttpSession session = request.getSession(false);

            if (session != null || allowSessionCreation) {
                request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION,
                        exception);
            }
        }
    }

Because the class forwardToDestination is false, else it will perform in the statement.

Save the error information to the WebAttributes.AUTHENTICATION_EXCEPTION attribute session:

public static final String AUTHENTICATION_EXCEPTION = "SPRING_SECURITY_LAST_EXCEPTION";

All we can to get through this error message attribute the session. (Thymeleaf)

<p th:if="${param.error}" th:text="${session?.SPRING_SECURITY_LAST_EXCEPTION?.message}" ></p>

Well, are introduced over, you can look at my CustomAuthenticationProvider:

package security.config;

import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.util.Assert;

public class CustomAuthenticationProvider extends DaoAuthenticationProvider {
    
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        // TODO Auto-generated method stub

        String presentedPassword = authentication.getCredentials().toString();
        if (!getPasswordEncoder().matches(presentedPassword, userDetails.getPassword())) {
            logger.debug("Authentication failed: password does not match stored value");

            throw new BadCredentialsException(messages.getMessage(
                    "UNameOrPwdIsError","Username or Password is not correct"));
        }
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // TODO Auto-generated method stub
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                () -> messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only UsernamePasswordAuthenticationToken is supported"));

        if("".equals(authentication.getPrincipal())) {
            throw new BadCredentialsException(messages.getMessage(
                    "UsernameIsNull","Username cannot be empty"));
        }
        if("".equals(authentication.getCredentials())) {
            throw new BadCredentialsException(messages.getMessage(
                    "PasswordIsNull","Password cannot be empty"));
        }
        
        String username = (String) authentication.getPrincipal();
        boolean cacheWasUsed = true;
        UserDetails user = this.getUserCache().getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;
            try {
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (UsernameNotFoundException notFound) {
                logger.debug("User '" + username + "' not found");

                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage(
                            "UNameOrPwdIsError","Username or Password is not correct"));
                }
                else {
                    throw notFound;
                }
            }
            Assert.notNull(user,
                    "retrieveUser returned null - a violation of the interface contract");
        }
        try {
            getPreAuthenticationChecks().check(user);
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (AuthenticationException exception) {
            if (cacheWasUsed) {
                cacheWasUsed = false;
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
                getPreAuthenticationChecks().check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            else {
                throw exception;
            }
        }

        getPostAuthenticationChecks().check(user);

        if (!cacheWasUsed) {
            this.getUserCache().putUserInCache(user);
        }

        Object principalToReturn = user;

        if (isForcePrincipalAsString()) {
            principalToReturn = user.getUsername();
        }

        return createSuccessAuthentication(principalToReturn, authentication, user);
    }
    

}

It is worth noting that "" .equals (Authentication.getPrincipal ()), "" .equals (authentication.getCredentials ())

Because if in accordance with the AbstractUserDetailsAuthenticationProvider class to write, never found that this step is null.

I by adding the code System.out.println (username); know, and it should be a pit.

Item code for your reference:

Link: https: //pan.baidu.com/s/1pNWQMyIgZOzX5_rF3Tvd2A
extraction code: m585

Also, if you think there is wrong, please comment, thank you!

Guess you like

Origin www.cnblogs.com/M-Anonymous/p/12003968.html