Spring Science - SMS verification code login function

Keywords: Mobile Spring Session REST

Spring Science (5) -- SMS verification code login function

Previous articles on spring Science Series

1,Spring security (1) -- authentication + authorization code implementation

2,Spring security (2) --- remember my feature implementation

3,Spring Science (3) -- function implementation of graphic verification code

4,Spring Science (4) -- implementation of SMS verification code function

1, Principle analysis of SMS login verification mechanism

Before we understand the login mechanism of SMS verification code, we first need to understand the mechanism of user account password login. Let's briefly analyze how Spring Security verifies the login method based on user name and password,

After the analysis, think about how to integrate SMS login authentication into Spring Security.

1. Account password login process

General account password login has the function of graphics verification code and remembering me, so its general process is like this.

1. After entering the user name, account number and picture verification code, the user clicks log in. For spring science, the SMS verification code Filter will be entered first, because it will be configured in the
 Before UsernamePasswordAuthenticationFilter, verify the information of the current verification code with the verification code of the picture verification code with session.

2. After the SMS verification code is passed, enter the UsernamePasswordAuthenticationFilter, and construct a temporary unauthenticated one according to the entered user name and password information
 UsernamePasswordAuthenticationToken, and submit the UsernamePasswordAuthenticationToken to the AuthenticationManager for processing.

3. The AuthenticationManager does not do the authentication processing itself. It traverses for each to find an AuthenticationProvider that conforms to the current login mode, and gives it to perform the authentication processing
 , for user name and password login mode, this Provider is DaoAuthenticationProvider.

4. Carry out a series of validation processing in this Provider. If the validation passes, a UsernamePasswordAuthenticationToken with added authentication will be reconstructed, and the
 token is passed back to UsernamePasswordAuthenticationFilter.

5. In the parent class AbstractAuthenticationProcessingFilter of the Filter, it will jump to successHandler or failureHandler according to the result of the previous step.

flow chart

2. SMS verification code login process

Because SMS login is not integrated into Spring Security, we often need to develop our own SMS login logic and integrate it into Spring Security, so here we imitate the account

Password login to achieve SMS verification code login.

1. There is a UsernamePasswordAuthenticationFilter for user name and password login. We have a SmsAuthenticationFilter and paste the code to change it.
2. UsernamePasswordAuthenticationToken is required for user name and password login. We need to create a SmsAuthenticationToken and paste the code to change it.
3. The user name and password login require DaoAuthenticationProvider. We imitate it as also implements authenticationprovider, which is called SmsAuthenticationProvider.

This picture is found on the Internet. I don't want to draw it

After we make the above three classes ourselves, the effect we want to achieve is shown in the figure above. When we log in with SMS verification code:

1. After SmsAuthenticationFilter, construct a SmsAuthenticationToken without authentication, and then submit it to the authentication manager for processing.

2. The authentication manager selects a suitable provider for processing through for each. Of course, we hope this provider is SmsAuthenticationProvider.

3. After the validation, an authenticated SmsAuthenticationToken is reconstructed and returned to SmsAuthenticationFilter.
Based on the verification results of the previous step, filter jumps to the processing logic of success or failure.

2, Code implementation

1,SmsAuthenticationToken

First of all, we write SmsAuthenticationToken. Here, we directly refer to the UsernamePasswordAuthenticationToken source code, directly paste it, and change it.

explain

The principle originally represents the user name, which is reserved here, but represents the mobile number.
The original code password of credentials can't be used for SMS login. Delete it directly.
SmsCodeAuthenticationToken() has two construction methods: one is to construct without authentication, the other is to construct with authentication.
The rest of the methods remove the useless attributes.

code

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    /**
     * In UsernamePasswordAuthenticationToken, this field represents the login user name,
     * Here is the mobile number for login
     */
    private final Object principal;

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

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

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

    @Override
    public Object getPrincipal() {
        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");
        }

        super.setAuthenticated(false);
    }

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

2,SmsAuthenticationFilter

Then write SmsAuthenticationFilter, refer to the source code of UsernamePasswordAuthenticationFilter, paste it directly, and change it.

explain

The original static fields, including username and password, are removed and replaced with our mobile number field.
The intercepting Url of this filter is specified in SmsCodeAuthenticationFilter(), which I specify as / sms/login in post mode.
The rest of the way is to delete and change the invalid ones.

code

public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    /**
     * form Field name of mobile number in the form
     */
    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";

    private String mobileParameter = "mobile";
    /**
     * POST only
     */
    private boolean postOnly = true;

    public SmsCodeAuthenticationFilter() {
        //The address of SMS verification code is / sms/login and the request is also a post
        super(new AntPathRequestMatcher("/sms/login", "POST"));
    }

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

        String mobile = obtainMobile(request);
        if (mobile == null) {
            mobile = "";
        }

        mobile = mobile.trim();

        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

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

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

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

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

    public String getMobileParameter() {
        return mobileParameter;
    }

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

