SpringBoot + mybatis-plus + HMACOTP implements two-factor authentication function

Original Lutiao Programming Lutiao Programming 2023-08-04 19:30 Published in Beijing

included in collection

#java30

#SpringBoot29

Picture

This series of courses will include various functional implementations and functional examples related to Springboot.

SpringBoot + mybatis-plus + HMACOTP implements two-factor authentication function

Two-Factor Authentication (2FA) is a method to enhance the security of user authentication, usually using a password in combination with another authentication factor, such as a one-time password (OTP). Here, we will use Spring Boot, MyBatis-Plus and HMACOTP to implement 2FA functionality. HMACOTP is an OTP algorithm based on HMAC (Hash Message Authentication Code).

To implement the two-factor authentication feature, we will use an interceptor to authenticate the user. First, we need to create a table in the database to save user information, including the fields id, user_name, password, nick_name, create_time and secret_key. Next, we will create a package named com.icoderoad.example.demo and provide the corresponding code and detailed comments in it.

SQL script to create user table and initialize users:

CREATE TABLE `otp_user` (</code><code> `id` bigint(20) NOT NULL AUTO_INCREMENT,</code><code> `user_name` varchar(50) NOT NULL,</code><code> `password` varchar(80) NOT NULL,</code><code> `nick_name` varchar(50) DEFAULT NULL,</code><code> `create_time` datetime DEFAULT NULL,</code><code> ` secret_key` varchar(120) NOT NULL,</code><code> PRIMARY KEY (`id`)</code><code>) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;</code>

<code>INSERT INTO `otp_user` (`id`, `user_name`, `password`, `nick_name`, `create_time`, `secret_key`)</code><code>VALUES</code><code>(1 , 'admin', '123456', 'Administrator', '2023-08-03 19:09:48', 'nl2pm0s078qbqg');

We need to configure the Spring Boot project and add the related dependencies of MyBatis-Plus and HMACOTP. Add the following dependencies in the pom.xml file:

<!-- Spring Boot --></code><code><dependency></code><code> <groupId>org.springframework.boot</groupId></code><code> <artifactId >spring-boot-starter-web</artifactId></code><code></dependency></code>

<code><!-- MyBatis-Plus --></code><code><dependency></code><code> <groupId>com.baomidou</groupId></code><code> <artifactId> mybatis-plus-boot-starter</artifactId></code><code> <version>3.5.3.1</version></code><code></dependency></code>

<code><!-- MySQL database driver--></code><code><dependency></code><code> <groupId>mysql</groupId></code><code> <artifactId>mysql- connector-java</artifactId></code><code></dependency>

Picture

Create the user entity class OtpUser.java and use Lombok annotations to simplify the code:

package com.icoderoad.example.demo.entity;</code>

<code>import lombok.Data;</code><code>import com.baomidou.mybatisplus.annotation.TableName;</code><code>import java.util.Date;</code>

<code>@Data</code><code>@TableName("otp_user")</code><code>public class OtpUser {<!-- --></code><code> private Long id;</code><code> private String userName;</code><code> private String password;</code><code> private String nickName;</code><code> private Date createTime;</code><code> private String secretKey;</code><code>}</code>

<code>Create user Mapper interface OtpUserMapper.java:</code>

<code>package com.icoderoad.example.demo.mapper;</code>

<code>import com.baomidou.mybatisplus.core.mapper.BaseMapper;</code><code>import com.icoderoad.example.demo.entity.OtpUser;</code>

<code>public interface OtpUserMapper extends BaseMapper<OtpUser> {<!-- --></code><code>}

Create a user service class OtpUserService.java for operating the database:

package com.icoderoad.example.demo.service;</code>

<code>import org.springframework.stereotype.Service;</code>

<code>import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;</code><code>import com.icoderoad.example.demo.entity.OtpUser;</code><code>import com.icoderoad. example.demo.mapper.OtpUserMapper;</code>

<code>@Service</code><code>public class OtpUserService extends ServiceImpl<OtpUserMapper, OtpUser> {<!-- --></code><code>}

Create two-factor authentication tool class HMACOTPUtil.java:

package com.icoderoad.example.demo.util;</code>

<code>import javax.crypto.Mac;</code><code>import javax.crypto.spec.SecretKeySpec;</code><code>import java.security.InvalidKeyException;</code><code>import java. security.NoSuchAlgorithmException;</code><code>import java.time.Instant;</code><code>import java.util.Base64;</code>

