Spring Security adds secondary authentication: improving application security

Spring Security adds secondary authentication: improving application security

1. Introduction

1 Overview of Spring Security

Spring Security is a security framework based on the Spring framework for providing authentication and authorization services for Java applications.

2 Necessity of secondary authentication

The traditional user name and password authentication methods have the risk of being cracked, so after the user logs in, secondary authentication is required to enhance the security of identity authentication.

2. How Spring Security implements secondary authentication

1 Use an existing secondary authentication service

1.1 Integrate Google Authenticator

Google Authenticator is an open source software based on the TOTP algorithm, which users can install on their smartphones for secondary authentication. Spring Security can perform secondary authentication through Google Authenticator. Below is a simple example.

1.1.1 Add dependencies in pom.xml

<dependency>
    <groupId>com.warrenstrange</groupId>
    <artifactId>googleauth</artifactId>
    <version>1.0.0</version>
</dependency>

1.1.2 Implement Google Authenticator

@Service
public class GoogleAuthenticatorService {
    
    

    private final GoogleAuthenticator gAuth = new GoogleAuthenticator();

    /** 获取密钥 **/
    public String createSecret() {
    
    
        final GoogleAuthenticatorKey gak = gAuth.createCredentials();
        return gak.getKey();
    }

    /** 验证身份 **/
    public boolean authorize(final String secret, final int otp) {
    
    
        return gAuth.authorize(secret, otp);
    }

    /** 获取二维码 **/
    public String getQR(final String secret, final String account) {
    
    
        final String format = "otpauth://totp/%s?secret=%s&issuer=%s";
        return String.format(format, account, secret, account);
    }

}

A GoogleAuthenticatorService class is created in the above code to implement related functions of Google Authenticator. Among them, the createSecret() method is used to create a new secret key, the authorize() method is used to verify whether a TOTP is valid, and the getQR() method is used to generate a TOTP QR code.

1.1.3 Configuring Google Authenticator in Spring Security

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Autowired
    private GoogleAuthenticatorService googleAuthenticatorService;

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
    
    
        http.csrf().disable()
                .authorizeRequests()
                    .antMatchers("/user/**").authenticated()
                    .antMatchers("/admin/**").hasAnyRole("ADMIN")
                    .and()
                .formLogin()
                    .and()
                .addFilterBefore(buildGoogleAuthFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    private GoogleAuthFilter buildGoogleAuthFilter() throws Exception {
    
    
        final GoogleAuthFilter filter = new GoogleAuthFilter("/check-google-auth");
        filter.setSecretProvider((request, username) -> {
    
    
            final String secret = userService.getSecret(username); // 获取用户的密钥
            return secret != null ? secret : "";
        });
        filter.setGoogleAuthenticator(googleAuthenticatorService.getGoogleAuthenticator());
        return filter;
    }

}

A SecurityConfig class is created in the above code, and a GoogleAuthFilter filter is defined in it to add the secondary authentication function. In this filter, we get the user's secret via the setSecretProvider() method, and then set an instance of Google Authenticator via the setGoogleAuthenticator() method.

1.2 Integrating Authy

Authy is a secondary authentication service provider based on the TOTP algorithm, which can provide secondary authentication services for Spring Security. Below is a simple example.

1.2.1 Add dependencies in pom.xml

<dependency>
    <groupId>com.authy</groupId>
    <artifactId>authy-client</artifactId>
    <version>1.2</version>
</dependency>

1.2.2 Implement Authy

@Service
public class AuthyService {
    
    

    /** Authy API Key **/
    private final static String AUTHY_API_KEY = "your-authy-api-key";

    /** Authy客户端 **/
    private final AuthyApiClient authyApiClient = new AuthyApiClient(AUTHY_API_KEY);

    /** 注册用户 **/
    public User createUser(final String email, final String countryCode, final String phone) throws Exception {
    
    
        final Users users = authyApiClient.getUsers();
        final User user = users.createUser(email, phone, countryCode);
        return user;
    }

