PasswordValidation 密码校验组件实现与详解

一、组件介绍

PasswordValidation 是一个基于 React 和 Ant Design 的通用密码强度校验与规则提示组件。它能够实时校验用户输入的密码是否符合企业安全要求,并以弹窗形式直观展示校验规则及当前状态。组件支持与 antd Form 无缝联动,便于在注册、修改密码等场景中复用。

二、使用场景

  • 用户注册、修改密码、重置密码等需要密码输入的表单
  • 需要统一密码强度策略的企业级后台系统
  • 需要实时提示密码合规性的前端页面
  • 需要与 Ant Design Form 组件集成的项目

三、核心思路

  • 规则配置:将密码校验规则(长度、字符类型、多样性)抽象为常量配置,便于维护和扩展。
  • 实时校验:每次输入变更时,自动校验所有规则,并将结果同步到表单校验状态。
  • 规则提示:通过 Popover 弹窗,实时展示每条规则的通过情况,提升用户体验。
  • 表单联动:支持与 antd Form 组件联动,自动设置字段校验状态和错误提示。
  • ref 暴露:通过 ref 暴露校验方法,便于外部自定义校验逻辑。

四、关键代码实现

1. 规则配置与校验
js 复制代码
const PASSWORD_RULES = {
  length: {
    min: 8,
    max: 20,
    test: (password: string) => password.length >= 8 && password.length <= 20
  },

  chars: {
    pattern: /^[a-zA-Z0-9!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~]+$/,
    test: (password: string) => PASSWORD_RULES.chars.pattern.test(password) && !/\\s/.test(password)
  },

  variety: {
    minTypes: 3,
    test: (password: string) => {
      let types = 0;
      if (/[A-Z]/.test(password)) types++;
      if (/[a-z]/.test(password)) types++;
      if (/[0-9]/.test(password)) types++;
      if (/[!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~]/.test(password)) types++;
      return types >= PASSWORD_RULES.variety.minTypes;
    }
  }
};
2. 核心实现
js 复制代码
export const PasswordValidation = forwardRef<WithPasswordRulesRef, WithPasswordRulesProps>(({
  children, 
  form, 
  value = '', 
  name, 
  trigger = 'hover', 
  placement = 'bottomLeft', 
  title, 
  ...restProps
}, ref) => {
  const validationsRef = useRef({ length: false, chars: false, variety: false });
  
  const validatePassword = useCallback((password: string) => ({
    length: PASSWORD_RULES.length.test(password),
    chars: PASSWORD_RULES.chars.test(password),
    variety: PASSWORD_RULES.variety.test(password)
  }), []);

  const updateFieldStatus = useCallback((isValid: boolean) => {
    if (!name || !form) return;
    if (!value) {
      form.setFields([{ name, errors: ['密码不能为空'], validating: false, touched: true }]);
    } else if (!isValid) {
      form.setFields([{ name, errors: ['请按正确格式输入密 码'], validating: false, touched: true }]);
    } else {
      form.setFields([{ name, errors: undefined, validating: false }]);
    }
  }, [name, form, value]);

  const passwordRulesContent = useMemo(() => {
    const validations = validatePassword(value);
    const rules = [
      { valid: validations.length, text: '8-20个字符', key: 'length' },
      { valid: validations.chars, text: '只能包含大小写字母、数字以及标点符号(除空格)', key: 'chars' },
      { valid: validations.variety, text: '大写字母、小写字母、数字和标点符号至少包含3种', key: 'variety' }
    ];

    return (
      <div className={styles.container}>
        {rules.map(rule => (
          <div className={styles.item} key={rule.key}>
            <CheckCircleOutlined className={classnames(styles.icon, { [styles.success]: rule.valid })} />
            <Text className={classnames(styles.text, { [styles.success]: rule.valid })}>{rule.text}</Text>
          </div>
        ))}
      </div>
    );
  }, [value, validatePassword]);

  const handleValidator = useCallback(async () => {
    const isValid = Object.values(validationsRef.current).every(valid => valid);
    if (!isValid) {
      return Promise.reject('请按正确格式输入密码');
    }
    return Promise.resolve();
  }, []);
  
  useEffect(() => {
    validationsRef.current = validatePassword(value);
    const isValid = Object.values(validationsRef.current).every(valid => valid);
    updateFieldStatus(isValid);
  }, [value, validatePassword, updateFieldStatus]);

  // 处理子组件焦点
  const handleChildFocus = useCallback((originalOnFocus?: (e: React.FocusEvent<HTMLInputElement>) => void) => {
    return (e: React.FocusEvent<HTMLInputElement>) => {
      originalOnFocus?.(e);
      if (form) {
        const isValid = Object.values(validationsRef.current).every(valid => valid);
        updateFieldStatus(isValid);
      }
    };
  }, [form, updateFieldStatus]);

  // 克隆子组件
  const childrenWithProps = useMemo(() => {
    return React.cloneElement(children, {
      onFocus: handleChildFocus(children.props.onFocus)
    });
  }, [children, handleChildFocus]);

  useImperativeHandle(ref, () => ({
    getValidations: () => validationsRef.current,
    handleValidator
  }), [handleValidator]);

  return (
    <Popover
      content={passwordRulesContent}
      trigger={trigger}
      placement={placement}
      title={title || '密码需满足以下要求'}
      overlayClassName={styles.password_popover}
      getPopupContainer={triggerNode => triggerNode.parentNode as HTMLElement}
      {...restProps}
    >
      {childrenWithProps}
    </Popover>
  );
});
3. 基础使用
js 复制代码
<Form.Item
  label="设置密码"
  name="password"
  rules={[
    { required: true, message: '请输入密码' },
    {
      validator: async (_, value) => {
        if (passwordRef.current) {
          await passwordRef.current.handleValidator();
        }
      }
    }
  ]}
