Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

Hello ya, I am ning Zaichun, blogger, come on!!

I wonder if you have ever thought of using Spring Security to implement various login methods. This time, my friend mentioned some login requirements to me. Besides the original account and password login, I need to implement phone verification code and email verification code login. Let me achieve third-party login, such as Gitee, Github, etc.

This paper mainly explains how Security can realize email and phone verification code login on the basis of account password and without changing the original service.

Cover: clouds seen from the playground in the evening

🐱 πŸ’» preface:

Last article I wrote the Security login detailed process, source code, analysis. By mastering this login process, we can better customize Security operations.

I wrote this article before, also read a lot of blogger’s article, write very good, have to the source code aspect analysis, also have to some related design concept understanding article.

This is suitable for those who have learned Security for some time and have a good understanding of it. However, it is not so friendly to me and other young people who are eager to solve the current problem. πŸ˜‚

I. πŸ€Έβ™‚οΈ Theoretical knowledge

So let’s think about what the process looks like, right?

  1. Enter the email number to obtain the verification code
  2. Enter the obtained verification code to log in (Login interface:/email/loginThe default cannot be used here/loginBecause we are extension)
  3. In custom filtersEmailCodeAuthenticationFilterTo check whether the verification code is correct and whether the email account is empty
  4. Encapsulate it into one that requires authenticationAuthentication, here we customize the implementation asEmailCodeAuthenticationToken.
  5. willAuthentiction To pass toAuthenticationManagerIn the interfaceauthenticateMethod for authentication processing
  6. AuthenticationManagerThe default implementation class isProviderManager ,ProviderManagerAnd entrusted toAuthenticationProviderFor processing
  7. Let’s make a custom oneEmailCodeAuthenticationProviderimplementationAuthenticationProviderTo implement authentication.
  8. The custom ofEmailCodeAuthenticationFilterinheritedAbstractAuthenticationProcessingFilter An abstract class,AbstractAuthenticationProcessingFilter insuccessfulAuthentication Method is used to process the login successSecurityContextHolder.getContext().setAuthentication()Method will beAuthentication Authentication information object bound toSecurityContextThe security context.
  9. In fact, there are two ways to deal with the authentication after passing, one is to directly rewrite the filtersuccessfulAuthenticationThe other is implementationAuthenticationSuccessHandlerTo process the authentication pass.
  10. Authentication failures are the same and can be overriddenunsuccessfulAuthenticationMethod can also be implementedAuthenticationFailureHandlerTo handle authentication failures.

That’s the general process. From this process, we can see that the following components need to be rewritten:

  1. EmailCodeAuthenticationFilter: mail authentication login filter
  2. EmailCodeAuthenticationToken: Authentication token
  3. EmailCodeAuthenticationProvider: Email identity authentication processing
  4. AuthenticationSuccessHandler: Processes the successful login operation
  5. AuthenticationFailureHandler: Processes login failures

Next, I am imitating the source code to write my code, I suggest you can use when, to see more, I am here to remove some of the code is not related to this.

Come on!!!!!

Second, the 🐱 🏍 EmailCodeAuthenticationFilter

We need to rewrite EmailCodeAuthenticationFilter, actual inherited AbstractAuthenticationProcessingFilter abstract class, we can’t write, Can first take a look at its default implementation UsernamePasswordAuthenticationFilter is how are you, copy homework, it is everybody’s strengths.

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	
	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";

	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

	private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login"."POST");
	// The parameters passed from the foreground
	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;

	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

	private boolean postOnly = true;
	
    // Initialize a user password authentication filter. The default login URI is /login. The request mode is POST
	public UsernamePasswordAuthenticationFilter(a) {
		super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
	}

	public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
		super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
	}

    /** Perform the actual authentication. The implementation should do one of the following: 1. Return the populated authentication token for the authenticated user, indicating that the authentication was successful; 2. Before returning, the implementation should perform any additional work needed to complete the process. 3. If the authentication process fails, throw AuthenticationException */
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		if (this.postOnly && ! request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: "+ request.getMethod()); } String username = obtainUsername(request); username = (username ! =null)? username :""; username = username.trim(); String password = obtainPassword(request); password = (password ! =null)? password :"";
        / / generated UsernamePasswordAuthenticationToken later to authenticate in the AuthenticationManager for certification
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
		// You can put some other information in
		setDetails(request, authRequest);
		return this.getAuthenticationManager().authenticate(authRequest);
	}

	@Nullable
	protected String obtainPassword(HttpServletRequest request) {
		return request.getParameter(this.passwordParameter);
	}

	@Nullable
	protected String obtainUsername(HttpServletRequest request) {
		return request.getParameter(this.usernameParameter);
	}

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

	//set and get methods
}
Copy the code

