SpringSecurity ,oAuth2.0 from entry to source code mastery spring security to realize mobile phone verification code login

In daily development, our application is not only form login, but also login through mobile verification code and third-party account in most cases. In fact, these different methods are almost the same. The mobile phone number verification code login is the same, so the password is not fixed; In fact, the third-party login is that our server gets a token from the third-party platform, then obtains the user information from the third-party platform according to the token, and then stores it in the SecurityContext of our server. The third-party login will be introduced in oauth2 later I'll introduce it in detail at 0. Today I mainly study the login function of user-defined mobile phone verification code.

1 review the authentication process of UsernamePasswordAuthenticationFilter

stay Interpretation of source code of authentication process and authority verification process of spring security In this article, we have introduced the authentication process of Spring Security in detail. UsernamePasswordAuthenticationFilter is the key. It intercepts the request with URI / login, then obtains the user name and password information entered by the user in the login interface from the request, and then seals it into a UsernamePasswordAuthenticationToken object and gives it to the AuthenticationManager for authentication.

The AuthenticationManager finally gives the authentication task to the AuthenticationProvider. During authentication, the user name and password saved during user registration need to be obtained from the database through UserDetailsService, and then compared with the information entered by the user to determine whether the authentication has passed.

Here we only do a general review. If you are not clear, you can take a look at that article again.

We also need to adopt this process when we log in with mobile verification code. We write the filter and provider in the process ourselves.

2. Create the interface and login interface for obtaining verification code

2.1 login interface

myLoginPage.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
    <meta charset="UTF-8">
    <title>My login page</title>
</head>
<body>
<h3>SMS login</h3>
<form action="/mobile/login" method="post">
    <table>
        <tr>
            <td>phone number:</td>
            <td><input id="mobileInput" type="text" name="mobile" value="12345678910"></td>
        </tr>
        <tr>
            <td>SMS verification code:</td>
            <td>
                <input type="text" name="code">
                <button id="smsCodeBtn" type="button">Get verification code</button>
            </td>
        </tr>
        <tr>
            <td colspan="2"><input type="submit" value="Sign in"/></td>
        </tr>
    </table>
</form>
</body>

<script>
    var Ajax={
        get: function(url, fn) {
            var xhr = new XMLHttpRequest();
            xhr.open('GET', url, true);
            xhr.onreadystatechange = function() {
                if (xhr.readyState == 4 && xhr.status == 200 || xhr.status == 304) {
                    fn.call(this, xhr.responseText);
                }
            };
            xhr.send();
        }
    }
    var smsCodeBtn = document.getElementById("smsCodeBtn");
    smsCodeBtn.onclick = function () {
        mobile = document.getElementById('mobileInput').value;
        url = "/smscode/send?mobile="+mobile;
        Ajax.get(url,function (data) {
            alert(data);
        })
    }
</script>
</html>

A simple login interface, click the "get verification code" button to get the verification code.

2.2 create an interface for obtaining verification code

When the user clicks to obtain the verification code in the login interface, call this method to send the verification code to the user's mobile phone. Here, we can print the verification code on the console. In the actual development, we must call the SMS service to send the SMS verification code to the user.

In this example, the generated verification code is stored in the session. Later, when logging in for verification, it is also obtained from the session. Of course, you can also store the verification code in redis, database, etc.

@RestController
public class SmsController {

    Logger logger = LoggerFactory.getLogger(SmsController.class);

    public static final String SMS_CODE = "SMS_CODE";

    @RequestMapping("/smscode/send")
    public String sendSmsCode(HttpSession session, HttpServletRequest request) throws ServletRequestBindingException {
        String mobile = request.getParameter("mobile");

        //Randomly generate a verification code
        Random rd=new Random();
        int code = rd.nextInt(10000);
        //Simulate sending SMS to users
        logger.info("send code to "+mobile+" : "+code);

        Map<String, Object> map = new HashMap<>();
        map.put("mobile", mobile);
        map.put("code", code);

        //Save the verification code into the session, and obtain it from the session during later verification
        session.setAttribute(SMS_CODE,map);
        return "The verification code was sent successfully. Please check it";
    }
}

