实现一个 type = text 模拟的密码框

lxf2023-05-05 05:52:01

1. 背景

  • 虽然浏览器提供了一种帮助用户记住密码,用户再次登录时无需手动输入密码的便捷功能。但有的用户不希望让浏览器记住自己输入的任何密码。
  • input在type=text模式下,浏览器不会记住用户输入的密码。

实现一个 type = text 模拟的密码框

2. 方案

2.1 使用autocomplete关闭表单自动填充

如何关闭表单自动填充 - Web 安全 | MDN

2.2 使用隐藏的密码框将密码框和文本框隔离开

阻止浏览器记住密码 - CodeAntenna

2.3 手动关闭浏览器保存密码的功能

实现一个 type = text 模拟的密码框

2.4 使用type=text实现密码框

  • 2.2的方法对于我这里是不起作用的(暂时未找出原因);
  • 2.1关闭表单的自动填充,但浏览器仍然记住了你的密码,只是不会告诉你它记住了;
  • 2.3 通过浏览器设置,手动关闭,不让浏览器记住密码;

综合以上三个方法的不足,我们需要探索新的方法去避免浏览器记住用户密码。浏览器只有在检测到 input 的 type 为password的时候,会认为其是密码框,提供密码记忆功能。所以,如果我们的密码框type不是password呢?浏览器不会记录我们的密码,因此这里考虑使用type=text的方式来模拟type=password的效果。

2.4.1 需要处理的关键问题:

  1. 将输入值替换为特殊字符“•”;
  2. 将输入值用“•”替换之后,怎么拿到真实的输入字符串

问题:

实现一个 type = text 模拟的密码框

用“•”替换真实字符之后,下一次输入时,onChange事件中拿到的字符串只有最后一位时当前真实输入的字符;

  1. 如果用户输入了一个特殊字符“•”怎么办?

2.4.2 相关事件以及概念

  • 用户输入字符的开始位置和结束位置:

实现一个 type = text 模拟的密码框实现一个 type = text 模拟的密码框

使用键盘输入时的selStart、selEnd位置

鼠标选中时的selStart、selEnd位置

实现一个 type = text 模拟的密码框

键盘输入字符"s"之后,当前光标所在位置

  • 在onSelect事件中,可以拿到输入字符前光标的起始位置、结束位置;
  • 当用户切换为中文输入法时,需要判断输入是否结束

实现一个 type = text 模拟的密码框

2.4.3 核心思路

  • 根据字符串长度变化来判断是输入了字符,还是删除了字符,不需要关注输入的具体字符是什么。

用上一次的字符串长度oldInputval.current.length 和当前的新字符串长度做差值,得到字符串长度变化lengthChange

根据lengthChange判断是插入了字符还是删除了字符,做对应的处理。如果是插入字符,oldInputval 需要从光标结束位置selEnd 开始后的所有值后移lengthChange 位,如果是删除字符,oldInputval 需要从curCursor 位置开始删除lengthChange位;

实现一个 type = text 模拟的密码框

2.4.4 核心代码

  • 需要的变量以及state

state:

  • displayValue :用特殊字符“•”替换真实密码之后的字符串;

变量:

  • oldInputval :用来保存真实的字符串
  • selStart: 保存输入光标开始位置
  • selEnd :保存输入光标结束位置
  • curTarget :保存input
  • cursor: 保存当前光标
  • compositionLockRef :判断输入是否完成
const Password = ()=>{
    ...
    定义state和变量
    ...
 useEffect(() => {
      if (props.value) {
        oldInputval.current = props.value;
        if (type === 'text' && !visibility) {
          setDisplayValue('•'.repeat(oldInputval.current.length));
        } else {
          setDisplayValue(props.value);
        }
        if (curTarget) {
          cursor.current = oldInputval.current.length;
        }
      }
    }, [props.value]);
  useEffect(() => {
      if (type === 'text' && !visibility) {
        curTarget.current && curTarget.current.setSelectionRange(cursor.current, cursor.current);
      }
    });

    useEffect(() => {
      if (type === 'text') {
        if (visibility) {
          setDisplayValue(oldInputval.current);
        } else {
          setDisplayValue('•'.repeat(oldInputval.current.length));
        }
      }
    }, [visibility]);
    function handleChange(curVal: string, e: React.ChangeEvent<HTMLInputElement>) {
          if (type === 'text') {
               const curCursor = e.target.selectionEnd;
                curTarget.current = e.target;
                let newVal = '';
                const oldStrLength = oldInputval.current.length;
                const lengthChange = curVal.length - oldStrLength;
                let oldStr: string[];
                if (lengthChange >= 0) {
                   处理增加字符的情况
                } else {
                   处理删除字符的情况
                }
                newVal = handleReplace(oldStr, curVal.split(''), selStart.current, curCursor - 1);
                oldInputval.current = newVal;//更新当前的真实字符串
                if (visibility) {
                    setDisplayValue(curVal);
                } else {
                    setDisplayValue('•'.repeat(newVal.length));
                }
                cursor.current = curCursor;//保存下当前光标位置
                props.onChange && props.onChange(newVal, e);//将真实值传递出去
            } else {
                setDisplayValue(curVal);
                props.onChange && props.onChange(curVal, e);
            }
         //每次触发onChange事件之后都需要更新一下光标开始、结束位置
           selStart.current = e.target.selectionStart;
           selEnd.current = e.target.selectionEnd;
}
        return (
            <Input
                ...
                type={visibility || type === 'text' ? 'text' : 'password'}
                value={displayValue}
                onChange={(v: string, e: React.ChangeEvent<HTMLInputElement>) => {
                    handleChange(v, e);
                }}
                onCompositionStartCapture={(e: React.SyntheticEvent<HTMLInputElement, Event>) => {
                    //中文输入,
                    compositionLockRef.current = true;
                    selStart.current = e.target.selectionStart;
                    selEnd.current = e.target.selectionEnd;
                }}
                onCompositionEndCapture={(e: React.SyntheticEvent<HTMLInputElement, Event>) => {
                //输入结束
                    compositionLockRef.current = false;
                }}
                onSelect={(e: React.SyntheticEvent<HTMLInputElement, Event>) => {
                //只有compositionLockRef.current为false时才在onSelect中更新光标起始位置、结束位置
                    if (!compositionLockRef.current) {
                        selStart.current = e.target.selectionStart;
                        selEnd.current = e.target.selectionEnd;
                    }
                }}
            />
        );
}

使用onCompositionEndCapture、onCompositionStartCapture是为了处理输入法为中文模式下的selStart、selEnd更新问题;

3. 效果

实现一个 type = text 模拟的密码框

当type=text时,输入密码,浏览器右侧上方没有小钥匙,避开了浏览器记住密码。