龙空技术网

Java实现OTP动态口令认证demo

蜗牛学技术 91

前言:

今天咱们对“java口令”大约比较珍视,同学们都想要分析一些“java口令”的相关知识。那么小编也在网上网罗了一些关于“java口令””的相关内容,希望兄弟们能喜欢,看官们一起来学习一下吧!

概念

一次性密码(One Time Password,简称OTP),又称“一次性口令”,是指只能使用一次的密码。一次性密码是根据专门算法、每隔60秒生成一个不可预测的随机数字组合,iKEY一次性密码已在金融 (opens new window)、电信 (opens new window)、网游 (opens new window)等领域被广泛应用,有效地保护了用户的安全。

一般的静态密码在安全性上容易因为木马 (opens new window)与键盘侧录程序 (opens new window)等而被窃取,而只要花上相当程度的时间,也有可能被暴力破解 (opens new window)。为了解决一般密码容易遭到破解情况,因此开发出一次性密码的解决方案。

在平时生活中,我们接触一次性密码的场景非常多,比如在登录账号、找回密码,更改密码和转账操作等等这些场景,其中一些常用到的方式有:

手机短信+短信验证码;邮件+邮件验证码;认证器软件+验证码,比如Microsoft Authenticator App,Google Authenticator App等等;硬件+验证码:比如网银的电子密码器;

这些场景的流程一般都是在用户提供了账号+密码的基础上,让用户再提供一个一次性的验证码来提供一层额外的安全防护。通常情况下,这个验证码是一个6-8位的数字,只能使用一次或者仅在很短的时间内可用(比如5分钟以内)

#形式

OTP从技术来分有三种形式, 时间同步、事件同步、挑战/应答。

#时间同步

原理是基于 动态令牌和 动态口令验证服务器的时间比对,基于 时间同步的 令牌,一般每60秒产生一个新口令,要求服务器能够十分精确的保持正确的时钟,同时对其令牌的晶振频率有严格的要求,这种技术对应的终端是硬件令牌。

#事件同步

基于事件同步的令牌,其原理是通过某一特定的事件次序及相同的种子值作为输入,通过HASH算法中运算出一致的密码。

#挑战/应答

常用于的网上业务,在网站/应答上输入 服务端下发的 挑战码, 动态令牌输入该挑战码,通过内置的算法上生成一个6/8位的随机数字,口令一次有效,这种技术目前应用最为普遍,包括刮刮卡、短信密码、动态令牌也有挑战/应答形式。 主流的动态令牌技术是时间同步和挑战/应答两种形式。

#OTP基本原理

计算OTP串的公式

OTP(K,C) = Truncate(HMAC-SHA-1(K,C))
K: 表示秘钥串,这个密钥的要求是每个 HOTP 的生成器都必须是唯一的。一般我们都是通过一些随机生成种子的库来实现。C: RFC 中把它称为移动元素(moving factor)是一个 8个 byte的数值,而且需要服务器和客户端同步。HMAC-SHA-1: 表示使用SHA-1做HMAC;Truncate: 是一个函数,就是怎么截取加密后的串,并取加密后串的哪些字段组成一个数字。#HOTP原理

HOTP(HMAC-Based One Time Password) 即是基于 HMAC(基于Hash的消息认证码)实现的一次性密码。算法细节定义在RFC4226 (opens new window).

HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))

一般规定 HOTP 的散列函数使用 SHA2,即:基于SHA-256 或者SHA-512[SHA2 (opens new window)] 的散列函数做事件同步验证。

步骤文解:

