Spring Security SMS verification code login in Spring Boot

In the section of adding graphic verification code in Spring Security, we have implemented the account password login based on Spring Boot + Spring Security, and integrated the function of graphic verification code. At present, another very common way of website login is SMS authentication code login, but Spring Security only provides the login authentication logic of account password by default, so to achieve the login authentication function of SMS authentication code, we need to imitate the Spring Security account password login logic code to achieve a set of our own authentication logic.

SMS verification code generation

In the previous section, Spring Security added a graphic verification code to integrate the function of SMS verification code login.

Similar to the graphic verification code, we first define a SMS verification code object, SmsCode:

public class SmsCode {
    private String code;
    private LocalDateTime expireTime;

    public SmsCode(String code, int expireIn) {
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
    }

    public SmsCode(String code, LocalDateTime expireTime) {
        this.code = code;
        this.expireTime = expireTime;
    }

    public boolean isExpire() {
        return LocalDateTime.now().isAfter(expireTime);
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public LocalDateTime getExpireTime() {
        return expireTime;
    }

    public void setExpireTime(LocalDateTime expireTime) {
        this.expireTime = expireTime;
    }
}

The SmsCode object contains two properties: the code verification code and the expireTime expiration time. The isExpire method is used to determine whether the SMS verification code has expired.

Then add the corresponding method of generating SMS verification code related request in validatecontroller:

@RestController
public class ValidateController {

    public final static String SESSION_KEY_SMS_CODE = "SESSION_KEY_SMS_CODE";


    @GetMapping("/code/sms")
    public void createSmsCode(HttpServletRequest request, HttpServletResponse response, String mobile) throws IOException {
        SmsCode smsCode = createSMSCode();
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_SMS_CODE + mobile, smsCode);
        // Output verification code to console instead of SMS sending service
        System.out.println("Your login verification code is:" + smsCode.getCode() + ",Valid for 60 seconds");
    }

    private SmsCode createSMSCode() {
        //Introducing commons Lang package
        String code = RandomStringUtils.randomNumeric(6);
        return new SmsCode(code, 60);
    }
}

Here we use the createmscode method to generate a 6-bit pure digital random number with an effective time of 60 seconds. Then, through the setAttribute method of the SessionStrategy object, the SMS verification code is saved to the Session, and the corresponding key is Session ﹣ key ﹣ SMS ﹣ code.

At this point, the SMS verification code generation module is completed, and the next step is to transform the login page.

Transformation landing page

We add a Form related to SMS verification code authentication in the login page:

<form action="/login/mobile" method="post">
    <div class="form">
        <h3>SMS verification code login</h3>
        <input type="text" placeholder="cell-phone number" name="mobile" value="18888888888" required="required"/>
        <br>
        <span style="display: inline">
            <input type="text" name="smsCode" placeholder="SMS verification code"/>
            <a href="/code/sms?mobile=18888888888">Send verification code</a>
        </span>
        <br>
        <button type="submit">Sign in</button>
    </div>
</form>

The value of the a tag's href attribute corresponds to the request URL of our SMS verification code generation method. The action of Form corresponds to the request URL of the SMS verification code login method, which is implemented below. At the same time, we need to configure / code/sms path authentication free in Spring Security:

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

    http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // Add verification code verification filter
            .formLogin() // Form login
                // http.httpBasic() // HTTP Basic
                .loginPage("/authentication/require") // Login jump URL
                .loginProcessingUrl("/login") // Process form login URL
                .successHandler(authenticationSucessHandler) // Login processed successfully
                .failureHandler(authenticationFailureHandler) // Failed to process login
            .and()
                .authorizeRequests() // Authorization configuration
                .antMatchers("/authentication/require",
                        "/login.html", "/code/image","/code/sms").permitAll() // Request path without authentication
                .anyRequest()  // All requests
                .authenticated() // All need certification
            .and()
                .csrf().disable();
}

Restart project, access http://localhost:8080/login.html:

Click send verification code, and the console output is as follows:

Next, we start to implement the login authentication logic using SMS verification code.

Add SMS verification code authentication

In Spring Security, the process of using user name and password authentication is as follows:

Spring Security uses the UsernamePasswordAuthenticationFilter filter to intercept user name and password Authentication requests, encapsulates the user name and password into a UsernamePasswordToken object and submits it to the AuthenticationManager for processing. AuthenticationManager will select an AuthenticationProvider (here is DaoAuthenticationProvider, one of the implementation classes of AuthenticationProvider) that supports handling this type of Token for Authentication. During the Authentication process, DaoAuthenticationProvider will call the loadUserByUsername method of UserDetailService to handle the Authentication. If the Authentication is passed (that is, UsernamePasswordToken If the user name and password in n Match), a UserDetails type object will be returned and the Authentication information will be saved in the Session. After Authentication, we can obtain the Authentication information through the Authentication object.

Since Spring Security does not use the process of providing SMS verification code authentication, we need to follow the above process to achieve:

In this process, we have customized a filter named SmsAuthenticationFitler to intercept SMS Authentication code login requests and encapsulate the mobile phone number into an object called SmsAuthenticationToken. In Spring Security, Authentication processing needs to be represented by Authentication manager, so we still leave SmsAuthenticationToken to Authentication manager. Then we need to define a SmsAuthenticationProvider that supports processing SmsAuthenticationToken object. SmsAuthenticationProvider calls the loadUserByUsername method of UserDetailService to process Authentication. Different from the user name and password Authentication, this is to query whether there is a corresponding user in the database through the mobile number in the SmsAuthenticationToken. If there is one, the user information will be encapsulated in the UserDetails object to return and the authenticated information will be saved in the Authentication object.

To implement this process, we need to define SmsAuthenticationFitler, SmsAuthenticationToken and SmsAuthenticationProvider, and add these components to Spring Security. Let's implement this process step by step.

Define SmsAuthenticationToken

Check the source code of UsernamePasswordAuthenticationToken, copy it and rename it to SmsAuthenticationToken, and modify it slightly. The modified code is as follows:

org.springframework.security.authentication.UsernamePasswordAuthenticationToken


public class SmsAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    /**
     * cell-phone number
     */
    private final Object principal;

    /**
     * SmsCodeAuthenticationFilter Unauthenticated Authentication built in
     * @param mobile
     */
    public SmsAuthenticationToken(String mobile) {
        super(null);
        this.principal = mobile;
        setAuthenticated(false);
    }


    /**
     * SmsCodeAuthenticationProvider Build authenticated Authentication in
     * @param principal
     * @param authorities
     */
    public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        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();
    }
}

SmsAuthenticationToken contains a principle attribute. From its two constructors, we can see that the principle stores the mobile number before authentication and the user information after authentication. UsernamePasswordAuthenticationToken originally contained a credentials attribute for storing passwords, which is not needed here.

Define SmsAuthenticationFilter

org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter

After defining the SmsAuthenticationToken, we then define the filter SmsAuthenticationFilter, which is used to process the SMS authentication code login request. Similarly, copy the UsernamePasswordAuthenticationFilter source code and modify it slightly:

public class SmsAuthenticationFilter extends
        AbstractAuthenticationProcessingFilter {

    /**
     * request Must have mobile parameter in
     */
    public static final String MOBILE_KEY = "mobile";

    private String mobileParameter = MOBILE_KEY;

    /**
     * post request
     */
    private boolean postOnly = true;

    /**
     * Mobile phone verification code login request processing url
     */
    public SmsAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login/mobile", "POST"));
    }


    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        //Determine if it is a post request
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        //Get phone number from request
        String mobile = obtainMobile(request);

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

        mobile = mobile.trim();

        //Create smscodeauthenticationtoken (not authenticated)
        SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile);

        //Set user information
        setDetails(request, authRequest);

        //Return Authentication instance
        return this.getAuthenticationManager().authenticate(authRequest);
    }

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

    protected void setDetails(HttpServletRequest request,
                              SmsAuthenticationToken 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() {
        return mobileParameter;
    }
}

The constructor specifies that the filter takes effect when the request is / login/mobile and the request method is POST. The value of the mobileParameter property is mobile, which corresponds to the name property of the mobile number input box on the login page. The attemptAuthentication method obtains the mobile parameter value from the request, and calls the SmsAuthenticationToken(String mobile) construction method of SmsAuthenticationToken to create a SmsAuthenticationToken. The next step, as shown in the flowchart, is for the SmsAuthenticationFilter to hand over the SmsAuthenticationToken to the authentication manager for processing.

Define SmsAuthenticationProvider

After creating the SmsAuthenticationFilter, we need to create a class that supports handling the Token of this type, namely SmsAuthenticationProvider, which needs to implement two abstract methods of AuthenticationProvider:


public class SmsAuthenticationProvider implements AuthenticationProvider {

    private UserDetailService userDetailService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
        //Call custom userDetailsService authentication
        UserDetails userDetails = userDetailService.loadUserByUsername((String) authenticationToken.getPrincipal());