    /** 发送验证码 **/
    public void sendVerification(final String userId, final String via) throws Exception {
    
    
        final Tokens tokens = authyApiClient.getTokens();
        tokens.requestSms(Integer.valueOf(userId), via);
    }

    /** 验证验证码 **/
    public boolean verifyToken(final String userId, final int token) throws Exception {
    
    
        final Tokens tokens = authyApiClient.getTokens();
        final TokenVerification tokenVerification = tokens.verify(Integer.valueOf(userId), token);
        return tokenVerification.isOk();
    }

}

An AuthyService class is created in the above code to interact with the Authy client. Among them, the createUser() method is used to register a new user, the sendVerification() method is used to send a verification code to the user, and the verifyToken() method is used to verify whether the verification code is valid.

1.2.3 Configuring Authy in Spring Security

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Autowired
    private AuthyService authyService;

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
    
    
        http.csrf().disable()
                .authorizeRequests()
                    .antMatchers("/user/**").authenticated()
                    .antMatchers("/admin/**").hasAnyRole("ADMIN")
                    .and()
                .formLogin()
                    .and()
                .addFilterBefore(buildAuthyFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    private AuthyFilter buildAuthyFilter() throws Exception {
    
    
        final AuthyFilter filter = new AuthyFilter("/check-authy");
        filter.setAuthyService(authyService);
        return filter;
    }

}

In the above code, a SecurityConfig class is created, and an AuthyFilter filter is defined in it to add the secondary authentication function. In this filter, we set an instance of AuthyService through the setAuthyService() method.

2 Develop the secondary authentication module by yourself

2.1 Realize the secondary authentication function

Developing the secondary authentication module by ourselves means that we need to implement our own TOTP algorithm. Below is a simple example.

@Service
public class TotpService {
    
    

    private final static int WINDOW_SIZE = 3;
    private final static int CODE_DIGITS = 6;
    private final static String HMAC_ALGORITHM = "HmacSHA1";

    private static final int[] DIGITS_POWER
            = // 0 1  2   3    4     5      6       7        8
            {
    
    1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000 };

    /** 获取密钥 **/
    public String createSecret() {
    
    
        final SecureRandom random = new SecureRandom();
        final byte[] bytes = new byte[64];
        random.nextBytes(bytes);
        return Base32Utils.encode(bytes);
    }

    /** 生成验证码 **/
    public int generateCode(final String secret) throws Exception {
    
    
        final long timeIndex = System.currentTimeMillis() / 30000L; // 30秒
        final byte[] keyBytes = Base32Utils.decode(secret);
        final byte[] data = new byte[8];
        for (int i = 7; i >= 0; i--) {
    
    
            data[i] = (byte) (timeIndex & 0xff);
            timeIndex >>= 8;
        }
        final SecretKeySpec signingKey = new SecretKeySpec(keyBytes, HMAC_ALGORITHM);
        final Mac mac = Mac.getInstance(HMAC_ALGORITHM);
        mac.init(signingKey);
        final byte[] hmac = mac.doFinal(data);
        int offset = hmac[hmac.length - 1] & 0xf;
        int binCode = ((hmac[offset] & 0x7f) << 24)
                | ((hmac[offset + 1] & 0xff) << 16)
                | ((hmac[offset + 2] & 0xff) << 8)
                | (hmac[offset + 3] & 0xff);
        return binCode % DIGITS_POWER[CODE_DIGITS];
    }

