SpringBoot project login and access to MFA secondary authentication

MFA multi-factor authentication ( Multi-Factor Authentication ):
Some services (such as websites) that require identity authentication, in order to improve security, usually require the user to perform a second identity authentication after the account password is successfully logged in to ensure that the correct user is logged in and avoid users User information is leaked due to leaked passwords or other reasons.
However, the user experience is relatively poor, because you have to log in twice.
Common usage scenarios:

  • Enterprise management background, especially systems involving sensitive information, such as CMS customer relationship management system, financial system, etc.
  • E-commerce background, such as Amazon store management background, usually requires secondary authentication
  • Git code repository
  • Viewing and modifying sensitive information of users on the site, such as the middle n digit of the mobile phone number is hidden by default, and secondary authentication is required to view; secondary authentication is required to modify the password, etc.

Common authentication methods are:

  • Mobile SMS verification code verification: send the verification code to the user via text message, and the user enters the verification code on the service (such as a website) and authenticates
  • Email verification code verification: the verification code is sent to the user by email, and the user enters the verification code and authenticates
  • MFA hardware device: allocate a hardware facility to the user to generate a dynamic password, and the user enters the password and authenticates.
    There is also a hardware device that can be automatically authenticated by plugging it into a computer
  • MFA software: the user installs a software on the computer or mobile phone, the software generates a dynamic password, and the user enters the password and authenticates
  • Biometrics: authentication through biometric features such as fingerprints and face recognition
  • Smart card: assign a card with identity information to the user, and the user puts the card on the card reading device of the service for authentication

This article only introduces the MFA software access solution of the website, and uses the mobile application for authentication.
As long as it is a mobile phone application based on the time synchronization algorithm, it can be supported, such as the following applications:

  • Google Authenticator (google authenticator)
  • Microsoft Authenticator (Microsoft Authenticator)
    Note: This kind of dynamic password authentication is usually also called OTP-Code (One-time Password), OTP token, two-step verification, secondary authentication, 2FA, etc.

The interaction process between the front end and the back end is as follows:

The backend provides 3 interfaces:

  • Account password login interface
  • Whether the account has been bound to the SecureKey interface
  • Secondary verification code verification interface

The complete interactive flowchart is as follows:
insert image description here

Implementation of SpringBoot project access

It is assumed that a SpringBoot project has been created.

add dependencies

Open pom.xmlthe file and add the following dependencies:

<!-- 用于SecureKey生成 -->
<dependency>
	<groupId>commons-codec</groupId>
	<artifactId>commons-codec</artifactId>
</dependency>

<!-- 用于二维码生成 -->
<dependency>
	<groupId>com.google.zxing</groupId>
	<artifactId>core</artifactId>
	<version>3.5.1</version>
</dependency>
<dependency>
	<groupId>com.google.zxing</groupId>
	<artifactId>javase</artifactId>
	<version>3.5.1</version>
</dependency>

SecureKey generation and verification code generation comparison class

This is the core implementation class, the main function:

  • Generate a random SecureKey, which is used for external business binding to the specified account, and is also used for subsequent verification code generation
  • Generate corresponding verification codes based on SecureKey and system time

Code reference:

package beinet.cn.googleauthenticatordemo.authenticator;

import org.apache.commons.codec.binary.Base32;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

public class GoogleGenerator {
    
    

    // 发行者(项目名),可为空,注:不允许包含冒号
    public static final String ISSUER = "beinet.cn";

    // 生成的key长度( Generate secret key length)
    public static final int SECRET_SIZE = 32;

    // Java实现随机数算法
    public static final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";

    // 最多可偏移的时间, 假设为2,表示计算前面2次、当前时间、后面2次,共5个时间内的验证码
    static int window_size = 1; // max 17
    static long second_per_size = 30L;// 每次时间长度,默认30秒

    /**
     * 生成一个SecretKey,外部绑定到用户
     *
     * @return SecretKey
     */
    public static String generateSecretKey() {
    
    
        SecureRandom sr;
        try {
    
    
            sr = SecureRandom.getInstance(RANDOM_NUMBER_ALGORITHM);
            sr.setSeed(getSeed());
            byte[] buffer = sr.generateSeed(SECRET_SIZE);
            Base32 codec = new Base32();
            byte[] bEncodedKey = codec.encode(buffer);
            String ret = new String(bEncodedKey);
            return ret.replaceAll("=+$", "");// 移除末尾的等号
        } catch (NoSuchAlgorithmException e) {
    
    
            // should never occur... configuration error
            throw new RuntimeException(e);
        }
    }

    /**
     * 生成二维码所需的字符串,注:这个format不可修改,否则会导致身份验证器无法识别二维码
     *
     * @param user   绑定到的用户名
     * @param secret 对应的secretKey
     * @return 二维码字符串
     */
    public static String getQRBarcode(String user, String secret) {
    
    
        if (ISSUER != null) {
    
    
            if (ISSUER.contains(":")) {
    
    
                throw new IllegalArgumentException("Issuer cannot contain the ':' character.");
            }
            user = ISSUER + ":" + user;
        }
        String format = "otpauth://totp/%s?secret=%s";
        String ret = String.format(format, user, secret);
        if (ISSUER != null) {
    
    
            ret += "&issuer=" + ISSUER;
        }
        return ret;
    }

