[Spring Security + OAuth2 + JWT entry to actual combat] 11. SMS verification code login

brief introduction

Using SMS authentication code to log in, security does not support SMS authentication code login process by default, so we imitate password login to do a new authentication process.

Login process

  • SmsCodeAuthenticationFilter SMS login request
  • SmsCodeAuthenticationProvider provides an implementation class for SMS login processing
  • SmsCodeAuthenticationToken stores authentication information (including parameter information transfer before authentication)
  • Finally, a filter is developed to verify the SMS verification code before the SMS login request

Because this filter only cares about whether the submitted verification code is normal. Therefore, it can be applied to any business to verify the short message submitted by any business

UsernamePasswordAuthenticationToken

The default password of security is to log in. On this basis, clear the password related code

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.security.authentication;

import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 520L;
    private final Object principal;
    private Object credentials;

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

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

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

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

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

    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}

SmsCodeAuthenticationToken

The core project creates the SmsCodeAuthenticationToken class

Copy all contents of UsernamePasswordAuthenticationToken class to SmsCodeAuthenticationToken class

Delete the code related to the credentials value, and remove the credentials field because the SMS authentication has been filtered before the authorization authentication

package com.spring.security.authentication.mobile;

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

import java.util.Collection;

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 520L;
    //Mobile phone number should be put before authentication, and authenticated user should be put after authentication
    private final Object principal;

    public SmsCodeAuthenticationToken(String mobile) {
        super((Collection)null);
        this.principal = mobile;
        //Put your phone number before you log in
        this.setAuthenticated(false);
    }

    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        //Log in successfully put user information
        super.setAuthenticated(true);
    }

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

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

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

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}

 

UsernamePasswordAuthenticationFilter

Write SmsCodeAuthenticationFilter and agree to refer to the security default interceptor

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.security.web.authentication;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;

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 String usernameParameter = "username";
    private String passwordParameter = "password";
    private boolean postOnly = true;

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

    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());
        } else {
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }

            username = username.trim();
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.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));
    }

    public void setUsernameParameter(String usernameParameter) {
        Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
        this.usernameParameter = usernameParameter;
    }

    public void setPasswordParameter(String passwordParameter) {
        Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
        this.passwordParameter = passwordParameter;
    }

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

    public final String getUsernameParameter() {
        return this.usernameParameter;
    }

    public final String getPasswordParameter() {
        return this.passwordParameter;
    }
}

SmsCodeAuthenticationFilter

Also copy the UsernamePasswordAuthenticationFilter code

package com.spring.security.authentication.mobile;

import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    //Define parameter name of carrying mobile number
    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";

    private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;

    private boolean postOnly = true;

    public SmsCodeAuthenticationFilter() {
        //Request connections to process
        //Login page form form action < form action = "/ authentication / mobile" method = "post" >
        super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
    }

    @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());
        } else {
            //Get mobile number
            String mobile = this.obtainMobile(request);
            if (mobile == null) {
                mobile = "";
            }
            //Remove spaces
            mobile = mobile.trim();
            SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
            // copy some information in the request to the token
            // Later, when the authentication succeeds, you need to copy the information to the new token
            this.setDetails(request, authRequest);
            //Authentication completed
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    /**
     * Get mobile number
     *
     * @param request
     * @return
     */
    @Nullable
    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(this.mobileParameter);
    }

    /**
     * Set authentication information to request header
     *
     * @param request
     * @param authRequest
     */
    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(this.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() {
        return this.mobileParameter;
    }
}

SmsCodeAuthenticationProvider

There is no imitation of this. No provider found with usernamePassword type

package com.spring.security.authentication.mobile;

import org.springframework.beans.factory.annotation.Autowired;
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;

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UserDetailsService userDetailsService;
    /**
     * Authentication logic
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
        //Get user information according to mobile phone number
        UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());

        if (user == null) {
            throw new InternalAuthenticationServiceException("Unable to get user information");
        }

        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user,user.getAuthorities());
        //Copy unauthenticated information to authenticated
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

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

smsCodeFilter

Directly copy the ValidateCodeFilter class and change its name to: smsCodeFilter

package com.spring.security.validate.code;

import com.spring.security.properties.SecurityProperties;
import com.spring.security.validate.code.image.ImageCode;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

/**
 * SMS verification code filter
 */