    /** 验证身份 **/
    public boolean authorize(final String secret, final int otp, final int tolerance) throws Exception {
    
    
        final long timeIndex = System.currentTimeMillis() / 30000L; // 30秒
        final byte[] keyBytes = Base32Utils.decode(secret);
        for (int i = -tolerance; i <= tolerance; i++) {
    
    
            final long ti = timeIndex + i;
            final byte[] data = new byte[8];
            for (int j = 7; j >= 0; j--) {
    
    
                data[j] = (byte) (ti & 0xff);
                ti >>= 8;
            }
            final SecretKeySpec signingKey = new SecretKeySpec(keyBytes, HMAC_ALGORITHM);
            final Mac mac = Mac.getInstance(HMAC_ALGORITHM);
            mac.init(signingKey);
            final byte[] hmac = mac.doFinal(data);
            int offset = hmac[hmac.length - 1] & 0xf;
            int binCode = ((hmac[offset] & 0x7f) << 24)
                    | ((hmac[offset + 1] & 0xff) << 16)
                    | ((hmac[offset + 2] & 0xff) << 8)
                    | (hmac[offset + 3] & 0xff);
            if (binCode % DIGITS_POWER[CODE_DIGITS] == otp) {
    
    
                return true;
            }
        }
        return false;
    }

}

In the above code, a TotpService class is created to implement the secondary authentication function. Among them, the createSecret() method is used to create a new key, the generateCode() method is used to generate a TOTP verification code, and the authorize() method is used to verify whether TOTP is valid.

2.2 Configure your own secondary authentication module in Spring Security

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Autowired
    private TotpService totpService;

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
    
    
        http.csrf().disable()
                .authorizeRequests()
                    .antMatchers("/user/**").authenticated()
                    .antMatchers("/admin/**").hasAnyRole("ADMIN")
                    .and()
                .formLogin()
                    .and()
                .addFilterBefore(buildTotpFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    private TotpFilter buildTotpFilter() throws Exception {
    
    
        final TotpFilter filter = new TotpFilter("/check-totp");
        filter.setTotpService(totpService);
        return filter;
    }

}

A SecurityConfig class is created in the above code, and a TotpFilter filter is defined in it to add the secondary authentication function. In this filter, we set the instance of TotpService through the setTotpService() method.

3. Implementation process

1 Integrate Google Authenticator

1.1 Install and configure Google Authenticator

First you need to install and configure Google Authenticator on the server side. The specific steps can be carried out as follows:

  1. Under Linux system, use the following command to install Google Authenticator:

    sudo apt-get install libpam-google-authenticator -y
    
  2. To create a Google Authenticator configuration file, use the following command:

    google-authenticator
    

    Configuration requires answering a series of questions, such as:

    • Do you want to enforce google authentication for mapped users?
    • Do you want to enforce Google Authentication for all new users?
    • Is Google Authentication allowed for SSH login?
    • Do you allow Google Authenticator for the web?
    • Are timestamp windows used when authenticating?

    In general, you can choose the default value. Finally, you will get a QR code and a key, which need to be recorded for later configuration.

  3. Configure the PAM service, edit the /etc/pam.d/sshd file, and add the following commands:

    auth required pam_google_authenticator.so nullok
    

    This command will force Google Authenticator authentication and prevent users from skipping authentication. If you only want some users to use Google Authenticator for authentication, you can use the following command:

    auth [success=done new_authtok_reqd=done default=ignore] pam_google_authenticator.so nullok
    

1.2 Configuring Google Authenticator in Spring Security