2. Customize MobileCodeAuthenticationFilter

We follow the example of UsernamePasswordAuthenticationFilter to write our MobileCodeAuthenticationFilter.

This class needs to inherit from AbstractAuthenticationProcessingFilter.

I'll code it myself. We can directly copy the code in UsernamePasswordAuthenticationFilter and modify it slightly.

MobileCodeAuthenticationFilter.java:

public class MobileCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    //request parameter parameter name
    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
    public static final String SPRING_SECURITY_FORM_CODE_KEY = "code";

    private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
    private String codeParameter = SPRING_SECURITY_FORM_CODE_KEY;

    //By default, our mobile phone verification code login only supports POST requests
    private boolean postOnly = true;

    public MobileCodeAuthenticationFilter() {
        //Default login request processing address
        super(new AntPathRequestMatcher("/mobile/login", "POST"));
    }

    //Method of completing verification function
    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);
        String code = obtainCode(request);

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

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

        mobile = mobile.trim();

        MobileCodeAuthenticationToken authRequest = new MobileCodeAuthenticationToken(
                mobile, code);

        setDetails(request, authRequest);

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


    //Get the verification code entered by the user
    @Nullable
    protected String obtainCode(HttpServletRequest request) {
        return request.getParameter(codeParameter);
    }

    //Get the mobile phone number entered by the user
    @Nullable
    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }

    //Set the IP address, sessionID and other information into the MobileCodeAuthenticationToken object
    protected void setDetails(HttpServletRequest request,
                              MobileCodeAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }
}

This code is basically the same as that in UsernamePasswordAuthenticationFilter, except that we changed the parameter name in the request.

3 MobileCodeAuthenticationToken

In the UsernamePasswordAuthenticationFilter, the obtained user information is encapsulated in the UsernamePasswordAuthenticationToken. Let's also define a similar xxxAuthenticationToken class to encapsulate the mobile phone number and authentication code entered by the user.

Like UsernamePasswordAuthenticationToken, MobileCodeAuthenticationToken is also inherited from AbstractAuthenticationToken class.

MobileCodeAuthenticationToken.java:

package com.llk.filter;

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

import java.util.Collection;

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

    private final Object principal;
    private Object credentials;
    
    public MobileCodeAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }


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


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

        super.setAuthenticated(false);
    }

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

MobileCodeAuthenticationToken is completely consistent with UsernamePasswordAuthenticationToken and basically does not need to be modified.

4 MobileCodeAuthenticationProvider

Next, we need to customize the AuthenticationProvider to complete login verification.

Take out the verification code obtained by the user from the session and match it with the mobile phone number and verification code entered by the user in the login interface to judge whether the user has successfully logged in.

We post the code directly:
MobileCodeAuthenticationProvider.java:

@Component
public class MobileCodeAuthenticationProvider implements AuthenticationProvider {
    
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * Logic of identity authentication
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        //The authenticationToken is the one we created in MobileCodeAuthenticationFilter
        MobileCodeAuthenticationToken authenticationToken = (MobileCodeAuthenticationToken)authentication;

        //Obtain the mobile phone number and verification code entered by the user
        String mobile = (String) authenticationToken.getPrincipal();
        String code = (String) authenticationToken.getCredentials();

        //Get user information according to mobile phone number
        //First, check whether the user is registered according to the mobile phone number
        UserDetails user = userDetailsService.loadUserByUsername(mobile);
        if(user == null){
            throw new InternalAuthenticationServiceException("Unable to get user information");
        }

        //Verify mobile phone number and verification code
        checkSmsCode(mobile,code);

