The default form-based authentication mode in Spring Security is password account login authentication. Therefore, Spring Security does not have a ready-made interface for SMS verification code + login, so you need to encapsulate a similar authentication filter and authentication processor to achieve SMS authentication.

SMS verification code authentication

Captcha object class design

Like the image verification code, you need to encapsulate a verification code object, which is used to generate the mobile phone verification code and send it to the mobile phone. This class contains only the verification code and the expiration time. The short message verification code can be used directly with this class:

import java.time.LocalDateTime;

import lombok.Data;

@Data
public class ValidateCode {

    private String code;

    private LocalDateTime expireTime;

    public ValidateCode(String code, int expireIn){
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
    }
    
    public boolean isExpried(a) {
        return LocalDateTime.now().isAfter(getExpireTime());
    }

	public ValidateCode(String code, LocalDateTime expireTime) {
		super(a);this.code = code;
		this.expireTime = expireTime; }}Copy the code

Image captcha inherits this class:

import java.awt.image.BufferedImage;
import java.time.LocalDateTime;

import org.woodwhales.king.validate.code.ValidateCode;

import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper=false)
public class ImageCode extends ValidateCode {

    private BufferedImage image;

    public ImageCode(BufferedImage image, String code, int expireId) {
        super(code, LocalDateTime.now().plusSeconds(expireId));
        this.image = image;
    }

    public ImageCode(BufferedImage image, String code, LocalDateTime localDateTime) {
        super(code, localDateTime);
        this.image = image; }}Copy the code

Captcha generation class design

Since both picture and SMS classes can generate corresponding verification codes, a verification code generation interface is designed directly, and the specific implementation classes are implemented according to services:

import org.springframework.web.context.request.ServletWebRequest;

public interface ValidateCodeGenerator {

	ValidateCode generate(ServletWebRequest request);

}
Copy the code

The pass-through is designed so that a ServletWebRequest can perform different business implementations based on the parameters in the front-end request

Currently only picture generator and captcha generator are implemented:

// Image captcha generator
@Component("imageCodeGenerator")
public class ImageCodeGenerator implements ValidateCodeGenerator {

    /** * Generate graphic captcha *@param request
     * @return* /
	@Override
    public ValidateCode generate(ServletWebRequest request) {...return newImageCode(image, sRand, SecurityConstants.EXPIRE_SECOND); }}// SMS verification code generator
@Component("smsCodeGenerator")
public class SmsCodeGenerator implements ValidateCodeGenerator {

	@Override
	public ValidateCode generate(ServletWebRequest request) {
		String code = RandomStringUtils.randomNumeric(SecurityConstants.SMS_RANDOM_SIZE);
        return newValidateCode(code, SecurityConstants.SMS_EXPIRE_SECOND); }}Copy the code

Design the interface for sending SMS verification codes

After the SMS verification code is generated, you need to design interfaces that rely on the SMS service provider to send the verification code. Therefore, at least one unified interface is designed for the SMS service provider to generate and send the SMS service.

public interface SmsCodeSender {
	//  You need at least a cell phone number and a verification code
	void send(String mobile, String code);

}
Copy the code

To demonstrate, design a virtual default SMS sender that prints only one line of log in the log file:

import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;

/** * SMS sending simulation *@author Administrator
 *
 */
@Slf4j
@Service
public class DefaultSmsCodeSender implements SmsCodeSender {

    @Override
    public void send(String mobile, String code) {
    	log.debug("Send to mobile: {}, code: {}", mobile, code); }}Copy the code

SMS verification code request Controller

All captecodecontroller requests are in the same ValidateCodeController, which incorporates two validatecodeGenerators. You can use Spring’s dependent lookup/search techniques to reconstruct the code, and all requests can be configured dynamically. Here is the temporary full hardCode in the code:

import java.io.IOException;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import org.woodwhales.king.core.commons.SecurityConstants;
import org.woodwhales.king.validate.code.ValidateCode;
import org.woodwhales.king.validate.code.ValidateCodeGenerator;
import org.woodwhales.king.validate.code.image.ImageCode;
import org.woodwhales.king.validate.code.sms.DefaultSmsCodeSender;

@RestController
public class ValidateCodeController {
	
	@Autowired
	private SessionStrategy sessionStrategy;

	@Autowired
	private ValidateCodeGenerator imageCodeGenerator;
	
	@Autowired
	private ValidateCodeGenerator smsCodeGenerator;
	
	@Autowired
	private DefaultSmsCodeSender defaultSmsCodeSender;
	