The following steps need to be completed when integrating Google Authenticator with Spring Security:

  1. add dependencies

    We need to add the following dependencies to our project:

    <dependency>
        <groupId>com.warrenstrange</groupId>
        <artifactId>googleauth</artifactId>
        <version>0.8.1</version>
    </dependency>
    

    This is an open source Google Authenticator implementation library that we will use to generate TOTP (Time-based One-Time Password).

  2. Implement custom Spring Security filters

    We need to customize a Spring Security filter to verify whether the user's TOTP is legal. The core code of the filter is as follows:

    public class TotpFilter extends OncePerRequestFilter {
          
          
    
        // 处理Totp认证请求的Handler
        private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler("/login?error");
    
        // 用于获取用户通过表单提交的Totp码
        private String totpParameter = "totpCode";
    
        // 处理Totp认证请求的URL
        private String authenticationUrl = "/authenticate/totp";
    
        // 用于生成Totp验证码
        private GoogleAuthenticator gAuth;
    
        // 构造函数,传入处理Totp认证请求的URL
        public TotpFilter(String authenticationUrl) {
          
          
            gAuth = new GoogleAuthenticator();
            setFilterProcessesUrl(authenticationUrl);
        }
    
        @Override
        public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
          
          
            try {
          
          
                // 获取表单中提交的Totp码
                String totpCode = request.getParameter(totpParameter);
                if (StringUtils.isBlank(totpCode)) {
          
          
                    throw new TotpException("Totp code is missing.");
                }
    
                // 获取当前用户
                Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                if (authentication == null || !(authentication.getPrincipal() instanceof User)) {
          
          
                    throw new TotpException("User not authenticated.");
                }
    
                // 生成Totp验证码
                User user = (User) authentication.getPrincipal();
                int code = Integer.parseInt(totpCode);
                boolean isCodeValid = gAuth.authorize(user.getTotpSecret(), code);
                if (isCodeValid) {
          
          
                    // 验证通过,继续执行过滤链
                    chain.doFilter(request, response);
                } else {
          
          
                    // 验证失败,跳转到错误页面
                    failureHandler.onAuthenticationFailure(request, response, new BadCredentialsException("Invalid totp code."));
                }
            } catch (TotpException e) {
          
          
                // 验证失败,跳转到错误页面
                failureHandler.onAuthenticationFailure(request, response, new BadCredentialsException(e.getMessage()));
            }
        }
    }
    

    The core logic of this filter is to obtain the TOTP code submitted by the user from the request, and use the Google Authenticator library to verify whether the TOTP code is legal. If the TOTP code is legal, continue to execute the filter chain, otherwise jump to the error page.

  3. Register custom filter in Spring Security configuration

    We need to register the custom filter in the Spring Security configuration:

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
          
          
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
          
          
            http
                    .addFilterBefore(new TotpFilter("/authenticate/totp"), UsernamePasswordAuthenticationFilter.class)
                    .authorizeRequests()
                    .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                    .antMatchers("/admin/**").hasRole("ADMIN")
                    .anyRequest().authenticated()
                    .and()
                    .formLogin().defaultSuccessUrl("/home")
                    .and()
                    .logout().logoutSuccessUrl("/login?logout");
        }
    
        @Autowired
        public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
          
          
            auth.inMemoryAuthentication()
                    .withUser("user").password("password").roles("USER")
                    .and()
                    .withUser("admin").password("password").roles("ADMIN");
        }
    }
    

    In the above code, the custom Totp filter we implemented is registered through the addFilterBefore() method. This filter will be executed before Spring Security's UsernamePasswordAuthenticationFilter to verify whether the TOTP code entered by the user is legal.

1.3 Test the integration of Google Authenticator

The following steps need to be completed when conducting Google Authenticator integration testing:

  1. Use the Google Authenticator mobile app to scan the image that generates the QR code. After scanning, you will get a six-digit secondary authentication Totp code.

  2. When logging in, in addition to the user name and password, you also need to enter the TOTP code.

  3. If the TOTP code is valid, it will automatically jump to the specified homepage.

2 Integrate Authy

2.1 Register and configure Authy

You first need to register an account on the Authy website. Then you need to create a new application and get the Api Key and App Id of the application. These two values ​​need to be used in the code later.

2.2 Configuring Authy in Spring Security