<code>public class HMACOTPUtil {<!-- --></code>

<code> // Valid HMACOTP verification time window (seconds) </code><code> private static final int VALID_WINDOW_SECONDS = 300;</code>

<code> public static boolean isValidHMACOTP(String otp, String secretKey) {<!-- --></code><code> try {<!-- --></code><code> // Get the current time timestamp (seconds)</code><code> long currentTimestamp = Instant.now().getEpochSecond();</code>

<code> // Try to verify HMACOTP within the time window within 5 minutes</code><code> for (int i = -1; i <= VALID_WINDOW_SECONDS; i + + ) {<!-- --></code><code>long timestamp = currentTimestamp + i;</code>

<code> // Calculate HMACOTP</code><code> String generatedOTP = generateHMACOTP(secretKey, timestamp);</code>

<code> // Compare whether the generated HMACOTP and the incoming HMACOTP are the same</code><code> if (otp.equals(generatedOTP)) {<!-- --></code><code> return true; </code><code> }</code><code> }</code><code> } catch (Exception e) {<!-- --></code><code> e.printStackTrace(); </code><code> }</code><code> return false;</code><code> }</code>

<code> public static String generateHMACOTP(String secretKey, long timestamp) throws NoSuchAlgorithmException, InvalidKeyException {<!-- --></code><code> // Convert timestamp to byte array</code><code> byte[] timestampBytes = String.valueOf(timestamp).getBytes();</code>

<code> // Create an HMAC algorithm instance, HmacSHA256 is used here</code><code> Mac hmacSha256 = Mac.getInstance("HmacSHA256");</code>

<code> // Initialize the HMAC algorithm using the secret key</code><code> SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256");</code><code> hmacSha256.init(secretKeySpec);</code>

<code> // Calculate HMAC</code><code> byte[] hmacBytes = hmacSha256.doFinal(timestampBytes);</code>

<code> // Base64 encode HMAC</code><code> return Base64.getEncoder().encodeToString(hmacBytes);</code><code> }</code><code>}

In the above code, we created a HMACOTPUtil class, in which the isValidHMACOTP method is used to verify whether the incoming HMACOTP is valid within the specified time. This method will attempt to verify HMACOTP with a timestamp within 5 minutes of the current time, allowing for a certain time difference.

Create the two-factor authentication interceptor HMACOTPInterceptor.java:

package com.icoderoad.example.demo.interceptor;</code>

<code>import javax.servlet.http.HttpServletRequest;</code><code>import javax.servlet.http.HttpServletResponse;</code>

<code>import org.springframework.beans.factory.annotation.Autowired;</code><code>import org.springframework.stereotype.Component;</code><code>import org.springframework.web.servlet.HandlerInterceptor; </code>

<code>import com.baomidou.mybatisplus.core.toolkit.Wrappers;</code><code>import com.icoderoad.example.demo.entity.OtpUser;</code><code>import com.icoderoad.example. demo.service.OtpUserService;</code><code>import com.icoderoad.example.demo.util.HMACOTPUtil;</code>

<code>@Component</code><code>public class HMACOTPInterceptor implements HandlerInterceptor {<!-- --></code>

<code>@Autowired</code><code>private OtpUserService otpUserService;</code>
<code> @Override</code><code> public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {<!-- --></code><code> String userName = request.getParameter(" userName");</code><code> String password = request.getParameter("password");</code><code> String otp = request.getParameter("otp"); // Second factor: HMACOTP verification code</code>

<code> // Check whether the user name and password are correct</code><code> OtpUser user = otpUserService.getOne(Wrappers.<OtpUser>lambdaQuery().eq(OtpUser::getUserName, userName));</code><code> if (user == null || !user.getPassword().equals(password)) {<!-- --></code><code> response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);</code> <code> response.getWriter().write("Username or password is wrong");</code><code> return false;</code><code> }</code>

<code> // Check whether the HMACOTP verification code is correct</code><code> String secretKey = user.getSecretKey();</code><code> if (!HMACOTPUtil.isValidHMACOTP(otp, secretKey)) {<!- - --></code><code> response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);</code><code> response.getWriter().write("HMACOTP verification code error");</code><code> return false;</code><code> }</code>

<code> // User authentication passed</code><code> return true;</code><code> }</code><code> </code><code>}

Register the interceptor into the Spring Boot application and modify the class InterceptorConfig.java:

package com.icoderoad.example.demo.conf;</code>

<code>import org.springframework.beans.factory.annotation.Autowired;</code><code>import org.springframework.context.annotation.Configuration;</code><code>import org.springframework.web.servlet. config.annotation.InterceptorRegistry;</code><code>import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;</code>

<code>import com.icoderoad.example.demo.interceptor.HMACOTPInterceptor;</code><code>import com.icoderoad.example.demo.interceptor.JwtInterceptor;</code>

<code>@Configuration</code><code>public class InterceptorConfig implements WebMvcConfigurer {<!-- --></code>

<code> private final JwtInterceptor jwtInterceptor;</code><code> </code><code> @Autowired</code><code> private HMACOTPInterceptor hmacotpInterceptor;</code>

<code> @Autowired</code><code> public InterceptorConfig(JwtInterceptor jwtInterceptor) {<!-- --></code><code> this.jwtInterceptor = jwtInterceptor;</code><code> }</code>

<code> @Override</code><code> public void addInterceptors(InterceptorRegistry registry) {<!-- --></code><code> // Add JwtInterceptor interceptor and specify the interception path</code> <code> registry.addInterceptor(jwtInterceptor)</code><code> .addPathPatterns("/api/jwt/user/**"); // Can be configured according to the actual path</code><code> </code> <code> registry.addInterceptor(hmacotpInterceptor)</code><code> .addPathPatterns("/secure/**"); // Add the URL path that requires authentication here</code><code> }</code><code>}

Let us add a Controller to implement the corresponding HMACOTP verification logic. Assume that our Controller path is /secure/login, which is used to handle user login requests. Here is the corresponding code:

Create the login response DTO class LoginResponseDTO.java:

package com.icoderoad.example.demo.dto;</code>

<code>import lombok.Data;</code>

<code>@Data</code><code>public class LoginResponseDTO {<!-- --></code><code> private String message;</code><code>}

Create the login Controller class OtpLoginController.java:

package com.icoderoad.example.demo.controller;</code>

<code>import java.security.InvalidKeyException;</code><code>import java.security.NoSuchAlgorithmException;</code><code>import java.time.Instant;</code>

<code>import org.springframework.beans.factory.annotation.Autowired;</code><code>import org.springframework.web.bind.annotation.GetMapping;</code><code>import org.springframework.web. bind.annotation.PostMapping;</code><code>import org.springframework.web.bind.annotation.RequestParam;</code><code>import org.springframework.web.bind.annotation.RestController;</code>

<code>import com.baomidou.mybatisplus.core.toolkit.Wrappers;</code><code>import com.icoderoad.example.demo.dto.LoginResponseDTO;</code><code>import com.icoderoad.example. demo.entity.OtpUser;</code><code>import com.icoderoad.example.demo.service.OtpUserService;</code><code>import com.icoderoad.example.demo.util.HMACOTPUtil;</code>

<code>@RestController</code><code>public class OtpLoginController {<!-- --></code>

<code> private final OtpUserService userService;</code>

<code> @Autowired</code><code> public OtpLoginController(OtpUserService userService) {<!-- --></code><code> this.userService = userService;</code><code> }</code>

<code> @PostMapping("/secure/login")</code><code> public LoginResponseDTO login(@RequestParam String userName, @RequestParam String password, @RequestParam String otp) {<!-- --></code>

<code>LoginResponseDTO responseDTO = new LoginResponseDTO();</code>

<code> // Check if the username and password are correct</code><code> OtpUser user = userService.getOne(Wrappers.<OtpUser>lambdaQuery().eq(OtpUser::getUserName, userName));</code><code> if (user == null || !user.getPassword().equals(password)) {<!-- --></code><code> responseDTO.setMessage("Incorrect username or password"); </code><code> return responseDTO;</code><code> }</code>

<code> // Check whether the HMACOTP verification code is correct</code><code> String secretKey = user.getSecretKey();</code><code> if (!HMACOTPUtil.isValidHMACOTP(otp, secretKey)) {<!- - --></code><code> responseDTO.setMessage("HMACOTP verification code error");</code><code> return responseDTO;</code><code> }</code>

<code> responseDTO.setMessage("Login successful");</code><code> return responseDTO;</code><code> }</code><code> </code><code> @GetMapping("/ otp/generate-otp")</code><code> public String generateOTP(@RequestParam String userName) {<!-- --></code><code> OtpUser user = userService.getOne(Wrappers.<OtpUser> lambdaQuery().eq(OtpUser::getUserName, userName));</code><code> if (user == null) {<!-- --></code><code> return "The user does not exist" ;</code><code> }</code>

<code> // Get the timestamp of the current time (seconds) </code><code> long currentTimestamp = Instant.now().plusSeconds(300).getEpochSecond();</code>

<code> // Generate HMACOTP</code><code> String otp="";</code><code>try {<!-- --></code><code>otp = HMACOTPUtil.generateHMACOTP(user .getSecretKey(), currentTimestamp);</code><code>} catch (InvalidKeyException e) {<!-- --></code><code>// TODO Auto-generated catch block</code><code>e.printStackTrace();</code><code>} catch (NoSuchAlgorithmException e) {<!-- --></code><code>// TODO Auto-generated catch block</code><code> e.printStackTrace();</code><code>}</code>

<code> return "Generated OTP:" + otp;</code><code> }</code><code> </code><code>}

In the above code, we create a LoginController, call /secure/generate-otp request to obtain the specified user admin to obtain the otp value, and process the POST request /secure/login based on the generated otp value. This controller receives a LoginRequestDTO object, which contains the username, password, and HMACOTP verification code. It then verifies the username and password and calls the previously defined isValidHMACOTP method to verify that the HMACOTP verification code is correct.

The code in the example can be downloaded from https://github.com/icoderoad/wxdemo.git.

That’s it for today. If you have any questions and need consultation, you can leave a message directly or scan the QR code below to follow the official account. You can also add happyzjp WeChat to be invited to join the learning community, and we will try our best to answer your questions. The practice website has been officially launched. You can log in to the website http://www.icoderoad.com to practice the examples in the article.