You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Keycloak如何解码totpSecret生成Google Authenticator式OTP验证码

Keycloak TOTP密钥生成6位OTP验证码实现方案

你拿到的totpSecret不是加密后的密文,是Base32编码的TOTP根密钥,不需要做特殊解码,直接按照TOTP标准(RFC6238,和Google Authenticator逻辑完全一致)计算就能得到对应6位验证码,Keycloak默认的TOTP参数和Google Authenticator完全兼容,不需要额外做适配。

核心参数(Keycloak默认配置)

  • 验证码长度:6位
  • 时间步长:30秒
  • 哈希算法:HMAC-SHA1(如果你在Realm设置里改了TOTP哈希算法、步长、码长,对应调整参数即可)
  • 密钥编码:Base32

实现逻辑

  • 先把拿到的字符串格式totpSecret做Base32解码,得到原始字节数组格式的根密钥
  • 取当前UTC时间的Unix时间戳,除以30取整得到时间计数器,转成8字节大端序字节数组
  • 用根密钥对时间计数器做HMAC-SHA1运算,得到20字节的哈希结果
  • 取哈希结果最后一个字节的低4位作为偏移量,从偏移位置截取4字节,转成整数后屏蔽最高位避免符号问题
  • 把得到的整数对1000000取模,不足6位的前面补0,得到原始6位验证码
  • 按前3位后3位加空格拆分,就是和Google Authenticator显示格式完全一致的验证码

适配Keycloak自定义Provider的Java代码示例

因为你是做Keycloak自定义资源扩展,直接用下面的代码即可,不需要额外引入第三方依赖,低版本Keycloak可以直接用服务自带的Base32工具类,避免依赖冲突:

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.time.Instant;
// JDK17+用自带java.util.Base32,低版本直接替换为Keycloak内部工具类:
// import org.keycloak.common.util.Base32;

public class TotpUtil {
    private static final String HMAC_ALGORITHM = "HmacSHA1";
    private static final int TIME_STEP = 30;
    private static final int CODE_LENGTH = 6;

    public static String getFormattedOtp(String base32Secret) throws Exception {
        // Base32解码密钥
        byte[] secretBytes = Base32.getDecoder().decode(base32Secret.trim());
        // 计算当前时间窗口计数器
        long counter = Instant.now().getEpochSecond() / TIME_STEP;
        byte[] counterBytes = ByteBuffer.allocate(8)
                .order(ByteOrder.BIG_ENDIAN)
                .putLong(counter)
                .array();
        // 计算HMAC哈希
        Mac mac = Mac.getInstance(HMAC_ALGORITHM);
        mac.init(new SecretKeySpec(secretBytes, HMAC_ALGORITHM));
        byte[] hashResult = mac.doFinal(counterBytes);
        // 动态截断取码
        int offset = hashResult[hashResult.length - 1] & 0x0F;
        int codeNum = ByteBuffer.wrap(hashResult, offset, 4)
                .order(ByteOrder.BIG_ENDIAN)
                .getInt() & 0x7FFFFFFF;
        // 补前导零生成6位码
        String rawCode = String.format("%0" + CODE_LENGTH + "d", codeNum % (int) Math.pow(10, CODE_LENGTH));
        // 格式化为xxx xxx的展示样式
        return rawCode.substring(0, 3) + " " + rawCode.substring(3);
    }
}

避坑提示

  • 必须保证Keycloak服务器的系统时间准确,和国际标准时间偏移超过30秒就会生成无效验证码
  • 如果你修改过Realm层面的TOTP配置(比如用HmacSHA256、步长60秒、8位码),必须对应修改代码里的常量值,否则生成的码和认证器App输出不一致
  • 生成的验证码不要持久化存储,生成后直接推送给用户即可,超过当前30秒窗口验证码就会自动失效
  • 不要自己实现Base32编解码逻辑,直接用JDK自带或者Keycloak内置的工具类,避免编解码错误导致生成的验证码不匹配

内容的提问来源于stack exchange,提问作者Shresh Maharjan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 13:27:17