使用 HMAC-SHA-1 算法基于 K 和 C 生成一个20个字节的十六进制字符串(HS)。关于如何生成这个是另外一个协议来规定的,RFC 2104 HMAC Keyed-Hashing for Message Authentication (opens new window). 实际上这里的算法并不唯一,还可以使用 HMAC-SHA-256HMAC-SHA-512 生成更长的序列。对应到协议中的算法标识就是HS = HMAC-SHA-1(K,C) 1选择这个20个字节的十六进制字符串(HS 下文使用 HS 代替 )的最后一个字节,取其低4位数并转化为十进制。比如图中的例子,第二个字节是 5a,第四位就是 a,十六进制也就是 0xa,转化为十进制就是 10。该数字我们定义为 Offset,也就是偏移量。根据偏移量 Offset,我们从 HS 中的第 10(偏移量)个字节开始选取 4 个字节,作为我们生成 OTP 的基础数据。图中例子就是选择 50ef7f19,十六进制表示就是 0x50ef7f19,我们称为 Sbits将上一步4个字节的十六进制字符串 Sbits 转化为十进制,然后用该十进制数对 10的Digit次幂 进行取模运算。其原理很简单根据取模运算的性质,比如 比10大的数 MOD 10 结果必然是 0到9, MOD 100 结果必然是 0-99。图中的例子,50ef7f19 转化为十进制为 1357872921,然后如果需要6位 OTP 验证码,则 1357872921 MOD 10^6 = 872921。 872921 就是我们最终生成的 OTP。这一步可能还需要注意一点就是图中案例 Digit 不能超过10,因为即使超过10,1357872921 取模后也不会超过10位了。所以如果我们希望获取更长的验证码,需要在三步中拿到更多的十六进制字节,从而得到更大的十进制数。这个十进制决定了生成的 OTP 编码的长度。#TOTP原理

TOTP 算法的关键在于如何更具当前时间和时间窗口计算出计数,也就是如何根据当前时间和 X 来计算 HOTP 算法中的 C。

HOTP 算法中的 C 是使用当前 Unix 时间戳 减去初始计数时间戳,然后除以时间窗口而获得的。

C = (T - T0) / X;
T 为当前时间T0 从哪个时间开始,一般取值为 0.X 表示时间步数,也就是说多长时间产生一个动态密码,这个时间间隔就是时间步数X,比如30秒;#代码实现
 				<!-- Java OTP 依赖 -->        <dependency>            <groupId>com.google.zxing</groupId>            <artifactId>core</artifactId>            <version>3.3.3</version>        </dependency>        <dependency>            <groupId>com.google.zxing</groupId>            <artifactId>javase</artifactId>            <version>3.3.3</version>        </dependency>        <dependency>            <groupId>commons-codec</groupId>            <artifactId>commons-codec</artifactId>            <version>1.15</version>        </dependency>

GoogleGeneratorUtil

