加密与安全_HOTP一次性密码生成算法
文章目录
- HOTP 的基础原理
- HOTP 的工作流程
- HOTP 的应用场景
- HOTP 的安全性
- 安全性增强措施
- Code
- 生成HOTP
- 可配置项
- 校验HOTP
- 可拓展功能
- 计数器(counter)
- 计数器在客户端和服务端的作用
- 计数器的同步机制
- 客户端和服务端中的计数器表现
- 服务端如何处理计数器不同步
- 计数器在客户端和服务端的举例
- 如何在 Java 实现中体现计数器
- 小结
- 一个服务端程序应对多个客户端
- 关键问题
- 解决方案
- 1. 计数器的存储和管理
- 2. 服务端管理多个客户端计数器的架构
- 3. 具体实现步骤
- 4. Java 示例代码
- 关键点解释
- 进一步优化
- 小结
- 计数器容错机制
- 验证失败常见原因
- 1. 计数器不同步
- 2. 密钥不匹配
- 3. 编码问题
- 4. 生成 OTP 时计数器没有递增
- 5. 输入 OTP 错误
- 解决方案
HOTP 的基础原理
HOTP 是基于 HMAC(Hash-based Message Authentication Code)算法的一种一次性密码生成机制。其核心思想是通过计数器的变化和共享密钥生成一次性密码。每次使用时,计数器递增,因此每个密码只能使用一次。 它遵循 RFC 4226 标准。
核心组件:
- 共享密钥(K):服务器和客户端事先约定并保存的密钥。
- 计数器(C):每生成一个一次性密码,计数器值增加,确保密码的唯一性。
- HMAC 算法:使用 HMAC-SHA-1 或其他 HMAC 哈希算法,结合共享密钥和计数器生成动态密码。
生成公式:
HOTP(K, C) = HMAC-SHA-1(K, C) mod 10^6
其中:
K
是共享密钥。C
是计数器。- 输出值取前 6 位数字(或更多,取决于配置),通常为 6 位数字密码。
HOTP 的工作流程
HOTP 的密码生成和验证基于计数器的增量。具体步骤如下:
-
密码生成:
- 客户端和服务器预先共享一个密钥
K
。 - 每次生成密码时,客户端使用当前计数器值
C
和密钥K
计算 HMAC 值。 - 从 HMAC 结果中截取 6 位或 8 位数字,生成一次性密码。
- 客户端和服务器预先共享一个密钥
-
密码验证:
- 服务器接收到客户端的密码后,使用相同的共享密钥
K
和当前计数器值C
生成 HMAC 值。 - 如果生成的 HMAC 值与客户端提供的密码匹配,认证成功,服务器递增计数器
C
。 - 服务器通常允许一定的容错窗口(如 ±2 个计数器值),以防止由于计数器不同步导致的验证失败。
- 服务器接收到客户端的密码后,使用相同的共享密钥
HOTP 的应用场景
HOTP 广泛应用于需要基于事件或计数器的系统中,典型场景包括:
- 硬件令牌:银行、企业安全系统等早期使用的物理设备,用户通过令牌生成动态密码。
- 基于事件的身份验证系统:每当发生某些特定事件(如用户发起登录请求或支付请求)时,系统使用 HOTP 生成密码。
- 离线环境:由于 HOTP 不依赖时间,因此在网络连接不稳定或设备无法持续联网的场景下尤为适用。
HOTP 的安全性
优势:
- 基于事件驱动:HOTP 的密码生成依赖于计数器,用户可以在不依赖时间同步的情况下生成一次性密码,适用于网络不稳定或离线操作场景。
- 兼容性强:HOTP 算法简单,易于实现,且支持广泛的设备和系统。
- 无时间同步问题:由于它基于计数器而非时间,客户端和服务器之间无需保持时间同步。
潜在问题:
- 密码有效期较长:HOTP 密码在未使用前一直有效,因此可能被攻击者截取后重放。这一点使得它在安全性方面较 TOTP 弱。
- 计数器同步问题:客户端和服务器的计数器需要同步。如果用户多次生成密码但没有使用,则可能导致计数器不同步。为此,服务器通常允许一个容错窗口,但这也可能被攻击者利用来猜测计数器的值。
- 有限容错窗口可能被滥用:服务器在验证时允许的容错窗口可能导致暴力破解攻击,即攻击者尝试多个计数器值,直到找到有效的密码。
安全性增强措施
- 设置较短的密码有效期:在服务端设置较短的密码有效期,确保未使用的 HOTP 密码快速失效。
- 配合其他身份验证手段:与二次身份验证或生物识别等方法结合使用,防止密码被重放攻击或暴力破解。
- 动态调整容错窗口:服务器可以根据用户行为动态调整容错窗口的大小,以减少密码被暴力破解的风险。
Code
生成HOTP
基于 HMAC-SHA-256 算法生成一次性密码(OTP)。
我们使用了 javax.crypto
包中的 HMAC 相关类来实现 HMAC-SHA-356 算法,并生成 6 位的 OTP。
package com.artisan.otp.hotp;import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;/*** @author 小工匠* @version 1.0* @date 2024/10/1 21:22* @mark: show me the code , change the world*/public class HOTP {// 生成HOTP码public static String generateHOTP(String key, long counter, int digits) throws Exception {// 将key转换为字节数组byte[] keyBytes = hexStr2Bytes(key);// 计算HMAC-SHA-1byte[] counterBytes = longToBytes(counter);byte[] hmacResult = hmacSHA1(keyBytes, counterBytes);// 获取动态截取码(Dynamic Truncation)int otp = dynamicTruncate(hmacResult) % (int) Math.pow(10, digits);// 格式化OTP为固定长度,不足位数用0填充return String.format("%0" + digits + "d", otp);}// 使用HmacSHA256生成hashprivate static byte[] hmacSHA1(byte[] key, byte[] counter) throws Exception {// HmacSHA1 HmacSHA256SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA256");Mac mac = Mac.getInstance("HmacSHA256");mac.init(signKey);return mac.doFinal(counter);}// 动态截取(Dynamic Truncation)private static int dynamicTruncate(byte[] hmacResult) {// 取HMAC结果的最后字节的低4位作为偏移量int offset = hmacResult[hmacResult.length - 1] & 0xf;// 从偏移位置起,取4个字节组成一个31位的整数return ((hmacResult[offset] & 0x7f) << 24) |((hmacResult[offset + 1] & 0xff) << 16) |((hmacResult[offset + 2] & 0xff) << 8) |(hmacResult[offset + 3] & 0xff);}// 将long类型的计数器转换为字节数组(8字节)private static byte[] longToBytes(long value) {return BigInteger.valueOf(value).toByteArray();}// 将十六进制字符串转换为字节数组private static byte[] hexStr2Bytes(String hex) {byte[] bytes = new byte[hex.length() / 2];for (int i = 0; i < bytes.length; i++) {bytes[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);}return bytes;}public static void main(String[] args) {try {// 测试HOTP生成String secret = "3132333435363738393031323334353637383930"; // 十六进制密钥long counter = 1; // 计数器int digits = 6; // OTP长度String hotp = generateHOTP(secret, counter, digits);System.out.println("Generated HOTP: " + hotp);} catch (Exception e) {e.printStackTrace();}}
}
代码解释
- 密钥 (
key
):密钥为一个十六进制字符串表示的共享密钥,转换为字节数组用于生成 HMAC。 - 计数器 (
counter
):每次生成新的 OTP 时,计数器递增。这是 HOTP 的核心机制。 - HMAC-SHA256:使用
javax.crypto.Mac
类生成 HMAC-SHA256哈希值。 - 动态截取 (
dynamicTruncate
):HOTP 的最后步骤,根据哈希结果的偏移量提取 31 位的数值,然后对其取模来生成 6 位的一次性密码。
运行结果
在 main
方法中,使用一个示例密钥和计数器来生成 6 位的 HOTP。例如,如果你使用的计数器为 1,生成的 HOTP 可能是 638487
。
可配置项
- 密钥长度:可以使用更长的密钥,建议密钥使用至少 160 位或 256 位的密钥(如 SHA-256 或更高)。
- OTP 位数:
digits
参数允许生成 6 位、7 位或 8 位等不同长度的 OTP。
校验HOTP
为了验证 HOTP,我们需要客户端生成的 OTP 与服务器端生成的 OTP 一致。由于 HOTP 依赖于共享密钥和计数器,所以我们要确保在客户端和服务器端使用相同的密钥和计数器值。
验证 HOTP 的 Java 实现
在下面的实现中,服务器会接受用户输入的 OTP,并与使用相同计数器生成的 OTP 进行比较。如果 OTP 匹配,则验证成功。
package com.artisan.otp.hotp;
import java.util.Scanner;public class HOTPVerifier2 {// 验证HOTPpublic static boolean verifyHOTP(String key, long counter, String inputOTP, int digits) throws Exception {// 生成服务器端的HOTPString serverHOTP = HOTP.generateHOTP(key, counter, digits);// 比较用户输入的OTP和服务器生成的HOTPreturn serverHOTP.equals(inputOTP);}public static void main(String[] args) {try {// 设置密钥和计数器String secret = "3132333435363738393031323334353637383930"; // 与客户端一致的十六进制密钥long counter = 1; // 当前计数器值int digits = 6; // OTP长度Scanner scanner = new Scanner(System.in);String inputOTP;boolean isValid;// 允许用户多次输入OTP进行验证while (true) {// 生成服务器端的HOTPString expectedHOTP = HOTP.generateHOTP(secret, counter, digits);System.out.println("服务器生成的 HOTP: " + expectedHOTP);// 提示用户输入OTPSystem.out.print("请输入 OTP(或输入 'exit' 退出):");inputOTP = scanner.nextLine();// 如果用户输入 'exit' 则退出程序if (inputOTP.equalsIgnoreCase("exit")) {System.out.println("退出验证程序。");break;}// 验证用户输入的OTP是否正确isValid = verifyHOTP(secret, counter, inputOTP, digits);if (isValid) {System.out.println("验证成功,OTP正确!");// 验证成功后增加计数器counter++;} else {System.out.println("验证失败,OTP不正确!");}System.out.println();}} catch (Exception e) {e.printStackTrace();}}
}
verifyHOTP
:该方法用于验证客户端生成的 OTP 是否与服务器生成的 OTP 相同。它会调用HOTP.generateHOTP
方法生成服务器端的 OTP,然后将其与用户输入的 OTP 进行比较。- 密钥 (
key
):服务器和客户端必须使用相同的密钥。此密钥在初始化时由双方共享。 - 计数器 (
counter
):计数器确保每次生成的 OTP 都是唯一的。每验证一次 OTP,计数器需要递增。 - OTP 位数 (
digits
):定义 OTP 的位数,如 6 位、7 位或 8 位。
验证流程
- 生成服务器端的 OTP:服务器根据密钥和计数器生成一个 OTP。
- 用户输入 OTP:用户在客户端生成 OTP 后,在服务器端输入以进行验证。
- 服务器验证 OTP:服务器通过比较自己生成的 OTP 和用户输入的 OTP,决定是否验证成功。
服务器和客户端使用相同的计数器值和密钥,所以 OTP 匹配。如果输入的 OTP 错误,服务器会显示验证失败。
可拓展功能
- 计数器同步:在实际应用中,计数器同步可能出现问题。你可以实现一个容错机制,允许服务器在一定范围内(例如,±2个计数器)接受 OTP 。
- 安全提示:为了提升安全性,可以结合其他验证方式,比如 IP 白名单、二次验证或时间限制等。
计数器(counter)
在 HOTP(HMAC-based One Time Password)机制中,计数器(counter) 是保证 OTP(一次性密码)唯一性和安全性的重要组成部分。它在客户端和服务端都存在,并且双方必须保持同步。
计数器在客户端和服务端的作用
-
客户端:每次用户请求生成 OTP 时,客户端都会使用当前的计数器值与共享密钥,通过 HMAC-SHA-256 算法生成一次性密码(OTP)。客户端自己维护计数器,每生成一次 OTP,计数器可以递增。
-
服务端:当服务端验证用户输入的 OTP 时,它也使用同样的密钥和计数器生成相同的 OTP。为了验证成功,服务端的计数器必须和客户端的计数器同步或相差在一个容忍窗口内(如±2)。每次验证成功后,服务端也需要递增计数器以准备下一次验证。
计数器的同步机制
-
初始同步:客户端和服务端通常从一个初始值开始,比如 0 或 1。初始值在客户端和服务端协商时确定。由于计数器是由双方共享的,客户端和服务端在初次使用时保持一致。
-
递增:每次生成 OTP 后,客户端和服务端的计数器都会递增。每当客户端生成一个 OTP 并输入,服务器验证后也递增计数器。这样,保证双方的计数器同步,并确保下一个 OTP 的生成不会重复。
客户端和服务端中的计数器表现
-
客户端计数器
- 每当用户请求生成 OTP 时,客户端会读取当前计数器值,生成 OTP。
- 在用户提交 OTP 后,客户端可以选择递增计数器以生成下一个 OTP。
- 如果客户端的计数器与服务端的计数器同步,它们生成的 OTP 是相同的。
-
服务端计数器
- 服务端在接收到用户提交的 OTP 时,会使用自己维护的计数器值生成 OTP,并将其与用户的 OTP 进行比较。
- 如果 OTP 匹配,服务端验证成功,并递增计数器。
- 如果 OTP 不匹配,服务端可以尝试在一定的计数器范围内(如 ±1 或 ±2)进行匹配,以防止客户端和服务端的计数器不同步。
服务端如何处理计数器不同步
由于某些原因(如客户端多次生成 OTP 而没有使用,网络延迟等),客户端和服务端的计数器可能会不同步。服务端可以通过以下策略处理这种情况:
-
容错窗口(window):服务端在验证 OTP 时,可以尝试在当前计数器值的基础上,向前或向后偏移几个计数器值。例如,服务端可以尝试使用
counter ± 1
或counter ± 2
的值进行 OTP 验证。这种方式允许计数器有一个容错范围,避免客户端和服务端不同步导致验证失败。 -
重同步机制:某些系统中,会通过一个重新同步流程来重新对齐客户端和服务端的计数器。例如,如果检测到计数器不同步,可以让客户端和服务端通过一个安全通道重新协商新的计数器值。
-
计数器更新:服务端在验证成功后,会更新自己的计数器值,使其与客户端保持同步。
计数器在客户端和服务端的举例
-
客户端生成 OTP
- 假设当前客户端的计数器为 5,密钥为
"secret"
, 生成 OTP 为123456
。 - 客户端显示 OTP 并递增计数器,计数器更新为 6。
- 假设当前客户端的计数器为 5,密钥为
-
服务端验证 OTP
- 服务端接收到 OTP
123456
,当前计数器也是 5(与客户端同步)。 - 服务端生成 OTP
123456
并验证成功,然后将计数器更新为 6。
- 服务端接收到 OTP
-
客户端下一次生成 OTP
- 客户端使用计数器值 6 生成新的 OTP(例如,
654321
),提交给服务端。 - 服务端的计数器也为 6,生成相同的 OTP 并验证成功,继续递增计数器。
- 客户端使用计数器值 6 生成新的 OTP(例如,
如何在 Java 实现中体现计数器
在 Java 实现中,计数器可以作为一个持久化的变量,客户端和服务端都需要维护各自的计数器值。以下是计数器的处理流程:
// 假设客户端生成OTP时计数器值为5
long clientCounter = 5;
String clientHOTP = HOTP.generateHOTP(secret, clientCounter, digits);
System.out.println("客户端生成的 OTP: " + clientHOTP);// 假设服务端当前计数器值为5
long serverCounter = 5;
boolean isValid = HOTPVerifier.verifyHOTP(secret, serverCounter, clientHOTP, digits);
if (isValid) {System.out.println("验证成功,计数器同步!");// 递增服务端的计数器serverCounter++;
} else {System.out.println("验证失败,可能计数器不同步。");
}
小结
- 客户端和服务端的计数器同步 是 HOTP 正常工作的核心。
- 容错机制 允许一定范围内的计数器不同步,以提升用户体验。
- 计数器持久化 是关键:客户端和服务端在每次生成或验证后都要更新并保存计数器的值。
一个服务端程序应对多个客户端
当一个服务端程序需要处理多个客户端的 HOTP 验证时,计数器(counter) 的管理变得更加复杂,因为每个客户端都会有自己的计数器,并且需要和服务端保持同步。为了确保 OTP 的唯一性和正确性,服务端必须为每个客户端维护独立的计数器,并正确更新计数器的状态。
关键问题
- 每个客户端都有独立的计数器:每个客户端的计数器必须单独管理,因为每个客户端的 OTP 会根据各自的计数器生成。
- 持久化计数器:服务端需要确保计数器在每次 OTP 验证后持久化,以避免计数器不同步的风险。如果服务重启或在不同会话间,需要从持久化存储中加载计数器。
- 并发控制:当多个客户端同时发起 OTP 请求时,服务端需要确保对同一个客户端的计数器读写操作是线程安全的,以防计数器状态被并发修改。
解决方案
1. 计数器的存储和管理
服务端可以为每个客户端使用独立的存储(如数据库、内存缓存等)来持久化和管理计数器。通常,可以通过客户端的唯一标识符(如用户 ID、设备 ID 等)来关联计数器。
- 存储方式:
- 数据库:可以使用关系型数据库(如 MySQL、PostgreSQL)或 NoSQL 数据库(如 Redis、MongoDB)来存储每个客户端的计数器。
- 内存缓存:在高性能环境中,使用内存缓存(如 Redis)存储计数器,以便快速访问和更新。
2. 服务端管理多个客户端计数器的架构
服务端需要为每个客户端分配唯一的计数器。以下是服务端如何处理多个客户端的示例架构:
-
客户端身份识别:服务端需要使用某种方式来识别每个客户端(例如,用户 ID 或设备 ID)。这样可以保证每个客户端有唯一的计数器。
-
计数器存储:每个客户端的计数器可以存储在数据库或缓存中,按客户端唯一 ID 来索引。
-
计数器读写的同步处理:在多线程或并发请求的场景下,确保对计数器的读写是线程安全的。可以使用锁机制来确保同一时刻只有一个请求在修改某个客户端的计数器。
3. 具体实现步骤
-
生成 OTP:
- 当客户端请求生成 OTP 时,服务端从数据库或缓存中读取该客户端的计数器,生成 OTP,并将计数器递增。
-
验证 OTP:
- 当客户端发送 OTP 给服务端验证时,服务端读取该客户端的计数器,并用相同的密钥生成 OTP。
- 服务端还可以允许一定范围的容错窗口(如
counter ± 2
)来应对客户端和服务端的计数器不同步问题。 - 验证成功后,更新计数器并持久化。
4. Java 示例代码
以下是处理多个客户端的伪代码示例,展示了如何为每个客户端维护独立的计数器。
package com.artisan.otp.hotp;import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class HOTPServer {// 使用ConcurrentHashMap存储每个客户端的计数器,key为客户端ID,value为计数器值private ConcurrentHashMap<String, Long> clientCounters = new ConcurrentHashMap<>();// 使用一个锁来保证对每个客户端计数器的线程安全访问private Lock lock = new ReentrantLock();// 模拟数据库存储的客户端密钥private ConcurrentHashMap<String, String> clientSecrets = new ConcurrentHashMap<>();public HOTPServer() {// 初始化假设有两个客户端,每个客户端都有一个共享密钥clientSecrets.put("client1", "3132333435363738393031323334353637383930");clientSecrets.put("client2", "3132333435363738393031323334353637383931");// 初始化计数器clientCounters.put("client1", 1L);clientCounters.put("client2", 1L);}// 生成OTP的函数,传入客户端IDpublic String generateOTP(String clientId) throws Exception {lock.lock(); // 锁定以确保计数器的安全访问try {// 获取客户端的计数器和密钥Long counter = clientCounters.get(clientId);String secret = clientSecrets.get(clientId);if (counter == null || secret == null) {throw new Exception("客户端未注册");}// 生成OTPString otp = HOTP.generateHOTP(secret, counter, 6);// 递增计数器并更新clientCounters.put(clientId, counter + 1);return otp;} finally {lock.unlock(); // 解锁}}// 验证OTP的函数public boolean verifyOTP(String clientId, String inputOTP) throws Exception {lock.lock();try {// 获取客户端的计数器和密钥Long counter = clientCounters.get(clientId);String secret = clientSecrets.get(clientId);if (counter == null || secret == null) {throw new Exception("客户端未注册");}// 生成服务器端的OTPString serverOTP = HOTP.generateHOTP(secret, counter, 6);// 验证客户端输入的OTPif (serverOTP.equals(inputOTP)) {// 验证成功后更新计数器clientCounters.put(clientId, counter + 1);return true;} else {return false;}} finally {lock.unlock();}}public static void main(String[] args) {try {HOTPServer server = new HOTPServer();String clientId = "client1";// 服务端生成OTPString otp = server.generateOTP(clientId);System.out.println("生成的 OTP: " + otp);// 客户端提交OTPboolean isValid = server.verifyOTP(clientId, otp);System.out.println("验证结果: " + (isValid ? "成功" : "失败"));} catch (Exception e) {e.printStackTrace();}}
}
关键点解释
-
ConcurrentHashMap
:使用ConcurrentHashMap
存储每个客户端的计数器,这样可以在并发情况下安全地处理多个客户端的请求。 -
线程安全:使用
ReentrantLock
确保对计数器的读写是线程安全的。每当一个客户端请求生成或验证 OTP 时,服务端会锁定该客户端的计数器,防止并发修改。 -
独立管理多个客户端:通过客户端唯一的 ID(如
clientId
),服务端可以独立管理每个客户端的计数器和密钥。 -
计数器递增:每次成功生成或验证 OTP 后,服务端都会递增相应客户端的计数器并持久化,以确保下次生成的 OTP 是唯一的。
进一步优化
-
计数器持久化:在生产环境中,计数器需要持久化到数据库或缓存中(如 Redis)以保证即使服务重启后,计数器也能正确恢复。
-
容错机制:可以允许服务端在计数器一定范围内(如 ±2)进行 OTP 验证,以应对客户端和服务端计数器不同步的问题。
-
并发优化:如果某个客户端有大量并发请求,可以进一步优化锁机制,使用更细粒度的锁或分布式锁来提升性能。
小结
- 服务端需要为每个客户端单独维护计数器,计数器与客户端的身份信息关联。
- 使用线程安全的数据结构和锁机制来确保并发情况下计数器的正确性。
- 持久化计数器以避免重启服务后丢失计数器状态,并使用容错窗口来应对客户端与服务端的计数器不同步问题。
计数器容错机制
可以修改 verifyOTP
方法,允许服务端在一定范围内(例如 ±2)验证 OTP,以应对计数器不完全同步的问题:
public boolean verifyOTP(String clientId, String inputOTP) throws Exception {lock.lock();try {// 获取客户端的计数器和密钥Long counter = clientCounters.get(clientId);String secret = clientSecrets.get(clientId);if (counter == null || secret == null) {throw new Exception("客户端未注册");}// 尝试在一定范围内验证 OTP(容错窗口为 ±2)int window = 2; // 容错范围for (int i = -window; i <= window; i++) {// 使用当前计数器加上偏移量生成 OTPString serverOTP = HOTP.generateHOTP(secret, counter + i, 6);// 验证客户端输入的 OTPif (serverOTP.equals(inputOTP)) {// 验证成功后更新计数器clientCounters.put(clientId, counter + i + 1); // 更新到同步的计数器值return true;}}// 如果在窗口范围内都验证失败,则返回falsereturn false;} finally {lock.unlock();}
}
验证失败常见原因
1. 计数器不同步
- 在 HOTP 中,计数器在每次生成 OTP 后都会递增。为了保证 OTP 的有效性,服务端和客户端的计数器必须同步。
- 如果在 OTP 生成和验证的过程中,服务端的计数器和客户端不一致,OTP 验证会失败。
2. 密钥不匹配
- HOTP 基于 HMAC 算法,需要服务端和客户端共享同一个密钥。如果在代码中,客户端和服务端使用了不同的密钥,会导致生成的 OTP 不同,从而验证失败。
3. 编码问题
- HOTP 的密钥需要经过 Base32 或 Base64 编码,有时密钥的编码格式可能不一致,导致 OTP 计算出现偏差。
4. 生成 OTP 时计数器没有递增
- 如果客户端生成 OTP 后没有递增计数器,服务端在验证时会发现生成的 OTP 和当前计数器不匹配,从而验证失败。
5. 输入 OTP 错误
- 如果用户输入的 OTP 有误(例如少了前导零,或者格式不对),即使客户端和服务端的密钥和计数器同步,也会导致验证失败。
解决方案
-
检查计数器同步:确保生成 OTP 时,客户端和服务端使用相同的计数器值。客户端的计数器每次生成 OTP 后应递增,而服务端在验证时也应同步递增。
-
检查密钥一致性:确保客户端和服务端使用相同的共享密钥,并且该密钥的编码一致(通常是 Base32)。
-
增加容错窗口:可以允许服务端在一定范围内(如
counter ± 1
或counter ± 2
)接受 OTP,以应对计数器稍微不同步的情况。可以修改验证部分的逻辑,允许在一定范围内检查 OTP。
相关文章:

加密与安全_HOTP一次性密码生成算法
文章目录 HOTP 的基础原理HOTP 的工作流程HOTP 的应用场景HOTP 的安全性安全性增强措施Code生成HOTP可配置项校验HOTP可拓展功能计数器(counter)计数器在客户端和服务端的作用计数器的同步机制客户端和服务端中的计数器表现服务端如何处理计数器不同步计…...

ResNet18果蔬图像识别分类
关于深度实战社区 我们是一个深度学习领域的独立工作室。团队成员有:中科大硕士、纽约大学硕士、浙江大学硕士、华东理工博士等,曾在腾讯、百度、德勤等担任算法工程师/产品经理。全网20多万粉丝,拥有2篇国家级人工智能发明专利。 社区特色…...
深度强化学习中收敛图的横坐标是steps还是episode?
在深度强化学习(Deep Reinforcement Learning, DRL)的收敛图中,横坐标选择 steps 或者 episodes 主要取决于算法的设计和实验的需求,两者的差异和使用场景如下: Steps(步数): 定义&a…...

一个真实可用的登录界面!
需要工具: MySQL数据库、vscode上的php插件PHP Server等 项目结构: login | --backend | --database.sql |--login.php |--welcome.php |--index.html |--script.js |--style.css 项目开展 index.html: 首先需要一个静态网页&#x…...
Vue中watch监听属性的一些应用总结
【1】vue2中watch的应用 ① 简单监视 在 Vue 2 中,如果你不需要深度监视,即只需监听顶层属性的变化,可以使用简写形式来定义 watch。这种方式更加简洁,适用于大多数基本场景。 示例代码 假设你有一个 Vue 组件,其中…...

MongoDB-aggregate流式计算:带条件的关联查询使用案例分析
在数据库的查询中,是一定会遇到表关联查询的。当两张大表关联时,时常会遇到性能和资源问题。这篇文章就是用一个例子来分享MongoDB带条件的关联查询发挥的作用。 假设工作环境中有两张MongoDB集合:SC_DATA(学生基本信息集合&…...

Redis数据库与GO(一):安装,string,hash
安装包地址:https://github.com/tporadowski/redis/releases 建议下载zip版本,解压即可使用。解压后,依次打开目录下的redis-server.exe和redis-cli.exe,redis-cli.exe用于输入指令。 一、基本结构 如图,redis对外有个…...
expressjs,实现上传图片,返回图片链接
在 Express.js 中实现图片上传并返回图片链接,你通常需要使用一个中间件来处理文件上传,比如 multer。multer 是一个 node.js 的中间件,用于处理 multipart/form-data 类型的表单数据,主要用于上传文件。 以下是一个简单的示例&a…...

爬虫——XPath基本用法
第一章XML 一、xml简介 1.什么是XML? 1,XML指可扩展标记语言 2,XML是一种标记语言,类似于HTML 3,XML的设计宗旨是传输数据,而非显示数据 4,XML标签需要我们自己自定义 5,XML被…...
常见排序算法汇总
排序算法汇总 这篇文章说明下排序算法,直接开始。 1.冒泡排序 最简单直观的排序算法了,新手入门的第一个排序算法,也非常直观,最大的数字像泡泡一样一个个的“冒”到数组的最后面。 算法思想:反复遍历要排序的序列…...

Golang | Leetcode Golang题解之第459题重复的子字符串
题目: 题解: func repeatedSubstringPattern(s string) bool {return kmp(s s, s) }func kmp(query, pattern string) bool {n, m : len(query), len(pattern)fail : make([]int, m)for i : 0; i < m; i {fail[i] -1}for i : 1; i < m; i {j : …...
0.计网和操作系统
0.计网和操作系统 熟悉计算机网络和操作系统知识,包括 TCP/IP、UDP、HTTP、DNS 协议等。 常见的页面置换算法: 先进先出(FIFO)算法:将最早进入内存的页面替换出去。最近最少使用(LRU)算法&am…...

探索Prompt Engineering:开启大型语言模型潜力的钥匙
前言 什么是Prompt?Prompt Engineering? Prompt可以理解为向语言模型提出的问题或者指令,它是激发模型产生特定类型响应的“触发器”。 Prompt Engineering,即提示工程,是近年来随着大型语言模型(LLM,Larg…...
滚雪球学Oracle[3.3讲]:数据定义语言(DDL)
全文目录: 前言一、约束的高级使用1.1 主键(Primary Key)案例演示:定义主键 1.2 唯一性约束(Unique)案例演示:定义唯一性约束 1.3 外键(Foreign Key)案例演示:…...

ssrf学习(ctfhub靶场)
ssrf练习 目录 ssrf类型 漏洞形成原理(来自网络) 靶场题目 第一题(url探测网站下文件) 第二关(使用伪协议) 关于http和file协议的理解 file协议 http协议 第三关(端口扫描)…...
ElasticSearch之网络配置
对官方文档Networking的阅读笔记。 ES集群中的节点,支持处理两类通信平面 集群内节点之间的通信,官方文档称之为transport layer。集群外的通信,处理客户端下发的请求,比如数据的CRUD,检索等,官方文档称之…...
【C语言进阶】系统测试与调试
1. 引言 在开始本教程的深度学习之前,我们需要了解整个教程的目标及其结构,以及为何进阶学习是提升C语言技能的关键。 目标和结构: 教程目标:本教程旨在通过系统化的学习,从单元测试、系统集成测试到调试技巧…...
多个单链表的合成
建立两个非递减有序单链表,然后合并成一个非递增有序的单链表。 注意:建立非递减有序的单链表,需要采用创建单链表的算法 输入格式: 1 9 5 7 3 0 2 8 4 6 0 输出格式: 9 8 7 6 5 4 3 2 1 输入样例: 在这里给出一组输入。例如…...

『建议收藏』ChatGPT Canvas功能进阶使用指南!
大家好,我是木易,一个持续关注AI领域的互联网技术产品经理,国内Top2本科,美国Top10 CS研究生,MBA。我坚信AI是普通人变强的“外挂”,专注于分享AI全维度知识,包括但不限于AI科普,AI工…...

Ollama 运行视觉语言模型LLaVA
Ollama的LLaVA(大型语言和视觉助手)模型集已更新至 1.6 版,支持: 更高的图像分辨率:支持高达 4 倍的像素,使模型能够掌握更多细节。改进的文本识别和推理能力:在附加文档、图表和图表数据集上进…...

网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...

网络六边形受到攻击
大家读完觉得有帮助记得关注和点赞!!! 抽象 现代智能交通系统 (ITS) 的一个关键要求是能够以安全、可靠和匿名的方式从互联车辆和移动设备收集地理参考数据。Nexagon 协议建立在 IETF 定位器/ID 分离协议 (…...
谷歌浏览器插件
项目中有时候会用到插件 sync-cookie-extension1.0.0:开发环境同步测试 cookie 至 localhost,便于本地请求服务携带 cookie 参考地址:https://juejin.cn/post/7139354571712757767 里面有源码下载下来,加在到扩展即可使用FeHelp…...

智慧医疗能源事业线深度画像分析(上)
引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...
QMC5883L的驱动
简介 本篇文章的代码已经上传到了github上面,开源代码 作为一个电子罗盘模块,我们可以通过I2C从中获取偏航角yaw,相对于六轴陀螺仪的yaw,qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...

STM32标准库-DMA直接存储器存取
文章目录 一、DMA1.1简介1.2存储器映像1.3DMA框图1.4DMA基本结构1.5DMA请求1.6数据宽度与对齐1.7数据转运DMA1.8ADC扫描模式DMA 二、数据转运DMA2.1接线图2.2代码2.3相关API 一、DMA 1.1简介 DMA(Direct Memory Access)直接存储器存取 DMA可以提供外设…...

转转集团旗下首家二手多品类循环仓店“超级转转”开业
6月9日,国内领先的循环经济企业转转集团旗下首家二手多品类循环仓店“超级转转”正式开业。 转转集团创始人兼CEO黄炜、转转循环时尚发起人朱珠、转转集团COO兼红布林CEO胡伟琨、王府井集团副总裁祝捷等出席了开业剪彩仪式。 据「TMT星球」了解,“超级…...
Frozen-Flask :将 Flask 应用“冻结”为静态文件
Frozen-Flask 是一个用于将 Flask 应用“冻结”为静态文件的 Python 扩展。它的核心用途是:将一个 Flask Web 应用生成成纯静态 HTML 文件,从而可以部署到静态网站托管服务上,如 GitHub Pages、Netlify 或任何支持静态文件的网站服务器。 &am…...

selenium学习实战【Python爬虫】
selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...
Swagger和OpenApi的前世今生
Swagger与OpenAPI的关系演进是API标准化进程中的重要篇章,二者共同塑造了现代RESTful API的开发范式。 本期就扒一扒其技术演进的关键节点与核心逻辑: 🔄 一、起源与初创期:Swagger的诞生(2010-2014) 核心…...