Spring Boot Note 6 - SMS verification code login for user authentication and authorization

Keywords: Spring Boot Spring Security

In the previous article, spring security, oauth2 and JWT were used to realize the most commonly used account and password login function. However, there are at least two login methods in the current external online system, and the most commonly used is the SMS verification code. This method has many advantages, such as naturally knowing the user's mobile phone number, Now let's use the authentication method of custom spring security to realize the SMS verification code login function.

Functional logic

1. The user obtains the SMS verification code through the mobile phone
2. The user fills in the verification code and submits it for login
3. The system judges whether the user's verification code is correct. If it is correct, the login is successful, and if it fails, an error will be prompted

The above three points are the basic process judgment of using SMS verification code to log in. Of course, in the actual process, comprehensive judgment needs to be made at each step. For example, before issuing the verification code, it is necessary to judge whether the mobile phone number exists, whether the SMS verification code has been sent in a short time, and whether the SMS verification code has expired. This is just to explain how to customize a login method, At the business level, I won't talk about it in detail, but just make a bottom-level custom SMS verification code login architecture.

database

SMS verification code table (sms_code)


Generate the corresponding control layer, service layer and persistence layer files. You can refer to the previous articles.

User defined verification code login

The custom login method is actually to simulate the whole process of spring security default account password login.
1, First, we have to have a login entry, that is, customize a similar "loadUserByUsername(username)" method. Because the login of the account password cannot be removed, we have to customize an interface and inherit the interface of "UserDetailsService"

/**
 * Custom login user service interface
 *
 * @author huangm
 * @since 2021 September 23
 */
public interface HnUserDetailsService extends UserDetailsService {

	/**
	 * Mobile authentication code login
	 *
	 * @author huangm
	 * @date 2021 September 23
	 * @param phone
	 *            cell-phone number
	 * @return
	 * @throws UsernameNotFoundException
	 */
	default CurrentLoginUser loadUserByPhone(final String phone) throws UsernameNotFoundException {
		return null;
	}
}

The method loadUserByPhone (String phone) in the interface is the entry method for SMS verification code login.

Two. Simulate the entrance path of landing.
The login path submitted by the front end is fixed to sms/login by default, and is submitted by POST. Here, you need to define an authentication filter for SMS verification code login. That is, simulate "UsernamePasswordAuthenticationFilter"

