SpringBoot プロジェクトのログインと MFA 二次認証へのアクセス

MFA 多要素認証 ( Multi-Factor Authentication ):
セキュリティを向上させるために ID 認証を必要とする一部のサービス (Web サイトなど) では、通常、アカウントのパスワードが正常にログインした後、ユーザーが正しいユーザーであることを確認するために 2 回目の ID 認証を実行する必要があります。ログインしているユーザーを回避する パスワード漏洩などによりユーザー情報が漏洩する。
ただし、2 回ログインする必要があるため、ユーザー エクスペリエンスは比較的悪くなります。
一般的な使用シナリオ:

  • 企業管理のバックグラウンド、特に CMS 顧客関係管理システム、財務システムなどの機密情報を含むシステム。
  • Amazon ストア管理のバックグラウンドなど、E コマースのバックグラウンドには通常、二次認証が必要です
  • Gitコードリポジトリ
  • サイト上のユーザーの機密情報の閲覧および変更(携帯電話番号の中央の n 桁はデフォルトで非表示になっており、閲覧には二次認証が必要です。パスワードの変更には二次認証が必要です)。

一般的な認証方法は次のとおりです。

  • モバイル SMS 認証コードの認証: テキスト メッセージで認証コードをユーザーに送信し、ユーザーはサービス (Web サイトなど) で認証コードを入力して認証します。
  • 電子メール検証コード検証: 検証コードが電子メールでユーザーに送信され、ユーザーは検証コードを入力して認証します。
  • MFA ハードウェア デバイス: 動的パスワードを生成するハードウェア機能をユーザーに割り当て、ユーザーはパスワードを入力して認証します。
    コンピュータに接続することで自動的に認証できるハードウェア デバイスもあります
  • MFA ソフトウェア: ユーザーがコンピューターまたは携帯電話にソフトウェアをインストールすると、ソフトウェアが動的なパスワードを生成し、ユーザーがパスワードを入力して認証します。
  • 生体認証: 指紋や顔認識などの生体認証機能による認証
  • スマート カード: ID 情報を含むカードをユーザーに割り当て、ユーザーは認証のためにサービスのカード読み取りデバイスにカードを置きます。

この記事では、Web サイトの MFA ソフトウェア アクセス ソリューションのみを紹介し、認証にはモバイル アプリケーションを使用します。
時刻同期アルゴリズムに基づく携帯電話アプリケーションであれば、以下のようなアプリケーションに対応できます。

  • Google認証システム(Google認証システム)
  • Microsoft Authenticator (Microsoft Authenticator)
    注: この種の動的パスワード認証は、通常、OTP コード (ワンタイム パスワード)、OTP トークン、2 段階認証、二次認証、2FA などとも呼ばれます。

フロントエンドとバックエンド間の対話プロセスは次のとおりです。

バックエンドは 3 つのインターフェイスを提供します。

  • アカウントパスワードログインインターフェース
  • アカウントが SecureKey インターフェイスにバインドされているかどうか
  • 二次認証コード検証インターフェース

完全な対話型フローチャートは次のとおりです。
ここに画像の説明を挿入

SpringBootプロジェクトアクセスの実装

SpringBootプロジェクトが作成されているものとします。

依存関係を追加する

pom.xmlファイルを開き、次の依存関係を追加します。

<!-- 用于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 生成と検証コード生成の比較クラス

これはコア実装クラス、つまりメイン関数です。

  • ランダムな SecureKey を生成します。これは、指定されたアカウントへの外部ビジネス バインディングに使用され、その後の検証コードの生成にも使用されます。
  • SecureKey とシステム時間に基づいて、対応する検証コードを生成します。

コードリファレンス:

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

ビジネスサービスの実装

2 つのメソッドをカプセル化するために使用されます。

  • アカウント番号を入力し、アカウントの SecureKey を生成してバインドし、同時に Google Authenticator に必要な QR コード URL を返します。
  • アカウント番号と認証コードを入力し、アカウントに対応するSecureKeyを取得し、現時点での認証コードを計算し、入力された認証コードと比較し、成功したか否かを返します。

リファレンス実装:

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

埋め込まれたログインフロー

これは、フロントエンドとバックエンドが分離されているプロジェクトなど、実際のコードに従って変更されます。

  • フロントエンド ページに正常にログインしたら、次のコードを追加します。
    • 現在のユーザーがバインドされているかどうかを確認します。バインドされていない場合は、バインドするユーザーの QR コードを表示します。
    • OTPCode 入力インターフェイスがポップアップし、2 回目の検証が成功すると、通常のビジネス ページにジャンプします。
  • バックエンド ビジネス インターフェイスは、入力する前に両方の Cookie が存在するかどうかを判断する必要があり、どちらかが欠落している場合はログイン失敗を返します。

完全なデモコード

モバイルアプリのダウンロード

Google Authenticatorダウンロード

使い方は比較的簡単で、携帯電話にログインせずに直接開くことができます。

Microsoft Authenticatorダウンロード

ある程度のセキュリティはあり、開くにはパスワードや指紋認証が必要です。

おすすめ

転載: blog.csdn.net/youbl/article/details/130966981