Let’s copy the homework:

package com.crush.security.auth.email_code;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.ArrayList;

/ * * *@Author: crush
 * @Date: 2021-09-08 21:13
 * version 1.0
 */
public class EmailCodeAuthenticationFilter  extends AbstractAuthenticationProcessingFilter {
    /** * The name of the argument passed from the front - used in request.getParameter to get */
    private final String DEFAULT_EMAIL_NAME="email";

    private final String DEFAULT_EMAIL_CODE="e_code";

    @Autowired
    @Override
    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        super.setAuthenticationManager(authenticationManager);
    }
    /** * Only post */
    private boolean postOnly = true;

    /** * Creates the Filter * that is, the url filtered by Filter */
    public EmailCodeAuthenticationFilter(a) {
        super(new AntPathRequestMatcher("/email/login"."POST"));
    }


    /** * filter obtains the user name (email) and password (verification code) to attach to the token, and then gives the token to the provider for authorization */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        if(postOnly && ! request.getMethod().equals("POST") ){
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }else{
            String email = getEmail(request);
            if(email == null){
                email = "";
            }
            email = email.trim();
            // If the captcha is not equal, deliberately error the token and go through the springSecurity error process
            boolean flag = checkCode(request);
            / / encapsulation token
            EmailCodeAuthenticationToken token = new EmailCodeAuthenticationToken(email,new ArrayList<>());
            this.setDetails(request,token);
            // Submit the certificate to manager
            return this.getAuthenticationManager().authenticate(token); }}/** * get the header information and let the appropriate provider validate it */
    public void setDetails(HttpServletRequest request , EmailCodeAuthenticationToken token ){
        token.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    /** * get the incoming Email message */
    public String getEmail(HttpServletRequest request ){
        String result=  request.getParameter(DEFAULT_EMAIL_NAME);
        return result;
    }

    /** * Determine the incoming verification code and the verification code in the session */
    public boolean checkCode(HttpServletRequest request ){
        String code1 = request.getParameter(DEFAULT_EMAIL_CODE);
        System.out.println("code1**********"+code1);
        // TODO writes another link to generate a captcha that is stored in Redis when it is generated
        // The verification code in TODO is written in Redis
        if(code1.equals("123456")) {return true;
        }
        return false;
    }
	// set, get...
}
Copy the code

Third, πŸ€– EmailCodeAuthenticationToken

We are inherited AbstractAuthenticationToken EmailCodeAuthenticationToken, in the same way, we then look at the AbstractAuthenticationToken default implementation is what kind of line.

/ * * * /
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // This is the password of the account
	private final Object principal;

	private Object credentials;

	SetAuthenticated (false) = unsignalable token */
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}

	SetAuthenticated (true) set to trusted token */
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection
        authorities) {
		super(authorities);
		this.principal = principal;
		this.credentials = credentials;
		super.setAuthenticated(true); // must use super, as we override
	}

	@Override
	public Object getCredentials(a) {
		return this.credentials;
	}

	@Override
	public Object getPrincipal(a) {
		return this.principal;
	}

	@Override
	public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { Assert.isTrue(! isAuthenticated,"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
		super.setAuthenticated(false);
	}

	@Override
	public void eraseCredentials(a) {
		super.eraseCredentials();
		this.credentials = null; }}Copy the code

Daily copy operation ha:

/ * * *@Author: crush
 * @Date: 2021-09-08 21:13
 * version 1.0
 */
public class EmailCodeAuthenticationToken extends AbstractAuthenticationToken {


    /** * principal refers to the email address (when not authenticated) */
    private final Object principal;

    public EmailCodeAuthenticationToken(Object principal) {
        super((Collection) null);
        this.principal = principal;
        setAuthenticated(false);
    }

    public EmailCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials(a) {
        return null;
    }

    @Override
    public Object getPrincipal(a) {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        } else {
            super.setAuthenticated(false); }}}Copy the code

That’s pretty easy. πŸ‘¨ πŸ’»