Authy can implement two-factor authentication without using Google Authenticator authentication. We can integrate Authy with Spring Security by following these steps:

  1. Add Authy SDK

    We need to add Authy's java SDK as a project dependency:

    <dependency>
        <groupId>com.authy</groupId>
        <artifactId>authy-java</artifactId>
        <version>3.0.0</version>
    </dependency>
    
  2. Custom Spring Security filters

    We need to customize a Spring Security filter to verify whether the user's Authy TOTP is legal. The core code of the filter is as follows:

     /**
      * 过滤器,用于校验Authy TOTP验证码是否合法。
      */
     public class AuthyTotpFilter extends OncePerRequestFilter {
          
          
    
         // 处理Totp认证请求的Handler
         private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler("/login?error");
    
         // 用于获取用户通过表单提交的Totp码
         private String totpParameter = "totpCode";
    
         // 处理Authy认证请求的URL
         private String authenticationUrl = "/authenticate/authy";
    
         // Authy SDK的实例
         private AuthyApiClient authyApiClient = new AuthyApiClient(
                 System.getProperty("authy.api.key"), System.getProperty("authy.api.url"));
    
         /**
          * 构造函数,需要传入处理Totp认证请求的URL。
          */
         public AuthyTotpFilter(String authenticationUrl) {
          
          
             setFilterProcessesUrl(authenticationUrl);
         }
    
         @Override
         protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
          
          
             try {
          
          
                 // 获取表单中提交的Authy TOTP码
                 String token = request.getParameter(totpParameter);
                 if (StringUtils.isBlank(token)) {
          
          
                     throw new AuthyException("Authy token is missing.");
                 }
    
                 // 获取当前用户
                 Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                 if (authentication == null || !(authentication.getPrincipal() instanceof User)) {
          
          
                     throw new AuthyException("User not authenticated.");
                 }
    
                 // 获取用户绑定的Authy设备
                 User user = (User) authentication.getPrincipal();
                 String authyId = user.getAuthyId();
                 if (StringUtils.isBlank(authyId)) {
          
          
                     throw new AuthyException("Authy ID not found.");
                 }
    
                 // 验证Authy TOTP码
                 StartTokenVerificationResponse response1 = authyApiClient.getTokens().verify(authyId, token, true);
                 if (response1.isValid()) {
          
          
                     // 认证通过,继续执行过滤链
                     filterChain.doFilter(request, response);
                 } else {
          
          
                     // 认证失败,跳转到错误页面
                     failureHandler.onAuthenticationFailure(request, response, new BadCredentialsException("Invalid authy token."));
                 }
             } catch (AuthyException e) {
          
          
                 // 认证失败,跳转到错误页面
                 failureHandler.onAuthenticationFailure(request, response, new BadCredentialsException(e.getMessage()));
             }
         }
     }
    

    The core logic of this filter is to obtain the user's Authy TOTP code from the form and verify it through the AuthyApiClient class. If the verification is passed, continue to execute the filter chain, otherwise jump to the error page.

  3. Register custom filter in Spring Security configuration

We need to register the custom filter in the Spring Security configuration:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
                .addFilterBefore(new AuthyTotpFilter("/authenticate/authy"), UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                    .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                    .antMatchers("/admin/**").hasRole("ADMIN")
                    .anyRequest().authenticated()
                    .and()
                .formLogin().defaultSuccessUrl("/home")
                    .and()
                .logout().logoutSuccessUrl("/login?logout");
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth.inMemoryAuthentication()
                .withUser("user").password("password").roles("USER")
                .and()
                .withUser("admin").password("password").roles("ADMIN");
    }
}

In the above code, the custom AuthyTotp filter we implemented is registered through the addFilterBefore() method. This filter will be executed before Spring Security's UsernamePasswordAuthenticationFilter to verify whether the Authy TOTP code entered by the user is legal.

2.3 Test the integration of Authy

The following steps need to be completed when conducting Authy integration testing:

  1. First, you need to obtain the user's Authy ID, which is related to Authy when the user registers. You can get this ID through text messages, phone calls, callbacks, and the Authy mobile app.

  2. A numeric code can be seen in the Authy mobile app to confirm the device. After entering this digital code, you can get a six-digit secondary authentication Totp code.

  3. When logging in, you need to enter the Authy TOTP code in addition to the user name and password.

  4. If the Authy TOTP code is valid, it will automatically jump to the specified home page.