        if (userDetails == null) {
            throw new InternalAuthenticationServiceException("The user corresponding to the mobile number was not found");
        }
        //If user is not empty, rebuild SmsCodeAuthenticationToken (authenticated)
        SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities());

        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }

    /**
     * Only Authentication for SmsCodeAuthenticationToken uses this Provider Authentication
     * @param authentication
     * @return
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return SmsAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailService getUserDetailService() {
        return userDetailService;
    }

    public void setUserDetailService(UserDetailService userDetailService) {
        this.userDetailService = userDetailService;
    }
}

The supports method specifies that the Token type to support processing is SmsAuthenticationToken, and the authenticate method is used to write specific authentication logic. In the authentication method, we take the phone number information from SmsAuthenticationToken and call the loadUserByUsername method of UserDetailService. In the authentication of user name and password type, the main logic of this method is to query the user information through the user name. If the user exists and the password is consistent, the authentication succeeds. In the authentication process of SMS verification code, this method needs to query the user through the mobile phone number, and if the user exists, the authentication passes. After passing the authentication, the SmsAuthenticationToken (object principal, collection <? Extends grantedauthority > authorities) constructor of SmsAuthenticationToken is called to construct an authenticated Token, which contains user information and user permissions.

You may ask, why is there no verification of SMS verification code in this step? In fact, the verification of SMS verification code is completed before SmsAuthenticationFilter, that is, only when the SMS verification code is correct can the authentication process be started. So next we need to set a filter to verify the correctness of SMS verification code.

Define SmsCodeFilter


@Component
public class SmsCodeFilter extends OncePerRequestFilter {

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
                                    FilterChain filterChain) throws ServletException, IOException {
        if (StringUtils.equalsIgnoreCase("/login/mobile", httpServletRequest.getRequestURI())
                && StringUtils.equalsIgnoreCase(httpServletRequest.getMethod(), "post")) {
            try {
                validateSmsCode(new ServletWebRequest(httpServletRequest));
            } catch (ValidateCodeException e) {
                authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
                return;
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }

    private void validateSmsCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {
        String smsCodeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "smsCode");
        String mobile = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "mobile");
        SmsCode codeInSession = (SmsCode) sessionStrategy.getAttribute(servletWebRequest, ValidateController.SESSION_KEY_SMS_CODE + mobile);

        if (StringUtils.isBlank(smsCodeInRequest)) {
            throw new ValidateCodeException("Verification code cannot be empty!");
        }
        if (codeInSession == null) {
            throw new ValidateCodeException("Verification code does not exist, please resend!");
        }
        if (codeInSession.isExpire()) {
            sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_SMS_CODE + mobile);
            throw new ValidateCodeException("The verification code has expired, please resend!");
        }
        if (!StringUtils.equalsIgnoreCase(codeInSession.getCode(), smsCodeInRequest)) {
            throw new ValidateCodeException("The verification code is incorrect!");
        }
        sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_SMS_CODE + mobile);

    }
}

 

Configuration takes effect

After defining the required components, we need to make some configuration, and combine these components to form a process corresponding to the above flowchart. Create a configuration class SmsAuthenticationConfig:


@Component
public class SmsAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    @Autowired
    private UserDetailService userDetailService;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
        smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        smsAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);

        SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
        smsAuthenticationProvider.setUserDetailService(userDetailService);

        http.authenticationProvider(smsAuthenticationProvider)
                .addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

    }
}

In the first step of the process, you need to configure SmsAuthenticationFilter, and set the AuthenticationManager, AuthenticationSuccessHandler, and AuthenticationFailureHandler properties respectively. These properties are from the AbstractAuthenticationProcessingFilter class inherited by SmsAuthenticationFilter.

The second step is to configure SmsAuthenticationProvider. In this step, we only need to inject our own UserDetailService.

Finally, HttpSecurity's authenticationprovider method is called to specify that the authenticationprovider is SmsAuthenticationProvider, and SmsAuthenticationFilter filter is added after UsernamePasswordAuthenticationFilter.

Now we have combined the components of SMS verification code authentication. The last step is to configure SMS verification code verification filter and add SMS verification code authentication process to Spring Security. Add the following configuration in the configure method of BrowserSecurityConfig:

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private MyAuthenticationSucessHandler authenticationSucessHandler;

    @Autowired
    private MyAuthenticationFailureHandler authenticationFailureHandler;

    @Autowired
    private ValidateCodeFilter validateCodeFilter;

    @Autowired
    private UserDetailService userDetailService;
    @Autowired
    private DataSource dataSource;

    @Autowired
    private SmsCodeFilter smsCodeFilter;

    @Autowired
    private SmsAuthenticationConfig smsAuthenticationConfig;


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // Add verification code verification filter
                .addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class) // Add SMS verification code verification filter
                .formLogin() // Form login
                .loginPage("/authentication/require") // Login jump URL
                .loginProcessingUrl("/login") // Process form login URL
                .successHandler(authenticationSucessHandler)//Login processed successfully
                .failureHandler(authenticationFailureHandler)//Failed to process login
                .and()
                .rememberMe()
                .tokenRepository(persistentTokenRepository()) // Configure token persistence warehouse
                .tokenValiditySeconds(3600) // remember expiration time, in seconds
                .userDetailsService(userDetailService) // Handling automatic login logic
                .and()
                .authorizeRequests() // Authorization configuration
                .antMatchers("/authentication/require", "/login.html", "/code/image","/code/sms").permitAll() // No authentication required for login jump URL
                .anyRequest()  // All requests
                .authenticated() // All need certification
                .and().csrf().disable()
                .apply(smsAuthenticationConfig);// Add SMS verification code authentication configuration to Spring Security
    }

    /**
     * token Persistent objects
     */

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        jdbcTokenRepository.setCreateTableOnStartup(false);
        return jdbcTokenRepository;
    }
}

 

test

Restart the project, and the browser opens two windows to access http://localhost:8080/login.html

Click the first window to send the verification code, and the console output is as follows:

Enter the verification code in the second window to log in:

Certification success

Source: https://gitee.com/hekang_admin/security-demo4.git

Keywords: Programming Mobile Spring Session Attribute

Added by Thoughtless on Tue, 24 Dec 2019 06:00:15 +0200