/**
 * The authentication filter for mobile phone verification code login is implemented by imitating UsernamePasswordAuthenticationFilter
 *
 * @author huangm
 * @since 2021 June 7
 */
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

	/**
	 * form Field name of mobile phone number in the form
	 */
	public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "phone";

	private String mobileParameter = SmsCodeAuthenticationFilter.SPRING_SECURITY_FORM_MOBILE_KEY;
	/**
	 * POST only
	 */
	private boolean postOnly = true;

	public SmsCodeAuthenticationFilter(final AuthenticationManager authManager,
			final AuthenticationSuccessHandler successHandler,
			final AuthenticationFailureHandler failureHandler,
			final ApplicationEventPublisher eventPublisher) {
		// SMS login request / sms/login in post mode
		super(new AntPathRequestMatcher("/sms/login", "POST"));
		this.setAuthenticationManager(authManager);
		this.setAuthenticationSuccessHandler(successHandler);
		this.setAuthenticationFailureHandler(failureHandler);
		this.setApplicationEventPublisher(eventPublisher);
	}

	@Override
	public Authentication attemptAuthentication(
			final HttpServletRequest request,
			final HttpServletResponse response) throws AuthenticationException {
		if (this.postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		String mobile = this.obtainMobile(request);
		if (mobile == null) {
			mobile = "";
		}

		mobile = mobile.trim();
		System.out.println("mobile 0000****" + mobile);
		SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

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

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

	protected String obtainMobile(final HttpServletRequest request) {
		return request.getParameter(this.mobileParameter);
	}

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

	public String getMobileParameter() {
		return this.mobileParameter;
	}

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

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

3, User defined TOKEN for placing authentication information

 /**
 * Log in to the AuthenticationToken with the mobile phone verification code and imitate the UsernamePasswordAuthenticationToken to achieve < br >
 * Place authentication information
 *
 * @author huangm
 * @since 2021 September 23
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	/**
	 * In UsernamePasswordAuthenticationToken, this field represents the login user name, and here it represents the login mobile phone number
	 */
	private final Object principal;

	/**
	 * Build a SmsCodeAuthenticationToken without authentication
	 */
	public SmsCodeAuthenticationToken(final Object principal) {
		super(null);
		this.principal = principal;
		this.setAuthenticated(false);
	}

	/**
	 * Build SmsCodeAuthenticationToken with authentication
	 */
	public SmsCodeAuthenticationToken(final Object principal,
			final Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		// must use super, as we override
		// Has it been certified
		super.setAuthenticated(true);
	}

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

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

	@Override
	public void setAuthenticated(final 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() {
		super.eraseCredentials();
	}
}

4, Unified judgment of verification code (authentication Provider)
This user-defined authentication Provider determines whether the authentication code submitted by the user is consistent with the valid authentication code in the database. If it is consistent, it indicates that the login is successful, otherwise an error message is returned.

/**
 * The mobile phone verification code logs in to the authentication Provider, and the AuthenticationProvider interface is required
 *
 * @author huangm
 * @since 2021 June 7
 */
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

	private HnUserDetailsService userDetailsService;

	@Override
	public Authentication authenticate(final Authentication authentication) throws AuthenticationException {
		final SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
		final String phone = (String) authenticationToken.getPrincipal();
		final CurrentLoginUser userDetails = this.userDetailsService.loadUserByPhone(phone);
		// Verify whether the mobile phone verification code is correct
		this.checkSmsCode(userDetails);
		// At this time, after the authentication is successful, a new authenticationResult with authentication should be returned
		final SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails,
				userDetails.getAuthorities());
		authenticationResult.setDetails(authenticationToken.getDetails());
		return authenticationResult;
	}

	private void checkSmsCode(final CurrentLoginUser userDetails) {
		final HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
				.getRequest();
		final String inputCode = request.getParameter("captcha");
		if (HnStringUtils.isBlank(inputCode)) {
			throw new BadCredentialsException("Verification code not detected");
		}
		final Map<String, Object> smsCodeMap = userDetails.getParamsMap();
		if (smsCodeMap == null) {
			throw new BadCredentialsException("Application verification code not detected");
		}
		final String smsCode = String.valueOf(smsCodeMap.get("captcha"));
		if (HnStringUtils.isBlankOrNULL(smsCode)) {
			throw new BadCredentialsException("Application verification code not detected");
		}
		final int code = Integer.valueOf(smsCode);
		if (code != Integer.parseInt(inputCode)) {
			throw new BadCredentialsException("Verification code error");
		}
	}

	@Override
	public boolean supports(final Class<?> authentication) {
		// Determine whether authentication is a subclass or sub interface of SmsCodeAuthenticationToken
		return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
	}

	public UserDetailsService getUserDetailsService() {
		return this.userDetailsService;
	}

	public void setUserDetailsService(final HnUserDetailsService userDetailsService) {
		this.userDetailsService = userDetailsService;
	}
}

The core code for judging the verification code is the checkSmsCode (loginUser) method, which needs to be analyzed together with the implementation of loadUserByPhone (phone). We will talk about this later when we write the login logic.

5, Successful login failed Handler
You can log in successfully with the same account password. You don't need to fill in, just fill in the failed login. That is, add the code for processing after SMS failure to SecurityHandlerConfig.java in the previous article.