3 Self-developed secondary authentication module

When the system requirements do not match the existing secondary authentication methods, we can develop the secondary authentication module by ourselves. Now, we intend to implement a two-factor authentication module based on SMS and integrate it into a Java Web application based on Spring Security.

3.3.1 Realize the secondary authentication based on SMS

Our SMS-based secondary authentication module needs to complete the following tasks:

  • Receive the mobile phone number and user ID submitted by the user
  • Verify the user's mobile phone number and send a SMS verification code to the mobile phone number
  • The user submits the SMS verification code and performs verification

We use Twilio API to implement the SMS verification code sending and verification process. Twilio is an easy-to-use cloud messaging platform that makes it easy to send SMS and voice messages. We can use the REST API provided by Twilio to send and verify the SMS verification code, and encapsulate it in a Java web application.

First, register an account on the website to obtain API Key and API Secret, which are used to send and verify SMS verification codes.

Secondly, the Java library provided by Twilio needs to be used in the Java web application, and the following code is introduced:

import com.twilio.Twilio;
import com.twilio.rest.api.v2010.account.Message;
import com.twilio.type.PhoneNumber;

public class TwilioSMSVerifier {
    
    
  private static final String TWILIO_ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
  private static final String TWILIO_AUTH_TOKEN = "your_auth_token";
  private static final String TWILIO_PHONE_NUMBER = "+1415XXXXXXX";

  public static void sendMessage(String toPhoneNumber, String message) {
    
    
    Twilio.init(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN);
    Message twilioMessage = Message.creator(
            new PhoneNumber(toPhoneNumber),
            new PhoneNumber(TWILIO_PHONE_NUMBER),
            message
    ).create();
  }

  public static boolean verifyCode(String toPhoneNumber, String code) {
    
    
    // 进行验证码验证...
    return true;
  }
}

Here we have introduced the Twilio library and defined methods for sending SMS and verifying verification codes. Note that the TWILIO_ACCOUNT_SIDand here TWILIO_AUTH_TOKENneed to be replaced with your own Twilio account information. TWILIO_PHONE_NUMBERis the phone number bound to your Twilio account.

Next sendMessage, implement the method to call the Twilio API to send the SMS verification code:

public static void sendMessage(String toPhoneNumber, String message) {
    
    
    Twilio.init(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN);
    Message twilioMessage = Message.creator(
            new PhoneNumber(toPhoneNumber),
            new PhoneNumber(TWILIO_PHONE_NUMBER),
            message
    ).create();

    System.out.println("Twilio message SID: " + twilioMessage.getSid());
}

Here, the object provided by Twilio is used Messageto send the SMS verification code. We use PhoneNumberan object to represent the phone number, and Messagespecify the phone number and the content of the message to be sent in tothe fromconstructor message. After sending, we pass System.outthe SID of the output Twilio message.

Next, implement verifyCodethe method to verify the SMS verification code submitted by the user:

public static boolean verifyCode(String toPhoneNumber, String code) {
    
    
    Twilio.init(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN);

    List<Message> messages = Message.reader()
            .setTo(new PhoneNumber(toPhoneNumber))
            .read();

    for (Message message : messages) {
    
    
        String body = message.getBody();
        if (body.contains(code)) {
    
    
            return true;
        }
    }

    return false;
}

Here, the object provided by Twilio is used Message.readerto query the SMS information of the mobile phone number. We loop through the query results to see if the message body contains the submitted captcha. Returns if present, trueotherwise false.

3.2 Configure the self-developed secondary authentication module in Spring Security

Now the development of the secondary authentication module based on SMS has been completed. Next, we will integrate it into a Spring Security based Java web application. To do this, we need to add a new authentication provider to Spring Security and integrate our captcha validation logic into it.

