一次性密码输入(OTP)组件状态管理 React Hook 开发实践 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
theprimone
V2EX    React

一次性密码输入(OTP)组件状态管理 React Hook 开发实践

  •  
  •   theprimone
    yunsii 2023-12-24 15:15:37 +08:00 2688 次点击
    这是一个创建于 722 天前的主题,其中的信息可能已经有所发展或是发生改变。

    动机

    本来想着 React 生态这么好应该能找到一个很好用的 OTP (One-time password) 组件的,结果一通折腾,没发现一个好用的,很多库甚至提供的在线示例就有些奇奇怪怪的小缺陷……以下是我找到并简单测试过的库(甚至还看了个 Vue 的 ):

    至于没有提供在线示例的,根本没有想测试的欲望。事已至此,那就自己上吧,本来打算也是直接写个类似的 React 组件出来的,结果写着写着发现还是封装成 React Hook 更灵活,接下来就介绍下专门为此开发的库 use-otp-input

    use-otp-input 在线示例

    设计

    因为找到的库都是会出现选择字符的情况,这在我看来是不理解的,这不符合一般用户直觉,因此需要手动控制光标的位置,说来也巧,之前刚好封装过一个 selection-extra 的库,用户操作和管理 Selection。现在是直接安装它来使用的,不过追求极致倒是可以只复制出核心逻辑即可,为了将来可能的扩展性还是直接装库了。

    刚开始全凭个人感觉在移动光标,搞完测试效果怎么都不太合适,最后发现直接参考普通的输入框组件的行为才是最符合直觉的,因此做了比较大的重构。

    支持的功能有:

    • 更符合普通输入组件直觉的输入逻辑,包括粘贴文本字符串
    • 支持BackspaceDelete按键,包括 Ctrl 快捷键
    • 支持左右方向键移动光标位置

    值得注意的是因为行为类似单个输入框,因此用户无法将光标移动到空白的输入框中,除非不存在前一个单元格或者前一个单元格存在有效输入。

    开发

    思路有了其实开发是相对简单的,只是会有很多边界情况需要处理,如果查看源码会看到多处因为光标出现的位置不符合预期而做的特殊处理。如果有需要可以自行参考源码查看更多细节。

    另外,因为这个库只做了 OTP 组件的状态管理,因此 UI 需要自行实现,本来 Demo 的 UI 都很原始,想着这个库应该多少有点用,还得写个文章引下流呢,所以又花了不少时间把 Demo 写得漂亮些,才有了这样的效果:

    use-otp-input screenshot

    总的来看,使用效果应该是超越了已发现的同类库的,甚至可以说是效果拔群

    总结

    越是基础的功能越是需要注重各种细节,因为这个功能需要自行控制光标位置,基本每一个小细节都需要考虑到,不然都可能出现奇奇怪怪的缺陷。所幸经过不断尝试与妥协,最后还是得到了一个自己比较满意的效果。欢迎感兴趣的朋友们尝试与反馈

    第 1 条附言    2023-12-24 22:08:43 +08:00
    用户操作和管理 Selection => 用于操作和管理 Selection
    18 条回复    2023-12-25 13:58:56 +08:00
    huntzhan
        1
    huntzhan  
       2023-12-24 17:06:22 +08:00
    赞,可以简单介绍一下 OTP 原理吗?
    theprimone
        2
    theprimone  
    OP
       2023-12-24 19:23:02 +08:00
    @huntzhan #1 我这个只是前端输入框的实现,你该不会说的是后端吧?
    nashaofu
        3
    nashaofu  
       2023-12-24 20:19:16 +08:00   3
    @huntzhan 简单原理:
    1. 服务器生成一个 secret ,
    2. OTP 客户端根据 secret ,与一个计数器生成 HMAC-SHA1 摘要的 hash ,如果是 TOTP ,计数器就是时间戳 / 30 ,所以我们通常会看到 TOTP 的 code 每 30 秒变化一次。hash 只有拥有 secret 的人才能生成出来,所以也就保证了安全性。
    3. 由于 hash 太长,不利于输入,所以通常会把 hash 转换为 6 位的数字,方便用户输入。转换方法为:取摘要结果最后一个字节的低 4 位,作为偏移值,然后以该偏移值为下标,从摘要中取从下标为该偏移值开始的 4 个字节,把这几个字节的内容转换为数字。然后把数字转换为 6 位字符串,不足 6 位,前面补 0 。
    ```
    let hash = algorithm.digest(&secret, &counter.to_be_bytes())?;
    let offset: usize = (hash[hash.len() - 1] & 0xf) as usize;

    let binary = ((hash[offset] as u64) & 0x7f) << 24
    | ((hash[offset + 1] as u64) & 0xff) << 16
    | ((hash[offset + 2] as u64) & 0xff) << 8
    | ((hash[offset + 3] as u64) & 0xff);

    let mut token = (binary % 10_u64.pow(digits)).to_string();

    while token.len() < (digits as usize) {
    token = format!("0{}", token);
    }
    ```

    完整代码可以参考这里: https://github.com/nashaofu/anyotp/blob/master/src/utils.rs
    相关 RFC 参考: https://datatracker.ietf.org/doc/html/rfc4226#section-5.1
    theprimone
        4
    theprimone  
    OP
       2023-12-24 22:14:52 +08:00
    @nashaofu #3 呐,这个就叫专业,不过我们的 OTP 是发送到邮箱的,没有 OTP 客户端,直接用的随机数字代替,哈哈哈 不过这么说起来,搞不好还真可以接个第三方 OTP 服务呢。
    huntzhan
        5
    huntzhan  
       2023-12-25 00:54:00 +08:00
    赞,可以简单介绍一下 OTP 原理吗?
    @nashaofu 大佬专业!
    body007
        6
    body007  
       2023-12-25 09:05:31 +08:00
    @huntzhan #5 你这个不科学,密码通过网络传输过,违背 OTP 的初衷。验证码 30 秒内有效,在客户端本地计算才安全。https://github.com/iamyuthan/2FA-Solver ,这里有个纯前端的项目,生成的代码也不复杂。
    theprimone
        7
    theprimone  
    OP
       2023-12-25 09:12:33 +08:00
    @body007 #6 这看起来是让用户自己一通操作得到 OTP ?
    body007
        8
    body007  
       2023-12-25 09:16:01 +08:00
    @theprimone #7 本来就是啊。OTP 运行的代码建议都在客户端,避免被中间人获取,不然怎么安全嘛,30 秒内有效就更严格的阻止安全问题了。
    body007
        9
    body007  
       2023-12-25 09:17:30 +08:00
    @theprimone #7 可以去 github 搜 2fa ,一大堆的命令行生成工具,要的就是本地生成验证码。
    theprimone
        10
    theprimone  
    OP
       2023-12-25 09:27:43 +08:00
    @body007 #8 这么说的话,特地试了一下微软认证的客户端,断网后确实也能生成验证码,但是 secret 应该是 App 自动获取的了。如果真的全程离线操作,这对普通用户并不友好。
    body007
        11
    body007  
       2023-12-25 09:39:31 +08:00
    @theprimone #10 secret 也得保密传输啊,泄露了就等于裸奔了,最好的方式就是定期更新。我自己用自建的 trilium 笔记,自己写 js 代码生成验证码,不用关心自动获取 secret , 我自己也够用了额。

    theprimone
        12
    theprimone  
    OP
       2023-12-25 10:08:32 +08:00
    @body007 #11 大佬是会玩的
    CodeCodeStudy
        13
    CodeCodeStudy  
       2023-12-25 12:14:57 +08:00
    @theprimone #10 大哥看来你不了解 TOTP 的原理啊,TOTP 本来就不依赖于网络的,是网站随机生成密码,然后发给用户(通常密码转成二维码,让用户扫二维码),用户通过密码和时间戳的一系列计算,得到 6 位数字。因为是对时间戳对 30 取余的,所以是每 30 秒变化一次。因为通过这 6 位数字,是反推不出原密码的,所以保证了密码的安全。
    theprimone
        14
    theprimone  
    OP
       2023-12-25 12:38:23 +08:00
    @CodeCodeStudy #13 哈哈哈,光想着折腾前端输入组件了,没怎么深入了解过具体实现 不过还是使用过的,目前只用过微软的 APP 。
    moonrailgun
        15
    moonrailgun  
       2023-12-25 13:24:12 +08:00
    TOTP 是纯客户端的。
    OTP 不是。这两不是一回事。

    你邮件发个临时密码这就叫 OTP
    theprimone
        16
    theprimone  
    OP
       2023-12-25 13:42:47 +08:00
    @moonrailgun #15 OTP 不应该更是一个笼统的大类吗
    moonrailgun
        17
    moonrailgun  
       2023-12-25 13:56:10 +08:00
    @theprimone 你要说笼统的一类那确实是。
    手机验证码也是 OTP 的一类。

    我的意思是评论区把 TOTP 和 OTP 混为一谈,是不对的。
    theprimone
        18
    theprimone  
    OP
       2023-12-25 13:58:56 +08:00
    @moonrailgun #17 那按我现在的理解有 HOTP TOTP 还有直接随机字符串这种很随意的 OTP
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     4114 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 30ms UTC 00:15 PVG 08:15 LAX 16:15 JFK 19:15
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86