/**
	 * SMS verification code login failed
	 *
	 * @return
	 */
	@Bean
	public AuthenticationFailureHandler smsCodeLoginFailureHandler() {
		return new AuthenticationFailureHandler() {

			@Override
			public void onAuthenticationFailure(
					final HttpServletRequest request,
					final HttpServletResponse response,
					final AuthenticationException exception) throws IOException, ServletException {
				String code = null;
				if (exception instanceof BadCredentialsException) {
					code = "smsCodeError";// Mobile phone verification code error
				} else {
					code = exception.getMessage();
				}
				System.out.println("******sms fail**** " + code);
				HnResponseUtils.responseJson(response, HttpStatus.OK.value(), new ErrorResponse(code));

			}
		};
	}

Modify the security configuration file

If the login of SMS verification code is added, the Security configuration file must be processed accordingly.
SecurityConfig.java :
1. To modify userDetailsService, add the verification code login HnUserDetailsService above
2. Interceptor processing for successful and failed login of verification code

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	HnUserDetailsService userDetailsService;
	@Autowired
	private AuthenticationSuccessHandler loginSuccessHandler;
	@Autowired
	private AuthenticationFailureHandler loginFailureHandler;
	@Autowired
	private AuthenticationEntryPoint authenticationEntryPoint;
	@Autowired
	private AuthenticationFailureHandler smsCodeLoginFailureHandler;
	@Autowired
	private LogoutSuccessHandler logoutSuccessHandler;
	@Autowired
	private TokenFilter tokenFilter;
	@Resource
	private ApplicationEventPublisher applicationEventPublisher;
	/**
	 * Landing page
	 */
	@Value("${login.loginHTML}")
	private String loginHtml;
	/**
	 * Log in to the background processing page
	 */
	@Value("${login.loginProcessingUrl}")
	private String loginProcessingUrl;
	/**
	 * Pages not blocked
	 */
	@Value("${login.permitAllUrl}")
	private String permitAllUrl;
	/**
	 * Password field name, default password
	 */
	@Value("${login.passwordParameter}")
	private String passwordParameter;

	public String[] getPermitAllUrl() {
		final String str = this.permitAllUrl.replaceAll(" ", "");
		return str.split(",");
	}

	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	@Override
	protected void configure(final HttpSecurity http) throws Exception {
		http.csrf().disable();
		if (HnStringUtils.isBlank(this.passwordParameter)) {
			this.passwordParameter = "password";
		}

		// Based on token, so session is not required
		http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

		http.authorizeRequests().antMatchers(this.getPermitAllUrl()).permitAll().anyRequest().authenticated();
		http.formLogin().loginPage(this.loginHtml).loginProcessingUrl(this.loginProcessingUrl)
				.passwordParameter(this.passwordParameter).successHandler(this.loginSuccessHandler)
				.failureHandler(this.loginFailureHandler).and().exceptionHandling()
				.authenticationEntryPoint(this.authenticationEntryPoint).and().logout().logoutUrl("/logout")
				.logoutSuccessHandler(this.logoutSuccessHandler);
		// Solve the problem that display in iframe is not allowed
		// http.headers().frameOptions().disable();
		// http.headers().cacheControl();

		http.addFilterBefore(this.tokenFilter, UsernamePasswordAuthenticationFilter.class);

		http.addFilterBefore(
				new SmsCodeAuthenticationFilter(this.authenticationManager(), this.loginSuccessHandler,
						this.smsCodeLoginFailureHandler, this.applicationEventPublisher),
				UsernamePasswordAuthenticationFilter.class);
	}

	@Override
	protected void configure(final AuthenticationManagerBuilder auth) throws Exception {		auth.userDetailsService(this.userDetailsService).passwordEncoder(this.passwordEncoder());
		// User defined SMS login identity authentication component
		final SmsCodeAuthenticationProvider smsProvider = new SmsCodeAuthenticationProvider();
		smsProvider.setUserDetailsService(this.userDetailsService);
		auth.authenticationProvider(smsProvider);		
	}
}

Simulated generation of SMS verification code

I'll simply simulate the generation of SMS verification code here, so I won't do the function of sending verification code.