>
  <PasswordValidation ref={passwordRef} form={form}>
    <Input.Password placeholder="请输入密码" />
  </PasswordValidation>
</Form.Item>

五、注意事项

  • 与 antd Form 联动时,必须传入 form 和 name 属性,否则无法自动设置表单校验状态。
  • 建议外部受控 value,组件内部不维护 value 状态。
  • Popover 默认 hover 触发,可通过 trigger 属性自定义。
  • 如需多语言或自定义文案,可自行扩展。
  • 如需自定义样式,可通过 overlayClassName 传递 className。
  • 如需自定义校验规则,可修改 PASSWORD_RULES 配置。

六、总结

PasswordValidation 组件极大简化了密码强度校验和规则提示的开发工作,支持灵活扩展和与 antd Form 的无缝集成。它不仅提升了用户体验,也保证了密码安全策略的统一落地。推荐在所有涉及密码输入的场景中使用。如需多语言、弱密码黑名单等高级功能,可在此基础上二次开发。

相关推荐
speedoooo15 分钟前
在现有App里嵌入一个AI协作者
前端·ui·小程序·前端框架·web app
全栈胖叔叔-瓜州27 分钟前
关于llamasharp 大模型多轮对话,模型对话无法终止,或者输出角色标识User:,或者System等角色标识问题。
前端·人工智能
三七吃山漆34 分钟前
攻防世界——wife_wife
前端·javascript·web安全·网络安全·ctf
用户479492835691538 分钟前
面试官问"try-catch影响性能吗",我用数据打脸
前端·javascript·面试
GISer_Jing1 小时前
前端营销技术实战:数据+AI实战指南
前端·javascript·人工智能
GIS之路1 小时前
使用命令行工具 ogr2ogr 将 CSV 转换为 Shp 数据(二)
前端
嘉琪0012 小时前
Vue3+JS 高级前端面试题
开发语言·前端·javascript
vipbic2 小时前
用 Turborepo 打造 Strapi 插件开发的极速全栈体验
前端·javascript
天涯学馆2 小时前
为什么 JavaScript 可以单线程却能处理异步?
前端·javascript
Henry_Lau6173 小时前
主流IDE常用快捷键对照
前端·css·ide