    /**
     * 验证用户提交的code是否匹配
     *
     * @param secret 用户绑定的secretKey
     * @param code   用户输入的code
     * @return 匹配成功与否
     */
    public static boolean checkCode(String secret, int code) {
    
    
        Base32 codec = new Base32();
        byte[] decodedKey = codec.decode(secret);
        // convert unix msec time into a 30 second "window"
        // this is per the TOTP spec (see the RFC for details)
        long timeMsec = System.currentTimeMillis();
        long t = (timeMsec / 1000L) / second_per_size;
        // Window is used to check codes generated in the near past.
        // You can use this value to tune how far you're willing to go.
        for (int i = -window_size; i <= window_size; ++i) {
    
    
            int hash;
            try {
    
    
                hash = verifyCode(decodedKey, t + i);
            } catch (Exception e) {
    
    
                // Yes, this is bad form - but
                // the exceptions thrown would be rare and a static
                // configuration problem
                e.printStackTrace();
                throw new RuntimeException(e.getMessage());
                // return false;
            }
            System.out.println("input code=" + code + "; count hash=" + hash);
            if (code == hash) {
    
     // addZero(hash)
                return true;
            }
/*            if (code==hash ) {
                return true;
            }*/
        }
        // The validation code is invalid.
        return false;
    }

    private static int verifyCode(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException {
    
    
        byte[] data = new byte[8];
        long value = t;
        for (int i = 8; i-- > 0; value >>>= 8) {
    
    
            data[i] = (byte) value;
        }
        SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1");
        Mac mac = Mac.getInstance("HmacSHA1");
        mac.init(signKey);
        byte[] hash = mac.doFinal(data);
        int offset = hash[20 - 1] & 0xF;
        // We're using a long because Java hasn't got unsigned int.
        long truncatedHash = 0;
        for (int i = 0; i < 4; ++i) {
    
    
            truncatedHash <<= 8;
            // We are dealing with signed bytes:
            // we just keep the first byte.
            truncatedHash |= (hash[offset + i] & 0xFF);
        }
        truncatedHash &= 0x7FFFFFFF;
        truncatedHash %= 1000000;
        return (int) truncatedHash;
    }

    private static byte[] getSeed() {
    
    
        String str = ISSUER + System.currentTimeMillis() + ISSUER;
        return str.getBytes(StandardCharsets.UTF_8);
    }
}

business service implementation

Used to encapsulate 2 methods:

  • Enter the account number, generate and bind SecureKey for the account, and return the QR code URL required by Google Authenticator at the same time
  • Enter the account number and verification code, obtain the SecureKey corresponding to the account, calculate the verification code at the current time, compare it with the input verification code, and return whether it is successful or not

Reference implementation:

package beinet.cn.googleauthenticatordemo.authenticator;

import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.Map;

@Service
public class AuthenticatorService {
    
    
    private Map<String, String> userKeys = new HashMap<>();

    /**
     * 生成一个secretKey,并关联到用户,
     * 然后返回二维码字符串
     *
     * @param username 用户名
     * @return 二维码字符串
     */
    public String generateAuthUrl(String username) {
    
    
        String secret = GoogleGenerator.generateSecretKey();
        // todo: 实际项目中,用户名与secretKey的关联关系应当存储在数据库里,否则变化了,就会无法登录
        userKeys.put(username, secret);
        return GoogleGenerator.getQRBarcode(username, secret);
    }

    /**
     * 根据用户名和输入的code,进行校验并返回成功失败
     *
     * @param username 用户名
     * @param code     输入的code
     * @return 校验成功与否
     */
    public boolean validateCode(String username, int code) {
    
    
        // todo: 从数据库里读取该用户的secretKey
        String secret = userKeys.get(username);
        if (!StringUtils.hasLength(secret)) {
    
    
            throw new RuntimeException("该用户未使用Google身份验证器注册,请先注册");
        }

        return GoogleGenerator.checkCode(secret, code);
    }
}

Login flow embedded

This is modified according to the actual code, such as the project where the front and back ends are separated:

  • When the front-end page is successfully logged in, add the code:
    • Determine whether the current user has been bound. If not bound, display the QR code for the user to bind
    • The OTPCode input interface pops up, and after the second verification is successful, it will jump to the normal business page
  • The back-end business interface needs to judge that both cookies exist before entering, and if one is missing, it will return login failure

Complete demo code

mobile app download

Google AuthenticatorDownload

It is relatively simple to use, and it can be opened directly without logging in on the mobile phone.

Microsoft AuthenticatorDownload

There is a certain degree of security, and a password or fingerprint verification is required to open it.

Guess you like

Origin blog.csdn.net/youbl/article/details/130966981