Four, 🐱 πŸ‘“ EmailCodeAuthenticationProvider

Custom EmailCodeAuthenticationProvider is to implement AuthenticationProvider interface, chaozuoye have to learn to look at the source code. Let’s keep going.

4.1, see AbstractUserDetailsAuthenticationProvider first, let’s imitate

The AuthenticationProvider interface has many implementation classes, so I won’t go into detail. We need to look at directly AbstractUserDetailsAuthenticationProvider, this class is designed to respond to UsernamePasswordAuthenticationToken authentication request. But it’s an abstract class, but there’s really only one step that’s implemented in its implementation class, very simple, and I’ll talk about that later.

In this source code I put and check related to some operations to delete, leaving only a few key points, let’s take a look.

/ / this class aims to response UsernamePasswordAuthenticationToken authentication request.
public abstract class AbstractUserDetailsAuthenticationProvider
		implements AuthenticationProvider.InitializingBean.MessageSourceAware {

	protected final Log logger = LogFactory.getLog(getClass());

	private UserCache userCache = new NullUserCache();

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports"."Only UsernamePasswordAuthenticationToken is supported"));
        // Get the user name
		String username = determineUsername(authentication);
		// Check whether the cache exists
        boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			cacheWasUsed = false;
			try {
                // There is no retrieveUser implemented through the word class in the cache to retrieve from the database and return a UserDetails object
				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException ex) {
				this.logger.debug("Failed to find user '" + username + "'");
				if (!this.hideUserNotFoundExceptions) {
					throw ex;
				}
				throw new BadCredentialsException(this.messages
						.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials"."Bad credentials"));
			}
			Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
		}
		try {
            // Perform relevant checks because it may be pulled from the cache and not be up to date
			this.preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException ex) {
			if(! cacheWasUsed) {throw ex;
			}
            // Failed to pass the check and retrieve the latest data
			cacheWasUsed = false;
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			this.preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
        // Check again
		this.postAuthenticationChecks.check(user);
		// Store it in the cache
        if(! cacheWasUsed) {this.userCache.putUserInCache(user);
		}
		Object principalToReturn = user;
		if (this.forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}
        // Create a trusted identity token to return
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

	private String determineUsername(Authentication authentication) {
		return (authentication.getPrincipal() == null)?"NONE_PROVIDED" : authentication.getName();
	}

	In short / * * is created an authenticated UsernamePasswordAuthenticationToken * /
	protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
				authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
		result.setDetails(authentication.getDetails());
		this.logger.debug("Authenticated user");
		return result;
	}


	/** allows subclasses to actually retrieve UserDetails from an implementation-specific location, with the option to throw an AuthenticationException immediately if the credentials provided are incorrect (if you need to bind to a resource as a user to get or generate a UserDetails) */
	protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException;
	/ /...
	

    // In a nutshell: Of course, sometimes we have multiple different 'authenticationProviders' that support different' Authentication 'objects, So when a concrete 'AuthenticationProvier' is passed inside the 'ProviderManager', It selects the supported provider from the AuthenticationProvider list to authenticate the corresponding Authentication object
	@Override
	public boolean supports(Class
        authentication) {
		return(UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)); }}Copy the code

About protected the abstract populated UserDetails retrieveUser implementation, AbstractUserDetailsAuthenticationProvider implementation is DaoAuthenticationProvider.

DaoAuthenticationProvider operations are two main, first retrieve the relevant information from a database, the second is to retrieve the password of user information encryption operation.

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

	private UserDetailsService userDetailsService;
    
	@Override
	protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
            // To retrieve the user, we generally implement the UserDetailsService interface, instead of retrieving the user information from the database to return the security core class UserDetails
			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 newInternalAuthenticationServiceException(ex.getMessage(), ex); }}@Override
	protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
        // Determine whether to use password encryption for this point is not deep, you can go to check this knowledge
		boolean upgradeEncoding = this.userDetailsPasswordService ! =null
				&& this.passwordEncoder.upgradeEncoding(user.getPassword());
		if (upgradeEncoding) {
			String presentedPassword = authentication.getCredentials().toString();
			String newPassword = this.passwordEncoder.encode(presentedPassword);
			user = this.userDetailsPasswordService.updatePassword(user, newPassword);
		}
		return super.createSuccessAuthentication(principal, authentication, user); }}Copy the code

