7. Short Message Verification Code Login Based on Spring Security

Posted by Danicek on Tue, 01 Oct 2019 17:37:40 +0200

Based on the analysis of the process of user login in SpringSecurity, we need to customize the following categories to implement the phone number plus verification code.

SmsCodeAuthenticationFilter
package com.liaoxiang.smsCodeLogin;

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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @auther Mr.Liao
 * @date 2019/8/22 17:14
 */
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
    private String mobileParameter = "mobile";
    private boolean postOnly = true;

    //Set interception path for mobile phone landing
    public SmsCodeAuthenticationFilter() {
        super(new AntPathRequestMatcher("/mobile/login", "POST"));
    }
    //Getting Authentication Objects: An Example of Authentication
    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 mobile = this.obtainMobile(request);
            if (mobile == null) {
                mobile = "";
            }
            mobile = mobile.trim();
            //Get the phone number and generate the SmsCodeAuthenticationToken object to authenticate the AuthenticationManager picked out by provider.
            SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    /**
     * Method of obtaining cell phone number
     * @param request
     * @return
     */
    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(this.mobileParameter);
    }

    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 mobileParameter() {
        return this.mobileParameter;
    }

}

SmsCodeAuthenticationToken
package com.liaoxiang.smsCodeLogin;

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

import java.util.Collection;

/**
 * @auther Mr.Liao
 * @date 2019/8/22 17:08
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    //Used to save cell phone numbers.
    private final Object principal;

    // ~ Constructors
    // ===================================================================================================

    /**
     * This constructor can be safely used by any code that wishes to create a
     * <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
     * will return <code>false</code>.
     *
     */
    public SmsCodeAuthenticationToken(String mobile) {
        super(null);
        this.principal = mobile;
        setAuthenticated(false);
    }

    /**
     * This constructor should only be used by <code>AuthenticationManager</code> or
     * <code>AuthenticationProvider</code> implementations that are satisfied with
     * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
     * authentication token.
     *
     * @param principal
     * @param authorities
     */
    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true); // must use super, as we override
    }

    // ~ Methods
    // ========================================================================================================

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

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

        super.setAuthenticated(false);
    }

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

SmsCodeAuthenticationProvider
package com.liaoxiang.smsCodeLogin;

import lombok.Data;
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;

/**
 * @auther Mr.Liao
 * @date 2019/8/22 17:56
 */
@Data
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
    private UserDetailsService userDetailsService;
    //
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken)authentication;
        //Check user information according to cell phone number.
        UserDetails userDetails = userDetailsService.loadUserByUsername((String) authentication.getPrincipal());
        if (userDetails == null){
            throw new InternalAuthenticationServiceException("Unable to access user information");
        }
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails,userDetails.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

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

Add the phone number field to the user table, add the Service method to query the user according to the phone number.

@Service
public class MobileLoginService implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException {
        //If User is null, an exception message is thrown: UsernameNotFoundException
        User user = userMapper.findUserByMobile(mobile);
        if (user == null){
            throw new UsernameNotFoundException("The cell phone number has not yet been registered.");
        }
        return user;
    }
}

After configuring the above classes, the configuration class is injected into the main configuration class of MySecurityConfig and applied to.apply(smsCodeAuthenticationSecurityConfig) in the main configuration. After that, authorization can be made according to the user's cell phone number.

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    private FailureHandler failureHandler;
    @Autowired
    private SuccessHandler successHandler;
    @Autowired
    private MobileLoginService mobileLoginService;
    @Override
    public void configure(HttpSecurity http) {
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(failureHandler);
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(successHandler);

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(mobileLoginService);

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

Here's how to get the authentication code

@RestController
public class ValidateCodeController {
    public static final String IMAGE_SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
    public static final String SMS_SESSION_KEY = "SESSION_KEY_SMS_CODE";
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    //Implementing Class Object of Injection Picture Verification Code Generator
    @Autowired
    private ValidateCodeGenerator imageCodeGenerator;
    @Autowired
    private ValidateCodeGenerator smsCodeGenerator;
    @Autowired
    private SmsCodeSender smsCodeSender;

    @GetMapping("/code/image")
    public void createImageCode(HttpServletResponse response, HttpServletRequest request) throws IOException {
        ServletWebRequest servletWebRequest = new ServletWebRequest(request);
        //Using Graphic Verification Code Generator to Generate Graphic Verification Code
        ImageCode imageCode = (ImageCode) imageCodeGenerator.generate(servletWebRequest);
        //Save the validation code in session for 5 minutes
        sessionStrategy.setAttribute(servletWebRequest, IMAGE_SESSION_KEY, imageCode);
        // Write the generated image in JPEG format into the output stream of the response
        ImageIO.write(imageCode.getImage(),"JPEG",response.getOutputStream());
    }

    @GetMapping("/code/sms")
    public void createSmsCode(HttpServletResponse response, HttpServletRequest request) throws IOException {
        ServletWebRequest servletWebRequest = new ServletWebRequest(request);
        //Using Graphic Verification Code Generator to Generate Graphic Verification Code
        ValidateCode smsCode = smsCodeGenerator.generate(servletWebRequest);
        //Save the validation code in session for 5 minutes
        sessionStrategy.setAttribute(servletWebRequest, SMS_SESSION_KEY, smsCode);
        String mobile = "";
        //Get cell phone number
        try {
            mobile = ServletRequestUtils.getRequiredStringParameter(request, "mobile");
        } catch (ServletRequestBindingException e) {
            System.out.println(e.getMessage());
        }
        //Send verification code via SMS provider to mobile phone.
        smsCodeSender.send(mobile, smsCode.getCode());
    }
}

page

<h2>Mobile authentication code landing</h2>
<form action="/mobile/login" method="post">
    <table>
        <tr>
            <td>Cell-phone number:</td>
            <td><input type="text" name="mobile" value="18312345678"></td>
        </tr>
        <tr>
            <td>Short Message Verification Code:</td>
            <td>
                <input type="password" name="smsCode">
                <a href="/code/sms?mobile=18312345678" onclick="return pop();">Send Short Message Verification Code</a>
            </td>
        </tr>
        <tr>
            <td colspan="2"><button type="submit">Sign in</button></td>
        </tr>
    </table>
</form>

When landing, first get the verification code through mobile phone, submit it to the background, first determine whether the authentication code is correct, inquire whether the user exists or not according to the phone number, and complete the authorization process.

Topics: Mobile Session Java Lombok