3,SmsAuthenticationProvider

This method is very important. Firstly, it can be selected by the authentication manager when using SMS authentication code to log in. Secondly, it needs to process the authentication logic in this class.

explain

Implement the AuthenticationProvider interface and the authentication () and supports() methods.

code

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    /**
     * Process session utility class
     */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_SMS";

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;

        String mobile = (String) authenticationToken.getPrincipal();

        checkSmsCode(mobile);

        UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
        // In this case, after the authentication is successful, a new authentication result with authentication should be returned
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }

    private void checkSmsCode(String mobile) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // Get picture verification code from session
        SmsCode smsCodeInSession = (SmsCode) sessionStrategy.getAttribute(new ServletWebRequest(request), SESSION_KEY_PREFIX);
        String inputCode = request.getParameter("smsCode");
        if(smsCodeInSession == null) {
            throw new BadCredentialsException("Application verification code not detected");
        }

        String mobileSsion = smsCodeInSession.getMobile();
        if(!Objects.equals(mobile,mobileSsion)) {
            throw new BadCredentialsException("Incorrect mobile number");
        }

        String codeSsion = smsCodeInSession.getCode();
        if(!Objects.equals(codeSsion,inputCode)) {
            throw new BadCredentialsException("Verification code error");
        }
    }

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

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

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

4,SmsCodeAuthenticationSecurityConfig

Now that you have customized the interceptor, you need to make changes in the configuration.

code

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    private SmsUserService smsUserService;
    @Autowired
    private AuthenctiationSuccessHandler authenctiationSuccessHandler;
    @Autowired
    private AuthenctiationFailHandler authenctiationFailHandler;

    @Override
    public void configure(HttpSecurity http) {
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenctiationSuccessHandler);
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenctiationFailHandler);

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        //It needs to change the interface of querying user information through user name to mobile number
        smsCodeAuthenticationProvider.setUserDetailsService(smsUserService);

        http.authenticationProvider(smsCodeAuthenticationProvider)
                .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

5,SmsUserService

Because the user name and password login ultimately query the user information through the user name, and the mobile phone authentication code login is through the mobile phone login, so we need to implement another SmsUserService by ourselves

@Service
@Slf4j
public class SmsUserService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RolesUserMapper rolesUserMapper;

    @Autowired
    private RolesMapper rolesMapper;

    /**
     * Mobile number query user
     */
    @Override
    public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException {
        log.info("Mobile number query user, mobile number = {}",mobile);
        //TODO here I didn't write the sql to check the user information through the mobile number, because when I first built the user table, I didn't build the mobile field, and now I don't want to add it temporarily
        //TODO, so I'm going to use the user name to query the user information for the moment
        User user = userMapper.findOneByUsername("Small");
        if (user == null) {
            throw new UsernameNotFoundException("No user information found");
        }
        //Get user association role information. If it is blank, the user is not associated with a role
        List<RolesUser> userList = rolesUserMapper.findAllByUid(user.getId());
        if (CollectionUtils.isEmpty(userList)) {
            return user;
        }
        //Get role ID collection
        List<Integer> ridList = userList.stream().map(RolesUser::getRid).collect(Collectors.toList());
        List<Roles> rolesList = rolesMapper.findByIdIn(ridList);
        //Insert user role information
        user.setRoles(rolesList);
        return user;
    }
}

6. Summary

The train of thought is very clear here. I'm here to summarize.

1. First, from the time of obtaining the verification, the current verification code information has been saved to the session, which includes the verification code and mobile phone number.

2. The user enters the authentication login, which is written directly in SmsAuthenticationFilter. First, verify whether the authentication code and mobile number are correct, and then query the user information. We can also open it into a user name and password to log in
 The filter specially verifies whether the verification code and mobile phone number are correct, and logs in the filter with the correct verification code.

3. There is also a key step in the SmsAuthenticationFilter process, that is, the user name and password login is the user-defined UserService. After the UserDetailsService is implemented, the user name information is queried through the user name, and here is
 Query user information by mobile number, so you need to customize SmsUserService to implement UserDetailsService.


3, Testing

1. Get verification code

The mobile number to obtain the verification code is 15612345678. Because there is no third-party sms SDK here, just output in the background.

Send the verification code: 254792 to the user whose mobile number is 15612345678

2. Landing

1) Incorrect verification code input

It is found that the login fails. Similarly, if the mobile number is not entered correctly, the login fails

2) Login succeeded

When the mobile number and SMS verification code are correct, the login is successful.


reference resources

1. Spring Security technology stack development enterprise level authentication and authorization (JoJo)

2,Spring boot integrates Spring Security (8) - SMS verification code login



I will be angry when others scold me for being fat, because I admit that I am fat in my heart. When people say I'm short, I feel funny because I know in my heart that I can't be short. That's why we get angry at other people's attacks.
The one who attacks my shield is the spear of my heart (21)

Posted by marky2coats on Sat, 27 Jun 2020 20:17:11 -0700