        //Notice the constructor called here
        //The code execution here shows that the mobile phone number and verification code entered by the user are correct,
        // You need to set authenticated to true, otherwise the subsequent process thinks that the user has not passed the authentication
        MobileCodeAuthenticationToken authenticationResult
                = new MobileCodeAuthenticationToken(user,null,user.getAuthorities());

        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }


    @Override
    public boolean supports(Class<?> authentication) {
        //Authentication methods supported by this provider
        return authentication.equals(MobileCodeAuthenticationToken.class);
    }

    //Verify whether the entered mobile phone number is consistent with that stored in the session
    private void checkSmsCode(String mobile, String codeParameter) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        //Obtain the verification code information saved in SmsController from session
        Map<String, Object> code = (Map<String, Object>) request.getSession().getAttribute(SmsController.SMS_CODE);
        if(code == null) {
            throw new BadCredentialsException("Verification code not applied");
        }

        String savedMobile = (String) code.get("mobile");
        int codeInt = (int) code.get("code");

        if(!savedMobile.equals(mobile)) {
            throw new BadCredentialsException("Inconsistent mobile phone numbers");
        }
        if(codeInt != Integer.parseInt(codeParameter)) {
            throw new BadCredentialsException("Verification code error");
        }
    }
}

You may think that the userDetailsService here is a little redundant, because we only need to take out the previously saved verification code from the session to verify the verification code entered by the user, and we don't need the userDetailsService to provide userDetails information.

In fact, it is not. Before verifying the verification code, we need to query whether the mobile phone number is our user through userDetailsService according to the mobile phone number entered by the user. If not, of course, we can't continue to execute the following logic.

Then, the most important point is the UserDetails information obtained through userDetailsService. We need to build a MobileCodeAuthenticationToken object to handle subsequent session management, adding authenticated users to SecurityContext and so on.

5 AuthUserService

Next, we define a UserService class to provide a method for MobileCodeAuthenticationProvider to obtain UserDetails.

The principle and function of the interface have been well understood by you after reading the previous article. Let's go directly to the code.

Here is just a simulation of obtaining a user from the database.

AuthUserService.java:

import com.llk.domain.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.ArrayList;
import java.util.List;

public class AuthUserService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException {

        //Here, it is simulated to obtain a user information from the database, etc
        User user = getUser(mobile);

        return user;
    }

    private User getUser(String mobile){
        User user = new User();
        user.setMobile(mobile);
        user.setPassword("123123123123123");
        user.setUsername("llk");

        //Impersonate the permissions the user has
        GrantedAuthority grantedAuthority = new GrantedAuthority(){

            @Override
            public String getAuthority() {
                return "USER";
            }
        };

        List<GrantedAuthority> authorityList = new ArrayList<>();
        authorityList.add(grantedAuthority);

        user.setAuthorities(authorityList);

        return user;
    }
}

User.java:

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

public class User implements UserDetails {

    private String mobile;
    private String password;
    private String username;

    private List<GrantedAuthority> authorities;