@Component
public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean {
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Autowired
    private SecurityProperties securityProperties;

    private Set<String> urls = new HashSet<>();


    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        String[] configUrls = StringUtils.splitPreserveAllTokens(securityProperties.getCode().getSms().getUrl(), ",");
        for (String configUrl : configUrls) {
            urls.add(configUrl);
        }
        //Add fixed submission address
        urls.add("/authentication/mobile");
    }

    /**
     * Inside the filter
     *
     * @param httpServletRequest  http Servlet request
     * @param httpServletResponse
     * @param filterChain         Filter chain
     * @throws ServletException Servlet abnormal
     * @throws IOException      IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        boolean action = false;
        for (String url : urls) {
            if (StringUtils.startsWithIgnoreCase(url, httpServletRequest.getRequestURI())
                    && StringUtils.startsWithIgnoreCase(httpServletRequest.getMethod(), "post")) {
                action = true;
            }
        }

        if (action) {
            try {
                validateCode(new ServletWebRequest(httpServletRequest));
            } catch (ValidateCodeException e) {
                authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
                return;
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);

    }

    /**
     * Validation code
     *
     * @param servletWebRequest servlet Web request
     * @throws ServletRequestBindingException Servlet Request binding exception
     */
    private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {
        ValidateCode validateSession = (ValidateCode) sessionStrategy.getAttribute(servletWebRequest, "SESSION_KEY_FOR_CODE_SMS");
        String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "smsCode");

        if (StringUtils.isEmpty(codeInRequest)) {
            throw new ValidateCodeException("Verification code cannot be empty!");
        }
        if (validateSession == null) {
            throw new ValidateCodeException("Verification code does not exist!");
        }
        if (validateSession.isExpire()) {
            sessionStrategy.removeAttribute(servletWebRequest, "SESSION_KEY_FOR_CODE_SMS");
            throw new ValidateCodeException("Verification code expired!");
        }
        if (!StringUtils.startsWithIgnoreCase(validateSession.getCode(), codeInRequest)) {
            throw new ValidateCodeException("The verification code is incorrect!");
        }
        sessionStrategy.removeAttribute(servletWebRequest, "SESSION_KEY_FOR_CODE_SMS");
    }
}

SmsCodeAuthenticationSecurityConfig

A few things are ready for you. Here we need to configure to add these to the authentication process of security;

package com.spring.security.authentication.mobile;

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 AuthenticationFailureHandler hkAuthenticationFailureHandler;
    @Autowired
    private AuthenticationSuccessHandler hkAuthenticationSuccessHandler;
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        SmsCodeAuthenticationFilter filter = new SmsCodeAuthenticationFilter();
        // Give the filter to the manager
        // The process in the figure, because the first SMS authentication filter (not the verification code, just the authentication)
        // To use the manager to get the provider, register the manager
        filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        //Authentication failure processor
        filter.setAuthenticationFailureHandler(hkAuthenticationFailureHandler);
        //Authentication successful processor
        filter.setAuthenticationSuccessHandler(hkAuthenticationSuccessHandler);

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
        //Add to security authentication process
        http
                // Register to AuthenticationManager
                .authenticationProvider(smsCodeAuthenticationProvider)
                // After adding to UsernamePasswordAuthenticationFilter
                // It seems that all the entries are UsernamePasswordAuthenticationFilter
                // Then the provider of UsernamePasswordAuthenticationFilter does not support the request for this address
                // So it will fall on our own certified filter. Complete the next certification
                .addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);
    }
}

BrowserSecurityConfig application side configuration

package com.spring.security;

import com.spring.security.authentication.HkAuthenticationFailureHandler;
import com.spring.security.authentication.HkAuthenticationSuccessHandler;
import com.spring.security.authentication.mobile.SmsCodeAuthenticationSecurityConfig;
import com.spring.security.properties.SecurityProperties;
import com.spring.security.validate.code.SmsCodeFilter;
import com.spring.security.validate.code.ValidateCodeFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private HkAuthenticationSuccessHandler hkAuthenticationSuccessHandler;

    @Autowired
    private HkAuthenticationFailureHandler hkAuthenticationFailureHandler;

    @Autowired
    private ValidateCodeFilter validateCodeFilter;

    @Autowired
    private SmsCodeFilter smsCodeFilter;

    // A data source is information that needs to be configured where it is used
    @Autowired
    private DataSource dataSource;

    // MyUserDetailsService previously written
    @Autowired
    private UserDetailsService userDetailsService;

    //SmsCodeAuthenticationSecurityConfig joins the authentication process
    @Autowired
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;

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

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        // org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer.tokenRepository
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        // There are statements defining the creation of tables in this object
        //create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)
        // You can set this class to create tables
        // However, this function is only used once. If the database already has tables, an error will be reported
        //jdbcTokenRepository.setCreateTableOnStartup(true);
        //One of two methods
        return jdbcTokenRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //To configure
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // Add verification code verification filter
                // SMS verification code verification filter
                .addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                .loginPage("/authentication/require")//Login page path
                // Process login request path
                .loginProcessingUrl("/authentication/form")
                .successHandler(hkAuthenticationSuccessHandler) // Login processed successfully
                .failureHandler(hkAuthenticationFailureHandler) // Failed to process login
                .and()
                // From here on, configure and remember my function
                .rememberMe()
                .tokenRepository(persistentTokenRepository())
                // New expired configuration, unit: Second
                .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
                // userDetailsService is required. Otherwise, it's wrong
                .userDetailsService(userDetailsService)
                .and()
                .authorizeRequests() // Authorized configuration
                //Path without authentication
                .antMatchers("/authentication/require", "/code/*", "/signIn.html", securityProperties.getBrowser().getLoginPage(), "/failure").permitAll()
                .anyRequest() // All requests
                .authenticated() // All need certification
                .and().csrf().disable()
                //Join SMS verification code authentication process
                .apply(smsCodeAuthenticationSecurityConfig);
    }
}

Testing

Launch project access: http://127.0.0.1:8080/signIn.html

Get verification code:

Enter error verification code:

Enter the correct verification code:

Keywords: Programming Mobile Spring Java IntelliJ IDEA

Added by afhouston on Thu, 05 Mar 2020 07:26:56 +0200