package com.springboot.demo.util;import lombok.extern.slf4j.Slf4j;import org.apache.commons.codec.binary.Base32;import org.apache.commons.codec.binary.Base64;import org.springframework.stereotype.Component;import javax.crypto.Mac;import javax.crypto.spec.SecretKeySpec;import java.security.InvalidKeyException;import java.security.NoSuchAlgorithmException;import java.security.SecureRandom;import java.util.Objects;@Slf4j@Componentpublic class GoogleGeneratorUtil {    // 生成的key长度( Generate secret key length)    public static final int SECRET_SIZE = 10;    public static final String SEED = "22150146801713967E8g";    // Java实现随机数算法    public static final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";    // 最多可偏移的时间    private static int OTP_WINDOW_SIZE = 1; // default 1 - max 17    /**     * 创建密钥     *     * @return {@link String}     */    public static String generateSecretKey() throws NoSuchAlgorithmException {        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            log.error("otp生成SecretKey失败", e);            throw e;        }    }    /**     * 这个format不可以修改,身份验证器无法识别二维码     * 二维码 url 格式     * @param user     * @param secret     * @return     */    public static String getQRBarcodeStr(String user, String secret) {        String format = "otpauth://totp/Otpdemo:%s?secret=%s&issuer=Otpdemo";        return String.format(format, user, secret);    }    public static boolean check_code(String secret, String code, long timeMsec) {        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 t = (timeMsec / 1000L) / 30L;        // 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 = -OTP_WINDOW_SIZE; i <= OTP_WINDOW_SIZE; ++i) {            long hash;            try {                hash = verify_code(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 ();                log.error("失败", e);                throw new RuntimeException(e.getMessage(), e);                // return false;            }            if (Objects.equals(code, addZero(hash))) {                return true;            }        }        // The validation code is invalid.        return false;    }    public static int verify_code(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;    }    public static String addZero(long code) {        return String.format("%06d", code);    }//    @NacosValue(value = "${otp.window.size}", autoRefreshed = true)    int otpWindowSize = 1;    public void setOtpWindowSize(int otpWindowSize) {        if (otpWindowSize > 17 || otpWindowSize < 0) {            throw new RuntimeException("otp.window.size最大值是17,最小为1");        }        OTP_WINDOW_SIZE = otpWindowSize;    }}

QrCodeUtil

package com.springboot.demo.util;import com.google.zxing.BarcodeFormat;import com.google.zxing.EncodeHintType;import com.google.zxing.MultiFormatWriter;import com.google.zxing.WriterException;import com.google.zxing.common.BitMatrix;import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;import lombok.extern.slf4j.Slf4j;import org.apache.tomcat.util.codec.binary.Base64;import javax.imageio.ImageIO;import java.awt.image.BufferedImage;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.util.Hashtable;@Slf4jpublic class QrCodeUtil {    private static final int BLACK = 0xFF000000;    private static final int WHITE = 0xFFFFFFFF;    public static String encode(String contents) throws Exception {        try {            BufferedImage bufferedImage = toBufferedImage(contents);            return writeToBase64(bufferedImage, "png");        } catch (Exception e) {            log.error("生成二维码base64字符串失败", e);            throw e;        }    }    private static String writeToBase64(BufferedImage bufferedImage, String format) throws IOException {        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();        ImageIO.write(bufferedImage, format, byteArrayOutputStream);        return Base64.encodeBase64String(byteArrayOutputStream.toByteArray());    }    private static BufferedImage toBufferedImage(String contents) throws WriterException {        Hashtable<EncodeHintType, Object> hintTypeObjectHashtables = new Hashtable<>();        hintTypeObjectHashtables.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);        hintTypeObjectHashtables.put(EncodeHintType.CHARACTER_SET, "utf-8");        hintTypeObjectHashtables.put(EncodeHintType.MARGIN, 1);        BitMatrix bitMatrix = new MultiFormatWriter().encode(contents, BarcodeFormat.QR_CODE, 500, 500,                hintTypeObjectHashtables);        int width = bitMatrix.getWidth();        int height = bitMatrix.getHeight();        BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);        for (int i = 0; i < width; i++) {            for (int j = 0; j < height; j++) {                bufferedImage.setRGB(i, j, bitMatrix.get(i, j) == true ? BLACK : WHITE);            }        }        return bufferedImage;    }}

测试验证

import com.springboot.demo.util.GoogleGeneratorUtil;import com.springboot.demo.util.QrCodeUtil;import org.apache.commons.codec.binary.Base32;/** * @author li * @version 1.0 * @date 2023/05/11 09:58 */public class testOtp {    public static void main(String[] args) throws Exception {        //生成秘钥        String secret = GoogleGeneratorUtil.generateSecretKey();        //生成认证url        String qrcode = GoogleGeneratorUtil.getQRBarcodeStr("test", secret);        //二维码Base64        String qrcodeBase64 = QrCodeUtil.encode(qrcode);        System.out.println(qrcode);        System.out.println(qrcodeBase64);        //生成6位otp验证码        Base32 codec = new Base32();        byte[] decodedKey = codec.decode(secret);        long t = (System.currentTimeMillis() / 1000L) / 30L;        int realOtpCode = GoogleGeneratorUtil.verify_code(decodedKey,t+1);        //校验otp6位验证码        String otpCode = GoogleGeneratorUtil.addZero(realOtpCode);        System.out.println("otpCode: "+  otpCode);        //校验otp6位数字是否正确        Boolean flag = GoogleGeneratorUtil.check_code(secret,otpCode,System.currentTimeMillis());        System.out.println(flag);    }}

标签: #java口令