We can AbstractUserDetailsAuthenticationProviderimplement our own authentication method through inheritance. Our authentication provider, needs to complete the following tasks:

  1. When performing authentication, obtain the mobile phone number and user ID submitted by the user, and send a verification code to the mobile phone number
  2. Receive the SMS verification code submitted by the user and verify its correctness

Below is the Java code for our self-developed secondary authentication provider:

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

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

public class SMSAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    
    
  private UserDetailsService userDetailsService;
  private int codeLength; // 验证码长度
  private int codeExpiration; // 验证码有效期

  public SMSAuthenticationProvider(UserDetailsService userDetailsService, int codeLength,
      int codeExpiration) {
    
    
    this.userDetailsService = userDetailsService;
    this.codeLength = codeLength;
    this.codeExpiration = codeExpiration;
  }

  @Override
  protected void additionalAuthenticationChecks(UserDetails userDetails,
      UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    
    
    String code = authentication.getCredentials().toString();
    SMSUserDetails smsUser = (SMSUserDetails) userDetails;
    if (!TwilioSMSVerifier.verifyCode(smsUser.getMobile(), code)) {
    
    
      throw new BadCredentialsException("Invalid verification code");
    }
  }

  @Override
  protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
    
    
    String mobile = (String) authentication.getDetails();
    UserDetails loadedUser;
    try {
    
    
      loadedUser = userDetailsService.loadUserByUsername(username);
    } catch (UsernameNotFoundException notFound) {
    
    
      throw new BadCredentialsException("Account not found");
    }
    return new SMSUserDetails(loadedUser.getUsername(), loadedUser.getPassword(), mobile,
        loadedUser.getAuthorities());
  }

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

  public class SMSUserDetails implements UserDetails {
    
    
    private final String username;
    private final String password;
    private final String mobile;
    private final List<GrantedAuthority> authorities;

    public SMSUserDetails(String username, String password, String mobile, List<GrantedAuthority> authorities) {
    
    
      this.username = username;
      this.password = password;
      this.mobile = mobile;
      this.authorities = new ArrayList<>(authorities);
    }

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

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

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

    public String getMobile() {
    
    
      return mobile;
    }

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

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

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

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

First, an internal class describing user details is defined SMSUserDetails, which is used to store information such as user ID, password, mobile phone number, and permissions. In retrieveUserthe method, we first get the mobile phone number submitted by the user, and then call to userDetailsServiceget the user information. Finally, we return SMSUserDetailsthe object, encapsulating the user details. It should be noted that the interface SMSUserDetailsmust be implemented UserDetailsto ensure that user information can be obtained correctly in the subsequent authentication process.

In additionalAuthenticationChecks, we use TwilioSMSVerifieran object to validate a captcha submitted by a user. Here, we obtain authentication.getCredentials(), that is, the verification code submitted by the user for verification. If the authentication fails, we throw a BadCredentialsExceptionto indicate that the authentication failed.

Finally, supportsreturn the supported authentication type in the method. Here, we only support UsernamePasswordAuthenticationTokentype authentication. Spring Security will throw an exception if any other type of authentication object is used.

3.3 Test the integration of the self-developed secondary authentication module

Now the development of the secondary authentication module based on mobile phone SMS has been completed, and it has been integrated into the Java Web application program based on Spring Security. Next, we will test this module to verify that it works properly.

First we need to send a test SMS to our own mobile number using the Twilio API. We can use the following code:

TwilioSMSVerifier.sendMessage("+8612345678901", "Your verification code is: 123456");

The following code can be used to authenticate in Spring Security after sending the SMS:

@RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(HttpServletRequest request) {
    
    
  String username = request.getParameter("username");
  String password = request.getParameter("password");
  String mobile = request.getParameter("mobile");
  String code = request.getParameter("code");

  UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
      username, password, Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));
  authRequest.setDetails(mobile);

  Authentication authentication = authManager.authenticate(authRequest);
  SecurityContextHolder.getContext().setAuthentication(authentication);

  return "redirect:/home";
}