	@GetMapping("code/image")
    public void createImageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
		ImageCode imageCode = (ImageCode)imageCodeGenerator.generate(new ServletWebRequest(request));
		sessionStrategy.setAttribute(new ServletWebRequest(request), SecurityConstants.SESSION_KEY, imageCode);
		ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
    }
	
	@GetMapping("code/sms")
    public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException {
		ValidateCode smsCode = smsCodeGenerator.generate(new ServletWebRequest(request));
		sessionStrategy.setAttribute(new ServletWebRequest(request), SecurityConstants.SESSION_KEY, smsCode);
		String mobile = ServletRequestUtils.getStringParameter(request, "mobile"); defaultSmsCodeSender.send(mobile, smsCode.getCode()); }}Copy the code

It can be seen from the above code that the request logic of image verification code and SMS verification code is similar: first, the verification code generation interface is called to generate the verification code, then the verification code is put into the session, and finally the verification code is returned to the front end or the user. Therefore, this routine process can be abstracted into a template method to enhance the maintainability and extensibility of the code.

Use a diagram to represent the structure of the refactored code:

Random verification code filter design

Since both pictures and mobile phones can generate verification codes, random verification codes can be sent by email for login verification. Therefore, the authentication of random verification codes can be independently encapsulated in a random verification code filter. And this filter is at the front of the entire Spring Security filter chain (it is the first authentication wall).

Random captcha filters inherit OncePerRequestFilter from the Spring framework to ensure that the filter is only called once when a request comes in. See the source code at the end of the article for detailed code implementation.

Highlighted here to explain how the random authentication code filters configured at the forefront spring security filters certification, need to rewrite SecurityConfigurerAdapter configure () method, And put the custom filters in AbstractPreAuthenticatedProcessingFilter before the filter can be:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.stereotype.Component;

import javax.servlet.Filter;


@Component
public class ValidateCodeSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain.HttpSecurity> {

    @Autowired
    private Filter validateCodeFilter;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        super.configure(http); http.addFilterBefore(validateCodeFilter, AbstractPreAuthenticatedProcessingFilter.class); }}Copy the code

SMS verification code authentication

Before customizing the SMS login authentication process, it is suggested that you can move to the previous article: SpringBoot + Spring Security learning Notes (two) Security authentication process source code, understand the authentication process to clear the user password to more easily understand the following classic flow chart:

On the left is the user + password authentication process. The whole process is authenticated by the user name + password authentication filter, and the request is encapsulated as token and injected into the AutheticationMananger, which is then verified by the default authentication validator. During the verification, the UserDetailsService interface is called to verify the token. After the verification succeeds, the authenticated token is put into the SecurityContextHolder.

Similarly, because of the message log method only need to use random authentication code check without the need for a password function, when the check is that a user authentication success after success, and therefore need to be modeled on the left side of the process to develop a custom message login authentication token, the token you just need to store a phone number, in the process of token validation, Instead of using the default validator, you need to develop your own validator to verify the current user-defined token and configure the customized filters and validators in the Spring Security framework.

Note: the message random authentication code verification process is completed before SmsCodeAuthticationFIlter.

SMS login authentication Token

Imitation UsernamePasswordAuthenticationToken to design a belongs to the SMS authentication authentication token object, why want to customize a message authentication token, spring security framework not only provides the user name + password authentication way, The ultimate determinant of user authentication is whether there is an AuthenticationToken in the SecurityContextHolder. Therefore, design an authentication object and set it to the SecurityContextHolder when the authentication succeeds.

import java.util.Collection;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private final Object principal;

	public SmsCodeAuthenticationToken(Object mobile) {
		super(null);
		this.principal = mobile;
		setAuthenticated(false);
	}

	public SmsCodeAuthenticationToken(Object mobile, Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = mobile;
		super.setAuthenticated(true); // must use super, as we override
	}

	public Object getPrincipal(a) {
		return this.principal;
	}
	
	public Object getCredentials(a) {
		return null;
	}

	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");
		}

		super.setAuthenticated(false);
	}

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

As you can see from the AuthenticationToken interface, we now have our own tokens for SMS login in the framework:

SMS login authentication filter

Message authentication code filter design by the same token, the imitation UsernamePasswordAuthenticationFilter filters, here again, SMS random authentication code

import java.util.Objects;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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.util.Assert;
import org.woodwhales.core.constants.SecurityConstants;