    public void setAuthorities(List<GrantedAuthority> authorities) {
        this.authorities = authorities;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getMobile() {
        return mobile;
    }

    public void setMobile(String mobile) {
        this.mobile = mobile;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

6. Handling of login success and login failure

LoginSuccessHandler.java:

@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
   /**
    This method is executed when the user logs in successfully
    */
   @Override
   public void onAuthenticationSuccess(HttpServletRequest request, 
   HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

       System.out.println("Login successful");
       System.out.println(authentication);
       response.setContentType("application/json; charset=utf-8");
       response.getWriter().write("LoginSuccessHandler Login successful");

   }
}

LoginFailureHandler.java:

@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
    /**
     *This method is executed when the authentication process throws an exception
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, 
    HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

        String message = exception.getMessage();
        response.setContentType("application/json; charset=utf-8");
        response.getWriter().write("LoginFailureHandler Login failed:"+message);
    }
}

7 configuration file

The next task is to configure the MobileCodeAuthenticationFilter and MobileCodeAuthenticationProvider defined above to Spring Security.

7.1 MobileCodeSecurityConfigurer

The main purpose of the configuration file is to create an instance of MobileCodeAuthenticationFilter, and then set the AuthenticationManager, AuthenticationSuccessHandler, SessionAuthenticationStrategy, etc. for the filter. Its function is similar to the configuration file FormLoginConfigurer for form login.

MobileCodeSecurityConfigurer.java:

import com.llk.filter.MobileCodeAuthenticationFilter;
import com.llk.handler.LoginFailureHandler;
import com.llk.handler.LoginSuccessHandler;
import com.llk.provider.MobileCodeAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;


@Configuration
public class MobileCodeSecurityConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    private LoginSuccessHandler loginSuccessHandler;

    @Autowired
    private LoginFailureHandler loginFailureHandler;

    @Autowired
    private MobileCodeAuthenticationProvider mobileCodeAuthenticationProvider;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        //Create our authentication filter
        MobileCodeAuthenticationFilter mobileCodeAuthenticationFilter = new MobileCodeAuthenticationFilter();
        //Set authentication manager for authentication filter
        mobileCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        //Set the processor after successful or failed login
        mobileCodeAuthenticationFilter.setAuthenticationSuccessHandler(loginSuccessHandler);
        mobileCodeAuthenticationFilter.setAuthenticationFailureHandler(loginFailureHandler);

        //Set the session management policy. If it is not set, it is the default NullAuthenticatedSessionStrategy
        SessionAuthenticationStrategy sessionAuthenticationStrategy = http
                .getSharedObject(SessionAuthenticationStrategy.class);
        if (sessionAuthenticationStrategy != null) {
            mobileCodeAuthenticationFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
        }

        //Add our mobileCodeAuthenticationProvider to the authenticationProviders collection
        http.authenticationProvider(mobileCodeAuthenticationProvider)
                //Add authentication filter to filter chain
                .addFilterAfter(mobileCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

7.2 WebSecurityConfig

This configuration file is similar to our previous one. It is mainly used to enable our Spring Security functions and some interface access configurations.

import com.llk.service.AuthUserService;
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.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

WebSecurityConfig.java: 
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MobileCodeSecurityConfigurer mobileCodeSecurityConfigurer;

    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

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

        http
                .authorizeRequests()
                .antMatchers("/smscode/send",//The interface for obtaining the verification code does not require login
                        "/toLoginPage",//Jump to login screen
                        "/mobile/login")//The interface that handles the login request does not require login
                .permitAll()
                .antMatchers("/book/get/**").hasAnyAuthority("USER","ADMIN")
                .anyRequest().authenticated()  //Any other request requires authentication

                //Set mobilecodesecurityconfigurator to httpSecurity
                .and().apply(mobileCodeSecurityConfigurer)

                .and().logout().permitAll()
                .and().csrf().disable();    //Disable CSRF

    }

    @Bean
    public UserDetailsService userDetailsService() {
        AuthUserService userService = new AuthUserService();
        return userService;
    }
}

In this configuration class, we don't configure forms. We only configure forms related to our current mobile phone verification code login. If your application also needs form login, just add the form login configuration in the configuration file as we did before. They don't interfere with each other.

8 other controllers, etc

LoginController.java:

@Controller
public class LoginController {
    @RequestMapping("/toLoginPage")
    public String toLoginPage(){
        return "myLoginPage";
    }
}

BookController.java:

@RestController
@RequestMapping("/book")
public class BookController {
    @RequestMapping("/get")
    public Book get(){
        Book book = new Book();
        book.setBookId("1");
        book.setBookName("<Thinking in Java>");
        book.setAuthor("Bruce Eckel");

        return book;
    }
}

Book.java:

public class Book {
    private String bookId;
    private String bookName;

    private String author;
    //setter getter
}

Well, so far, the function of login through mobile verification code has been completed. You can test it yourself.

After practicing this function, I think everyone has a deeper understanding of the authentication process of Spring Security. No matter what login function is completed in the future, it can be like this case.

In our daily development, we often use front and back-end separated applications, and our form login may not be used, because the request data we pass is in json format. If we want to complete the login function, we only need to rewrite an authentication filter, get the user name, password and other data in json format from the request in the attemptAuthentication() method, and then encapsulate it to xxxAuthenticationToken.

9 example code address

Example code address: https://github.com/coderllk/spring-security-oauth2-demos

Tags: Java oauth2

Posted by Submerged on Wed, 04 May 2022 06:07:10 +0300