@RestController
@RequestMapping("sms")
public class SysSmsCodeController {

	@Autowired
	ISysSmsCodeService sysSmsCodeService;

	@PostMapping("createCode")
	public AjaxResponse createCode(final String phone, final String codeType) {
		final String captcha = String.valueOf((int) Math.ceil(Math.random() * 9000 + 1000));
		final Date date = new Date();
		final String expireTime = HnDateUtils.add(date, Calendar.MINUTE, 3, "yyyy-MM-dd HH:mm:ss", Locale.CHINA);
		// if (true) {
		// throw new HnException("phoneNonExist");
		// }
		final SysSmsCode smsCode = new SysSmsCode();
		smsCode.setId(HnIdUtils.getNewId());
		smsCode.setPhone(phone);
		smsCode.setCodeType(codeType);//Type of verification code, such as login, password retrieval, etc
		smsCode.setCaptcha(captcha);//Verification Code
		smsCode.setExpireTime(expireTime);//3-minute validity period
		smsCode.setCreateDateTime(HnDateUtils.format(date, "yyyy-MM-dd HH:mm:ss"));
		smsCode.setCreateTimeMillis(System.currentTimeMillis());
		this.sysSmsCodeService.save(smsCode);
		System.out.println(HnStringUtils.formatString("{0}: by {1} Set SMS verification code:{2}", smsCode.getId(), phone, captcha));
		return new SucceedResponse("Verification code generated successfully!");
	}
}

Post run results

The data sheet is as follows:

A valid verification code is generated for this mobile phone number, which is valid for 3 minutes.

Verification code login logic implementation

Modify the UserDetailsServiceImpl class to implement the above custom interface HnUserDetailsService, and then add a verification code login method,

@Override
	public CurrentLoginUser loadUserByPhone(final String phone) throws UsernameNotFoundException {	
		final QueryWrapper<SysUser> wrapper = new QueryWrapper<SysUser>();
		wrapper.eq("phone", phone);
		final SysUser user = this.sysUserService.getOne(wrapper);
		if (user == null) {
			// "Mobile number does not exist"
			throw new UsernameNotFoundException("phoneNonExist");
		}
		// Obtain the current valid verification code of the current mobile phone number for framework verification and write it to the current user's paramsMap
		final String datetimeStr = HnDateUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss");

		final QueryWrapper<SysSmsCode> smsCodeWrapper = new QueryWrapper<SysSmsCode>();
		smsCodeWrapper.eq("phone", phone);
		smsCodeWrapper.eq("code_type", "login");
		smsCodeWrapper.ge("expire_time", datetimeStr);
		SysSmsCode sysSmsCode = null;
		final CurrentLoginUser loginUser = this.checkUser(user);
		try {
			sysSmsCode = sysSmsCodeService.getOne(smsCodeWrapper);
			loginUser.addAttribute("captcha", sysSmsCode.getCaptcha());// Write the currently valid SMS verification code to
		} catch (final Exception e) {
			// Verification code has expired
			throw new UsernameNotFoundException("codeExpireTime");
		}
		return loginUser;
	}

Note: loginUser.addAttribute("captcha", sysSmsCode.getCaptcha()) here; That is, the valid verification code in the current database is written into the paramsMap of the login user object
Final map < string, Object > smscodemap = userdetails. Getparamsmap() in the method checkSmsCode() in the verification code authentication Provider; This sentence obtains the verification code written here, and then compares it with the verification code submitted by the front-end user to determine whether it is consistent.
The effect is shown in the figure below: the verification code is invalid

Phone number does not exist:

Verification code error:

Login succeeded:

The successful effect is the same as the successful login of account and password. How can we achieve the login effect of two ways.

PS: to generate SMS verification code and log in SMS verification code, permission judgment needs to be removed, so this path should be added to the configuration file yml

Posted by kingsol on Thu, 23 Sep 2021 07:28:52 -0700