public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

	/** * The parameters in the request */
	private String mobileParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_MOBILE;
	
	private boolean postOnly = true;

	public SmsCodeAuthenticationFilter(a) {
        super(new AntPathRequestMatcher(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE, "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());
		}

		// Get the parameter values in the request
		String mobile = obtainMobile(request);

		if (Objects.isNull(mobile)) {
			mobile = "";
		}

		mobile = mobile.trim();
		
		SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);

		return this.getAuthenticationManager().authenticate(authRequest);
	}

	/** * Obtain the mobile phone number */
	protected String obtainMobile(HttpServletRequest request) {
		return request.getParameter(mobileParameter);
	}

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

	public void setMobileParameter(String mobileParameter) {
		Assert.hasText(mobileParameter, "Mobile parameter must not be empty or null");
		this.mobileParameter = mobileParameter;
	}

	public void setPostOnly(boolean postOnly) {
		this.postOnly = postOnly;
	}

	public final String getMobileParameter(a) {
		returnmobileParameter; }}Copy the code

Message authentication code filters also serve as a subclass of the AbstractAuthenticationProcessingFilter, later need to register to the security configuration, let it become a link in a chain security authentication filter:

SMS login authentication validator

The function of the SMS-BASED login authentication validator is to verify authenticationToken by calling the loadUserByUsername() method of the UserDetailsService. The root interface of all validators is: AuthenticationProvider, so custom SMS login authentication validator implements this interface, override authenticate() :

import java.util.Objects;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
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.UserDetailsService;

import lombok.Data;

@Data
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

	private UserDetailsService userDetailsService;
	
	@Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;

        /** * call {@link UserDetailsService}
         */
        UserDetails user = userDetailsService.loadUserByUsername((String)authenticationToken.getPrincipal());

        if (Objects.isNull(user)) {
            throw new InternalAuthenticationServiceException("Unable to obtain user information");
        }

        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());

        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }

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

Notice that the @data annotation is used to generate setter and getter methods.

SMS login authentication security configuration design

Design an encapsulated SMS login authentication configuration class for external callers to call directly:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain.HttpSecurity> {

    @Autowired
    private AuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler myAuthenticationFailureHandler;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity http) throws Exception {

        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);

        // Get the verification code provider
        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);

        / / the message authentication code validator registration to HttpSecurity, and add a message authentication code filter before UsernamePasswordAuthenticationFilterhttp.authenticationProvider(smsCodeAuthenticationProvider).addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }}Copy the code

When an external want to refer to the enclosed good configuration, only need in custom AbstractChannelSecurityConfig security authentication configuration can be added, pay attention to this configuration object using the @ Component annotation, registered in the spring, So you can reference it directly via @autowired, as in:

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.stereotype.Component;
import org.woodwhales.core.authentication.sms.AbstractChannelSecurityConfig;
import org.woodwhales.core.authentication.sms.SmsCodeAuthenticationSecurityConfig;
import org.woodwhales.core.validate.code.config.ValidateCodeSecurityConfig;

@Component
public class BrowserSecurityConfig extends AbstractChannelSecurityConfig {

    @Autowired
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;

    @Autowired
    private ValidateCodeSecurityConfig validateCodeSecurityConfig;
    
    @Autowired
    protected AuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    protected AuthenticationFailureHandler authenticationFailureHandler;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private DataSource dataSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.formLogin()
            .loginPage("/authentication/require") // Login page callback
            .successHandler(authenticationSuccessHandler)// The authentication callback succeeded
            .failureHandler(authenticationFailureHandler)
        
	        // Verification configuration of the following verification codes
	        .and()
	        .apply(validateCodeSecurityConfig) 
	
	        // The configuration of SMS login authentication is as follows
	        .and()
	        .apply(smsCodeAuthenticationSecurityConfig)
	            
	        // Remember my configuration
	        .and()
	        .rememberMe()
	        .tokenRepository(persistentTokenRepository())
	        .tokenValiditySeconds(3600) // Set to remember my expiration time
	        .userDetailsService(userDetailsService)
	        
	        .and()
	        // Request authorization configuration
	        .authorizeRequests() 
	        // The following request paths do not require authentication
	        .antMatchers("/authentication/require"."/authentication/mobile"."/login"."/code/*"."/")
	        .permitAll() 
	        .anyRequest() // Any request
	        .authenticated() // All require authentication
            
            // Temporarily disables the defense cross-site request forgery function
            .and()
            .csrf().disable();
    }
    
    /** * Configure TokenRepository *@return* /
    @Bean
    public PersistentTokenRepository persistentTokenRepository(a) {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        Remember my database table, I recommend to see the source code to create directly
		// jdbcTokenRepository.setCreateTableOnStartup(true);
        returnjdbcTokenRepository; }}Copy the code

Some of the code in the configuration appears redundant configuration, can be all encapsulated into an abstract template, complete some basic configuration.

Project source: github.com/woodwhales/…

Reference source: github.com/imooc-java/…

Personal blog: Woodwhale’s Blog

Blogland: The blog of the wooden whale