Here, the authentication information submitted by the user is first obtained, including user name, password, mobile phone number and SMS verification code. Next, we create an UsernamePasswordAuthenticationTokenobject and set its type to ROLE_USER. We set the mobile number as the object's details. Finally, we authManagerauthenticate with and save the authentication result in SecurityContextHolder.

Now the integration test of the self-developed secondary authentication module has been completed. It should be noted that a real mobile phone number needs to be used for testing to ensure that the SMS verification code can be sent and verified normally. If you do not have a real mobile phone number, you can register a Twilio account and use the test number provided by Twilio for testing.

4. Summary and review

In this article, the concept of secondary authentication and its advantages and disadvantages are introduced in detail. Secondary authentication can be implemented in a variety of ways including based on hardware tokens, SMS verification codes and mobile applications. Different implementation methods have their own advantages and disadvantages, and you need to choose according to the actual situation.

Second, it demonstrates how to add two-factor authentication to a Web-based application using Spring Security. A two-factor authentication module can be easily integrated with our application by using Spring Security. In the process, learn how to configure Spring Security and how to use the security features provided by Spring Security.

Some possible problems and solutions are also discussed at the end. For example, issues such as compatibility with mobile devices and malicious attacks were mentioned. Some methods and suggestions are provided to solve these problems, such as using newer mobile devices and implementing preventive measures.

I hope you can understand the main concepts of secondary authentication and how to use Spring Security to implement secondary authentication. At the same time, I also hope that everyone can understand the challenges that may be encountered in the second authentication, and can choose an appropriate solution.

4.1 Advantages and disadvantages of secondary authentication

Secondary authentication can provide additional security and enhance the security of user accounts. Through secondary authentication, users need to perform additional verification when logging in, so that other people cannot directly log in to the account. Especially in some key application scenarios, such as bank accounts and e-commerce platforms, secondary authentication can provide more reliable account protection.

Although secondary authentication can provide higher security, it may also affect the convenience and ease of use of the system. Especially in some applications that require frequent logins, such as Internet social networking applications and game applications, too many verification steps may cause users to feel disgusted. In addition, the complexity of implementing secondary authentication may also cause unnecessary burden and cost.

Therefore, it needs to be selected according to the actual situation in practice. We can use secondary authentication in key application scenarios, and use other authentication methods in other application scenarios to provide a better balance between user experience and system security.

4.2 Effect of Spring Security adding secondary authentication

The two-factor authentication module can be easily integrated into our application by using Spring Security. You can use the secondary authentication filter provided by Spring Security and configure our authentication method as required. Many security features provided by Spring Security can be used when authenticating to protect our application from malicious attacks.

At the same time, other security features can be easily implemented through Spring Security, such as user role control, session management and password encryption. In this way, we can build a more robust and secure application, and help users have a better experience and sense of trust.

4.3 Possible problems and solutions

In practice, some problems may be encountered, such as mobile device compatibility, SMS verification code implementation, and malicious attacks. In order to solve these problems, we can take some measures such as:

  • Select a newer version device. As mobile device technology continues to evolve, newer devices may better support multiple authentication methods, such as fingerprint recognition and facial recognition.

  • Use multiple captcha verification methods. We can use a variety of verification code verification methods, such as email verification codes, phone verification codes, and graphic verification codes, to improve security.

  • Implement preventive measures. We can prevent malicious attacks and intrusions by using security technologies such as Web firewalls and intrusion detection systems.

In short, by effectively managing and securing our applications, we can provide a higher quality user experience and security. With Spring Security, we can easily add two-factor authentication modules and make our applications more reliable and secure.

Guess you like

Origin blog.csdn.net/u010349629/article/details/130702882