CAS 单点登录

Java实现 OTP动态口令

2025-03-10  本文已影响0人  饱饱抓住了灵感

一、概述

OTP(One-Time Password)动态口令是一种基于时间戳算法的一次性密码,通常每30或60秒产生一个新口令,要求客户端和服务器能够保持一致的时间来进行验证。OTP动态口令的认证原理是基于共享密钥和时间戳算法,确保每次生成的口令都是唯一的,使用后即作废,从而提高了安全性。

OTP动态口令的实现方式主要有两种:

OTP动态口令广泛应用于多种系统渠道,如Web应用、手机应用、电话应用、ATM自助终端等。由于其简单易用且与系统集成良好,OTP动态口令技术在网络安全管理中得到广泛应用,可以有效降低帐号被冒用、盗用等风险。

OTP动态口令与静态口令的比较
与传统静态口令相比,OTP动态口令具有以下优势:

  1. 安全性更高:每次生成的口令都是唯一的,使用后即作废,难以被猜测和破解。
  2. 防截获:由于口令是动态生成的,黑客难以通过网上或电话线截获静态密码。
  3. 防内部泄露:由于口令是动态的,内部工作人员无法通过合法授权取得用户密码而非法使用。

OTP动态口令的不足
尽管OTP动态口令具有较高的安全性,但其也存在一些不足:

依赖时间同步:客户端和服务器必须能够保持一致的时间,否则无法生成正确的动态口令。
设备依赖:需要使用专门的动态口令生成设备或应用程序,增加了用户的设备负担。

二、TOTP实现

Java

import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

@Component
public class OtpUtil {

    /** 时间步长(秒) */
    private static final int PERIOD = 30;
    /** OtpCode 长度 */
    private static final int DIGITS = 6;
    /** 哈希算法,SHA1,SHA256,SHA512 */
    private static final String ALGORITHM = "SHA256";

    /** 时间容错范围, 0:不容错, 1:前后30秒, 2:前后1分钟 ... 最大值见 {@link #timeErrorRangeMax}  */
    @Value("${login.otp.timeErrorRange:2}")
    private static final int timeErrorRange = 2;
    /** 时间容错范围最大值 */
    private static final int timeErrorRangeMax = 6;


    /** 开启错误过多封禁 */
    private static final boolean isOpenBlock = false;
    /** 最大尝试次数 */
    private static final int MAX_ATTEMPTS = 5;
    /** 封锁时间(毫秒) */
    private static final long BLOCK_DURATION = 300_000;
    /** 尝试次数 */
    private static final ConcurrentHashMap<String, AtomicInteger> attemptCounter = new ConcurrentHashMap<>();
    /** 解禁时间 */
    private static final ConcurrentHashMap<String, Long> blockedUsers = new ConcurrentHashMap<>();


    // 生成的key长度( Generate secret key length)
    private static final int SECRET_SIZE = 10;
    // 随机数生成器的起始值
    private static final String SEED = "8ff7425cc8344021b7f386c1241e87b3";
    // Java实现随机数算法
    private static final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";

    public static String generateSecretKey() {
        SecureRandom sr = null;
        try {
            sr = SecureRandom.getInstance(RANDOM_NUMBER_ALGORITHM);
            sr.setSeed(Base64.decodeBase64(SEED));
            byte[] buffer = sr.generateSeed(SECRET_SIZE);
            Base32 codec = new Base32();
            byte[] bEncodedKey = codec.encode(buffer);
            String encodedKey = new String(bEncodedKey);
            return encodedKey;
        } catch (NoSuchAlgorithmException e) {
            // should never occur... configuration error
        }
        return null;
    }

    /**
     * 获取要生成二维码的Otp地址
     * @param user
     * @param secret
     * @return
     */
    public static String getOtpAuthQrcode(String user, String secret) {
        return QRCodeGenerator.genQRCodeBase64(getOtpAuthUrl(user, secret));
    }

    public static String getOtpAuthUrl(String user, String secret) {
        String format = "otpauth://totp/%s?secret=%s&algorithm=%s&digits=%d&period=%d";
        return String.format(format, user, secret, ALGORITHM, DIGITS, PERIOD);
    }


    /**
     * 生成 TOTP
     */
    public static String generateTOTP(String secret) throws Exception {
        long timeIndex = Instant.now().getEpochSecond() / PERIOD;
        return generateOtp(secret, timeIndex);
    }

