介绍
在大多数 Web 应用程序中,密码通常用于用户身份验证。因此,以安全的方式存储密码非常重要。多年来,诸如单向密码散列等技术已被用于隐藏存储在数据库中的密码的真实表示。
虽然密码散列是保护密码的重要一步,但用户仍然对密码安全提出了重大挑战:使用常用词作为密码的用户会使散列的努力徒劳无功,因为暴力攻击可以快速破解此类密码.
为了解决这个问题,今天的许多 Web 应用程序都坚持要求用户使用强密码,方法是确保密码长度最短,或者密码中的字母数字字符和符号的某种组合。为了测量密码强度,Dropbox 开发了一种算法,用于受密码破解者启发的真实密码强度估计器。该算法封装在一个名为 .js 的 JavaScript 库中zxcvbn
。此外,该软件包还包含常用英文单词、名称和密码的字典。
在本教程中,我们将使用 React JavaScript 框架创建一个包含全名、电子邮件和密码字段的表单。我们将执行一些轻量级表单验证,并zxcvbn
在提供视觉反馈的同时使用该库来估计表单中密码的强度。
查看此CodeSandbox 演示,了解您将在本教程结束时创建的内容。
先决条件
在开始之前,请确保您的系统上安装了最新版本的Node。
要遵循本教程,您将需要以下内容:
- 您的机器上安装了最新版本的Node。有关如何安装它的更多信息,请从How To Install Node.js集合中选择您的发行版。
yarn
安装以运行所有NPM脚本并为项目安装依赖项。您可以按照此Yarn 安装指南yarn
在您的系统上进行安装。
步骤 1 — 设置应用程序
本教程将使用该create-react-app
包生成新的 React 应用程序。create-react-app
如果您尚未安装,请运行以下命令在您的系统上安装:
- npm install -g create-react-app
安装完成后,使用以下命令启动一个新的 React 应用程序:
- create-react-app react-password-strength
此命令将其命名为react-password-strength
,但您可以随意命名。
注意:如果您使用的是npm
5.2 或更高版本,它会附带一个额外的npx
二进制文件。使用npx
二进制文件,您无需create-react-app
在系统上全局安装。你可以开始一个新的应用程序做出反应用这个命令:。npx create-react-app react-password-strength
接下来,您将安装应用程序所需的依赖项。运行以下命令安装所需的依赖项:
- yarn add zxcvbn isemail prop-types node-sass bootstrap
此命令安装以下依赖项:
zxcvbn
– 上述密码强度估计库。isemail
– 电子邮件验证库。prop-types
– 运行时检查传递给组件的预期属性类型。node-sass
– 用于将 Sass 文件编译为 CSS。
您可能已经注意到,您将bootstrap
软件包安装为应用程序的依赖项以获得一些默认样式。要在应用程序中包含 Bootstrap,请编辑该src/index.js
文件并在每个其他import
语句之前添加以下行:
import 'bootstrap/dist/css/bootstrap.min.css';
最后,启动您的应用程序:
- yarn start
应用程序现已启动,可以开始开发。请注意,已使用实时重新加载功能为您打开了一个浏览器选项卡。这将在您开发时与应用程序中的更改保持同步。
此时,您的应用程序视图将类似于以下屏幕截图:
第 2 步 – 构建组件
此应用程序将使用全名、电子邮件和密码的表格。它还将对字段执行一些轻量级表单验证。在这一步中,您将创建以下 React 组件:
-
FormField
– 用其属性和更改事件处理程序包装表单输入字段。 -
EmailField
– 包装电子邮件FormField
并向其添加电子邮件验证逻辑。 -
PasswordField
– 包装密码FormField
并向其添加密码验证逻辑。还将密码强度计和其他一些视觉提示附加到该字段。 -
JoinForm
–包含表单域的虚构加入支持团队表单。
在应用程序目录中创建一个components
目录src
以容纳所有组件。
该FormField
组件
FormField.js
在src/components
目录中创建一个新文件并向其中添加以下代码片段:
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
class FormField extends Component {
// initialize state
state = { value: '', dirty: false, errors: [] }
hasChanged = e => {
e.preventDefault();
// destructure props - assign default dummy functions to validator and onStateChanged props
const { label, required = false, validator = f => f, onStateChanged = f => f } = this.props;
const value = e.target.value;
const isEmpty = value.length === 0;
const requiredMissing = this.state.dirty && required && isEmpty;
let errors = [];
if (requiredMissing) {
// if required and is empty, add required error to state
errors = [ ...errors, `${label} is required` ];
} else if ('function' === typeof validator) {
try {
validator(value);
} catch (e) {
// if validator throws error, add validation error to state
errors = [ ...errors, e.message ];
}
}
// update state and call the onStateChanged callback fn after the update
// dirty is only changed to true and remains true on and after the first state update
this.setState(({ dirty = false }) => ({ value, errors, dirty: !dirty || dirty }), () => onStateChanged(this.state));
}
render() {
const { value, dirty, errors } = this.state;
const { type, label, fieldId, placeholder, children } = this.props;
const hasErrors = errors.length > 0;
const controlClass = ['form-control', dirty ? hasErrors ? 'is-invalid' : 'is-valid' : '' ].join(' ').trim();
return (
<Fragment>
<div className="form-group px-3 pb-2">
<div className="d-flex flex-row justify-content-between align-items-center">
<label htmlFor={fieldId} className="control-label">{label}</label>
{/** Render the first error if there are any errors **/}
{ hasErrors && <div className="error form-hint font-weight-bold text-right m-0 mb-2">{ errors[0] }</div> }
</div>
{/** Render the children nodes passed to component **/}
{children}
<input type={type} className={controlClass} id={fieldId} placeholder={placeholder} value={value} onChange={this.hasChanged} />
</div>
</Fragment>
);
}
}
FormField.propTypes = {
type: PropTypes.oneOf(["text", "password"]).isRequired,
label: PropTypes.string.isRequired,
fieldId: PropTypes.string.isRequired,
placeholder: PropTypes.string.isRequired,
required: PropTypes.bool,
children: PropTypes.node,
validator: PropTypes.func,
onStateChanged: PropTypes.func
};
export default FormField;
我们正在这个组件中做一些事情。让我们稍微分解一下:
输入状态:首先,您state
对表单字段组件进行了初始化,以跟踪value
输入字段的当前dirty
状态、字段状态以及任何现有的验证errors
。字段在其值第一次更改并保持脏状态的那一刻就变得脏了。
处理输入更改:接下来,您添加了hasChanged(e)
事件处理程序,以便value
在每次更改输入时将状态更新为当前输入值。在处理程序中,您还可以解析dirty
字段的状态。您检查该字段是否是required
基于 props的字段,errors
如果值为空,则向状态数组添加验证错误。
但是,如果该字段不是必填字段或者是必填字段但不为空,那么您委托给可选validator
prop 中传递的验证函数,使用当前输入值调用它,并将抛出的验证错误添加到状态errors
数组(如果有任何错误)。
最后,您更新状态并传递更新后要调用的回调函数。回调函数调用可选onStateChanged
prop 中传递的函数,将更新的状态作为参数传递。这对于在组件外部传播状态更改将变得非常方便。
渲染和道具:在这里渲染输入字段及其标签。您还可以有条件地呈现状态errors
数组中的第一个错误(如果有任何错误)。请注意您如何使用 Bootstrap 的内置类动态设置输入字段的类以显示验证状态。您还可以渲染组件中包含的任何子节点。
从组件的 中可以看出,该组件propTypes
所需的道具是type
('text'
或'password'
) label
、placeholder
、 和fieldId
。其余组件是可选的。
该EmailField
组件
EmailField.js
在src/components
目录中创建一个新文件并向其中添加以下代码片段:
import React from 'react';
import PropTypes from 'prop-types';
import { validate } from 'isemail';
import FormField from './FormField';
const EmailField = props => {
// prevent passing type and validator props from this component to the rendered form field component
const { type, validator, ...restProps } = props;
// validateEmail function using the validate() method of the isemail package
const validateEmail = value => {
if (!validate(value)) throw new Error('Email is invalid');
};
// pass the validateEmail to the validator prop
return <FormField type="text" validator={validateEmail} {...restProps} />
};
EmailField.propTypes = {
label: PropTypes.string.isRequired,
fieldId: PropTypes.string.isRequired,
placeholder: PropTypes.string.isRequired,
required: PropTypes.bool,
children: PropTypes.node,
onStateChanged: PropTypes.func
};
export default EmailField;
在EmailField
组件中,您正在渲染一个FormField
组件并将电子邮件验证功能传递给validator
prop。您正在使用包的validate()
方法isemail
进行电子邮件验证。
您可能还会注意到,除了type
和props 之外的所有其他 propsvalidator
都从EmailField
组件传输到FormField
组件。
该PasswordField
组件
PasswordField.js
在src/components
目录中创建一个新文件并向其中添加以下代码片段:
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import zxcvbn from 'zxcvbn';
import FormField from './FormField';
class PasswordField extends Component {
constructor(props) {
super(props);
const { minStrength = 3, thresholdLength = 7 } = props;
// set default minStrength to 3 if not a number or not specified
// minStrength must be a a number between 0 - 4
this.minStrength = typeof minStrength === 'number'
? Math.max( Math.min(minStrength, 4), 0 )
: 3;
// set default thresholdLength to 7 if not a number or not specified
// thresholdLength must be a minimum value of 7
this.thresholdLength = typeof thresholdLength === 'number'
? Math.max(thresholdLength, 7)
: 7;
// initialize internal component state
this.state = { password: '', strength: 0 };
};
stateChanged = state => {
// update the internal state using the updated state from the form field
this.setState({
password: state.value,
strength: zxcvbn(state.value).score
}, () => this.props.onStateChanged(state));
};
validatePasswordStrong = value => {
// ensure password is long enough
if (value.length <= this.thresholdLength) throw new Error("Password is short");
// ensure password is strong enough using the zxcvbn library
if (zxcvbn(value).score < this.minStrength) throw new Error("Password is weak");
};
render() {
const { type, validator, onStateChanged, children, ...restProps } = this.props;
const { password, strength } = this.state;
const passwordLength = password.length;
const passwordStrong = strength >= this.minStrength;
const passwordLong = passwordLength > this.thresholdLength;
// dynamically set the password length counter class
const counterClass = ['badge badge-pill', passwordLong ? passwordStrong ? 'badge-success' : 'badge-warning' : 'badge-danger'].join(' ').trim();
// password strength meter is only visible when password is not empty
const strengthClass = ['strength-meter mt-2', passwordLength > 0 ? 'visible' : 'invisible'].join(' ').trim();
return (
<Fragment>
<div className="position-relative">
{/** Pass the validation and stateChanged functions as props to the form field **/}
<FormField type="password" validator={this.validatePasswordStrong} onStateChanged={this.stateChanged} {...restProps}>
<span className="d-block form-hint">To conform with our Strong Password policy, you are required to use a sufficiently strong password. Password must be more than 7 characters.</span>
{children}
{/** Render the password strength meter **/}
<div className={strengthClass}>
<div className="strength-meter-fill" data-strength={strength}></div>
</div>
</FormField>
<div className="position-absolute password-count mx-3">
{/** Render the password length counter indicator **/}
<span className={counterClass}>{ passwordLength ? passwordLong ? `${this.thresholdLength}+` : passwordLength : '' }</span>
</div>
</div>
</Fragment>
);
}
}
PasswordField.propTypes = {
label: PropTypes.string.isRequired,
fieldId: PropTypes.string.isRequired,
placeholder: PropTypes.string.isRequired,
required: PropTypes.bool,
children: PropTypes.node,
onStateChanged: PropTypes.func,
minStrength: PropTypes.number,
thresholdLength: PropTypes.number
};
export default PasswordField;
此组件使用zxcvbn
JavaScript 密码强度估计器包。该包导出一个zxcvbn()
函数,该函数将密码字符串作为其第一个参数,并返回一个具有多个用于估计密码强度的属性的对象。在本教程中,我们只关心 score 属性,它是一个来自0
–的整数4
,它对于实现视觉强度条很有用。
以下是PasswordField
组件中发生的事情的细分:
初始化:在 中constructor()
,您创建了两个实例属性,thresholdLangth
和minStrength
,从它们对应的 prop 传递给组件。该thresholdLength
是最小密码长度,才能足够长的考虑。它默认为7
并且不能更低。该minStrength
是最低zxcvbn
分数之前,密码被认为是足够强大的。它的值范围从0-4
。3
如果未指定,则默认为。
您还初始化了密码字段的内部状态以存储当前password
和密码strength
。
处理密码更改:您定义了一个密码验证函数,该函数将传递给validator
底层FormField
组件的prop 。该函数确保密码长度大于thresholdLength
并且还具有zxcvbn()
指定的最小分数minStrength
。
您还定义了一个stateChanged()
函数,该函数将传递给组件的onStateChanged
prop FormField
。此函数检索FormField
组件的更新状态并使用它来计算和更新PasswordField
组件的新内部状态。
内部状态更新后会调用回调函数。回调函数调用组件的可选onStateChanged
属性中传递的函数PasswordField
,将更新的FormField
状态作为参数传递。
渲染和道具:在这里,您渲染了底层FormField
组件以及输入提示、密码强度计和密码长度计数器的一些元素。
密码强度计根据状态指示strength
当前password
的 ,invisible
如果密码长度为 ,则动态配置为0
。仪表将针对不同的强度级别显示不同的颜色。
密码长度计数器指示密码何时足够长。如果密码不长于thresholdLength
,则显示密码长度,否则显示thresholdLength
后跟一个plus(+)
。
该PasswordField
组件接受两个额外的可选字段minStrength
和thresholdLength
,如组件的propTypes
.
该JoinForm
组件
JoinForm.js
在src/components
目录中创建一个新文件并向其中添加以下代码片段:
import React, { Component } from 'react';
import FormField from './FormField';
import EmailField from './EmailField';
import PasswordField from './PasswordField';
class JoinForm extends Component {
// initialize state to hold validity of form fields
state = { fullname: false, email: false, password: false }
// higher-order function that returns a state change watch function
// sets the corresponding state property to true if the form field has no errors
fieldStateChanged = field => state => this.setState({ [field]: state.errors.length === 0 });
// state change watch functions for each field
emailChanged = this.fieldStateChanged('email');
fullnameChanged = this.fieldStateChanged('fullname');
passwordChanged = this.fieldStateChanged('password');
render() {
const { fullname, email, password } = this.state;
const formValidated = fullname && email && password;
// validation function for the fullname
// ensures that fullname contains at least two names separated with a space
const validateFullname = value => {
const regex = /^[a-z]{2,}(\s[a-z]{2,})+$/i;
if (!regex.test(value)) throw new Error('Fullname is invalid');
};
return (
<div className="form-container d-table-cell position-relative align-middle">
<form action="/" method="POST" noValidate>
<div className="d-flex flex-row justify-content-between align-items-center px-3 mb-5">
<legend className="form-label mb-0">Support Team</legend>
{/** Show the form button only if all fields are valid **/}
{ formValidated && <button type="button" className="btn btn-primary text-uppercase px-3 py-2">Join</button> }
</div>
<div className="py-5 border-gray border-top border-bottom">
{/** Render the fullname form field passing the name validation fn **/}
<FormField type="text" fieldId="fullname" label="Full Name" placeholder="Enter Full Name" validator={validateFullname} onStateChanged={this.fullnameChanged} required />
{/** Render the email field component **/}
<EmailField fieldId="email" label="Email" placeholder="Enter Email Address" onStateChanged={this.emailChanged} required />
{/** Render the password field component using thresholdLength of 7 and minStrength of 3 **/}
<PasswordField fieldId="password" label="Password" placeholder="Enter Password" onStateChanged={this.passwordChanged} thresholdLength={7} minStrength={3} required />
</div>
</form>
</div>
);
}
}
export default JoinForm;
该JoinForm
组件包装了构成我们表单的表单字段组件。我们初始化状态保持三个表单域的有效性:fullname
,email
,和password
。它们最初都是false
或invalid
。
我们还为每个字段定义了状态更改监视函数,以相应地更新表单状态。watch 函数检查errors
字段中是否存在 ,并将该字段的表单内部状态更新为true
, 或valid
。然后将这些监视功能分配给onStateChanged
每个表单字段组件的prop 以监视状态变化。
最后,呈现表单。请注意,您向该fullname
字段添加了一个验证函数,以确保至少提供两个名称,由空格分隔且仅包含字母字符。
该App
组件
直到此时,浏览器仍然呈现样板 React 应用程序。现在,您将修改该App.js
文件中的src
目录,以使JoinForm
内部AppComponent
。
该App.js
文件将类似于以下代码段:
import React from 'react';
import JoinForm from './components/JoinForm';
import './App.css';
function App() {
return (
<div className="main-container d-table position-absolute m-auto">
<JoinForm />
</div>
);
}
export default App;
第 3 步 — 使用 Sass 进行样式设置
您离应用程序的最终外观和感觉仅一步之遥。此刻,一切似乎都有些格格不入。在这一步中,您将继续定义一些样式规则来设置表单的样式。
为了利用强大的 Sass 变量、嵌套和循环,我们之前安装了node-sass
. 您正在使用 Sass 生成浏览器可以理解的 CSS 文件。
安装依赖项后,您需要更改两件事才能在应用程序中使用 Sass:
- 将文件重命名
src/App.css
为src/App.scss
. - 编辑导入行
src/App.js
以引用重命名的文件。
重命名src/App.css
文件后,将文件更新src/App.js
为以下内容:
import './App.scss';
保存并关闭文件。
接下来,App.scss
使用以下代码替换文件中的现有内容以格式化应用程序:
/** Declare some variables **/
$primary: #007bff;
// Password strength meter color for the different levels
$strength-colors: (darkred, orangered, orange, yellowgreen, green);
// Gap width between strength meter bars
$strength-gap: 6px;
body {
font-size: 62.5%;
}
.main-container {
width: 400px;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.form-container {
bottom: 100px;
}
legend.form-label {
font-size: 1.5rem;
color: desaturate(darken($primary, 10%), 60%);
}
.control-label {
font-size: 0.8rem;
font-weight: bold;
color: desaturate(darken($primary, 10%), 80%);
}
.form-control {
font-size: 1rem;
}
.form-hint {
font-size: 0.6rem;
line-height: 1.4;
margin: -5px auto 5px;
color: #999;
&.error {
color: #C00;
font-size: 0.8rem;
}
}
button.btn {
letter-spacing: 1px;
font-size: 0.8rem;
font-weight: 600;
}
.password-count {
bottom: 16px;
right: 10px;
font-size: 1rem;
}
.strength-meter {
position: relative;
height: 3px;
background: #DDD;
margin: 7px 0;
border-radius: 2px;
// Dynamically create the gap effect
&:before,
&:after {
content: '';
height: inherit;
background: transparent;
display: block;
border-color: #FFF;
border-style: solid;
border-width: 0 $strength-gap 0;
position: absolute;
width: calc(20% + #{$strength-gap});
z-index: 10;
}
// Dynamically create the gap effect
&:before {
left: calc(20% - #{($strength-gap / 2)});
}
// Dynamically create the gap effect
&:after {
right: calc(20% - #{($strength-gap / 2)});
}
}
.strength-meter-fill {
background: transparent;
height: inherit;
position: absolute;
width: 0;
border-radius: inherit;
transition: width 0.5s ease-in-out, background 0.25s;
// Dynamically generate strength meter color styles
@for $i from 1 through 5 {
&[data-strength='#{$i - 1}'] {
width: (20% * $i);
background: nth($strength-colors, $i);
}
}
}
您已成功添加应用程序所需的样式。请注意在.strength-meter:before
和.strength-meter:after
伪元素中使用生成的 CSS 内容为密码强度计添加间隙。
您还使用 Sass@for
指令为不同密码强度级别的强度计动态生成填充颜色。
最终的应用程序屏幕将如下所示:
对于验证错误,屏幕将如下所示:
没有任何错误,当所有字段都有效时,屏幕将如下所示:
结论
在本教程中,您基于zxcvbn
React 应用程序中的JavaScript 库创建了一个密码强度计。有关库的详细使用指南和文档zxcvbn
,请参阅zxcvbn
GitHub 上的存储库。有关本教程的完整代码示例,请查看 GitHub 上的password-strength-react-demo存储库。您还可以在 Code Sandbox 上获得本教程的现场演示。
如果你对本文的AngularJS版本感兴趣,可以看看:AngularJS中的密码强度计。