4.2. Copy the homework

Read the source code, in fact, if we want to rewrite, the main thing to do the following:

  1. Public Boolean supports(Class
    authentication) method.

    Sometimes we have multiple AuthenticationProviders that support different Authentication objects, so when a specific AuthenticationProvier is passed inside the ProviderManager, It selects the supported provider from the AuthenticationProvider list to authenticate the corresponding Authentication object

    In short, you specify which Authentication object the AuthenticationProvider authenticates. Such as specified UsernamePasswordAuthenticationToken DaoAuthenticationProvider certification,

    So we specify EmailCodeAuthenticationToken EmailCodeAuthenticationProvider authentication.

  2. Retrieve the database and return a security core class, UserDetail.

  3. Create an authenticated Authentication object

Now that we know what to do, we can start looking at the code.

/ * * *@Author: crush
 * @Date: 2021-09-08 21:14
 * version 1.0
 */
@Slf4j
public class EmailCodeAuthenticationProvider implements AuthenticationProvider {

    ITbUserService userService;

    public EmailCodeAuthenticationProvider(ITbUserService userService) {
        this.userService = userService;
    }


    /** * Authentication */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        if(! supports(authentication.getClass())) {return null;
        }
        log.info("EmailCodeAuthentication authentication request: %s", authentication);
        EmailCodeAuthenticationToken token = (EmailCodeAuthenticationToken) authentication;

        UserDetails user = userService.getByEmail((String) token.getPrincipal());

        System.out.println(token.getPrincipal());
        if (user == null) {
            throw new InternalAuthenticationServiceException("Unable to obtain user information");
        }
        System.out.println(user.getAuthorities());
        EmailCodeAuthenticationToken result =
                new EmailCodeAuthenticationToken(user, user.getAuthorities());
                /* Details contains attributes like IP address, sessionId, etc. */
        result.setDetails(token.getDetails());
        return result;
    }

    @Override
    public boolean supports(Class
        aClass) {
        returnEmailCodeAuthenticationToken.class.isAssignableFrom(aClass); }}Copy the code

πŸ‘¨πŸ’» Perform the configuration in the configuration class

Basically do the following things:

  1. Inject filters and authenticators intospringIn the
  2. The logon success handler and logon failure handler are injected intoSpring, or handle login successes and failures in custom filters.
  3. Add to the filter chain
    @Bean
    public EmailCodeAuthenticationFilter emailCodeAuthenticationFilter(a) {
        EmailCodeAuthenticationFilter emailCodeAuthenticationFilter = new EmailCodeAuthenticationFilter();
        emailCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
        emailCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
        return emailCodeAuthenticationFilter;
    }

    @Bean
    public EmailCodeAuthenticationProvider emailCodeAuthenticationProvider(a) {
        return new EmailCodeAuthenticationProvider(userService);
    }

    /** * Use BCryptPasswordEncoder to encrypt the password, so use BCryptPasswordEncoder to authenticate the password@param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
        //authenticationProvider Adds the identity authenticationProvider based on the custom authenticationProvider passed in.Auth. AuthenticationProvider (emailCodeAuthenticationProvider ()); }Copy the code
.and()
    .authenticationProvider(emailCodeAuthenticationProvider())
    .addFilterBefore(emailCodeAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class)

    .authenticationProvider(mobileCodeAuthenticationProvider())
    .addFilterBefore(mobileCodeAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class)
Copy the code

πŸ§™β™‚οΈ test and source code

The specific configuration, startup mode, environment, etc., of the project are explained in details on github and Gitee documents.

The source code contains SQL files, configuration files, and links to related blogs. The source code is also annotated to make it as clear as possible.

To ensure that everyone can run and test correctly to the greatest extent possible.

Source: gitee ws-security

πŸ˜€ talk to yourself

If you don’t quite understand the content of this article, you can read my other article first:

SpringBoot integrates Security for Security control and uses Jwt to make tokens.

It should be easier to come back to this article later.

I really want to teach people, not just to scribble, but mostly to get the kind of joy that comes from success, which makes people feel good.

That’s all for today’s article.

Hello, THIS is blogger Ning Zaichun: homepage

If you encounter doubts in the article, please leave a message or private letter, or add the homepage contact information, will reply as soon as possible.

If you find any problems in the article, please correct them. Thank you very much.

If you think it will help you, please click “like” before leaving!