    /**
     * 验证OtpCode
     */
    public static boolean validateTOTP(String secret, String otpCode, String userName) throws Exception {
        checkIsBlocked(userName);

        long timeIndex = Instant.now().getEpochSecond() / PERIOD;

        // 在验证窗口内检查 OTP
        boolean isPass = false;
        // 容错范围, 0表达当前时间的结果, -1退后30秒的结果, -2退后1分钟, 1前进30秒的结果, 2前进1分钟的结果
        int end = Math.min(timeErrorRange, timeErrorRangeMax);
        int start = -end;
        for (int i = start; i <= end; i++) {
            String generatedOtp = generateOtp(secret, timeIndex + i);
//            System.out.println(i + "次: generatedOtp = " + generatedOtp);
            if (generatedOtp.equals(otpCode)) {
                isPass = true;
            }
        }
        if (isPass) {
            clearAttempts(userName);
        } else {
            recordFailedAttempt(userName);
        }
        return isPass;
    }

    private static String generateOtp(String secret, long timeIndex) throws Exception {
        // 解码 Base32 密钥
        Base32 base32 = new Base32();
        byte[] keyBytes = base32.decode(secret);

        // 转换时间索引为字节数组
        byte[] timeBytes = ByteBuffer.allocate(8).putLong(timeIndex).array();

        // 使用 HMAC-SHA1 生成哈希
        Mac mac = Mac.getInstance("Hmac" + ALGORITHM);
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "Hmac" + ALGORITHM);
        mac.init(keySpec);
        byte[] hash = mac.doFinal(timeBytes);

        // 提取动态偏移量
        int offset = hash[hash.length - 1] & 0x0F;
        int binary = ((hash[offset] & 0x7F) << 24) | ((hash[offset + 1] & 0xFF) << 16)
                | ((hash[offset + 2] & 0xFF) << 8) | (hash[offset + 3] & 0xFF);

        // 生成 OTP
        int otp = binary % (int) Math.pow(10, DIGITS);
        return String.format("%0" + DIGITS + "d", otp);
    }

    private static void recordFailedAttempt(String userId) {
        if (!isOpenBlock) {
            return;
        }
        attemptCounter.putIfAbsent(userId, new AtomicInteger(0));
        int attempts = attemptCounter.get(userId).incrementAndGet();

        if (attempts >= MAX_ATTEMPTS) {
            blockedUsers.put(userId, System.currentTimeMillis() + BLOCK_DURATION);
        }
    }

    private static void clearAttempts(String userId) {
        if (!isOpenBlock) {
            return;
        }
        attemptCounter.remove(userId);
        blockedUsers.remove(userId);
    }

    private static void checkIsBlocked(String userId) {
        if (!isOpenBlock) {
            return;
        }

        if (!blockedUsers.containsKey(userId)) {
            return;
        }

        Long blockTime = blockedUsers.get(userId);
        if (System.currentTimeMillis() > blockTime) {
            blockedUsers.remove(userId);
            return;
        }
        throw new RuntimeException(String.format("%s错误次数过多已被封禁,请在%s之后重试", userId, DateFormatUtils.format(blockTime, "yyyy-MM-dd HH:mm:ss")));
    }
}

生成二维码工具类

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class QRCodeGenerator {

    private static final int width = 350;  // 二维码的宽度
    private static final int height = 350;  // 二维码的高度

    private static void generateQRCodeImage(String text, String filePath) throws WriterException, IOException {
        QRCodeWriter qrCodeWriter = new QRCodeWriter();
        Map<EncodeHintType, Object> hints = new HashMap<>();
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);  // 设置纠错等级,L为最低,M为中等,Q为较高,H为最高
        hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");  // 设置字符编码为UTF-8
        BitMatrix bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, width, height, hints);
        Path path = FileSystems.getDefault().getPath(filePath);
        MatrixToImageWriter.writeToPath(bitMatrix, "PNG", path);  // 输出图片格式为PNG,也可以选择其他格式如JPG等
    }

    // 你要编码的文本,例如一个网址
    public static String genQrCodeImg(String text, String filePath) {
        try {
            // 输出文件的路径和文件名
            generateQRCodeImage(text, filePath);
            return filePath;
        } catch (WriterException | IOException e) {
            System.err.println("Could not generate QR Code, WriterException | IOException :" + e.getMessage());
            return "";
        }
    }

    public static String genQRCodeBase64(String text) {
        try {
            QRCodeWriter qrCodeWriter = new QRCodeWriter();
            Map<EncodeHintType, Object> hints = new HashMap<>();
            hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);  // 设置纠错等级,L为最低,M为中等,Q为较高,H为最高
            hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");  // 设置字符编码为UTF-8
            BitMatrix bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, 400, 400, hints);
            BufferedImage image = MatrixToImageWriter.toBufferedImage(bitMatrix);
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

            ImageIO.write(image, "png", byteArrayOutputStream);
            byte[] bytes = byteArrayOutputStream.toByteArray();
            String base64 = Base64.getEncoder().encodeToString(bytes);
            return "data:image/png;base64," + base64;
        } catch (WriterException | IOException e) {
            throw new RuntimeException(e);
        }
    }
}
上一篇 下一篇

猜你喜欢

热点阅读