Reactでパスワード強度メーターを構築する方法
序章
パスワードは、ほとんどのWebアプリケーションでユーザー認証に一般的に使用されます。 このため、パスワードを安全な方法で保存することが重要です。 長年にわたり、データベースに保存されているパスワードの実際の表現を隠すために、一方向パスワードハッシュなどの手法が採用されてきました。
パスワードハッシュはパスワードを保護するための大きな一歩ですが、ユーザーは依然としてパスワードのセキュリティに大きな課題を抱えています。ブルートフォース攻撃のため、パスワードとして一般的な単語を使用するユーザーは、ハッシュの努力を無駄にします。 ]は、そのようなパスワードをすばやく解読できます。
これに対処するために、今日の多くのWebアプリケーションは、パスワードの最小長を確保するか、パスワードに英数字と記号を組み合わせることにより、強力なパスワードを持つユーザーを要求しています。 パスワード強度を測定するために、Dropboxは、パスワードクラッカーに触発された現実的なパスワード強度推定器のアルゴリズムを開発しました。 このアルゴリズムは、zxcvbnというJavaScriptライブラリにパッケージ化されています。 さらに、パッケージには、一般的に使用される英語の単語、名前、およびパスワードの辞書が含まれています。
このチュートリアルでは、React JavaScriptフレームワークを使用して、氏名、電子メール、およびパスワードのフィールドを持つフォームを作成します。 軽量のフォーム検証を実行し、 zxcvbn ライブラリを使用して、視覚的なフィードバックを提供しながら、フォーム内のパスワードの強度を推定します。
このチュートリアルの終わりまでに作成するもののこのCodeSandboxデモをチェックしてください。
前提条件
開始する前に、システムにNodeの最新バージョンがインストールされていることを確認してください。
このチュートリアルに従うには、次のものが必要です。
- マシンにインストールされているNodeの最新バージョン。 これをインストールする方法の詳細については、 How To InstallNode.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をグローバルにインストールする必要はありません。 次のコマンドで新しいReactアプリケーションを開始できます: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パッケージをインストールしました。 アプリケーションにブートストラップを含めるには、src/index.jsファイルを編集し、他のすべてのimportステートメントの前に次の行を追加します。
src / index.js
import 'bootstrap/dist/css/bootstrap.min.css';
最後に、アプリケーションを開始します。
yarn start
これでアプリケーションが開始され、開発を開始できます。 ライブリロード機能を備えたブラウザタブが開いていることに注意してください。 これにより、開発時にアプリケーションの変更と同期が保たれます。
この時点で、アプリケーションビューは次のスクリーンショットのようになります。
ステップ2—コンポーネントの構築
このアプリケーションは、氏名、電子メール、およびパスワードのフォームを使用します。 また、フィールドで軽量のフォーム検証を実行します。 このステップでは、次のReactコンポーネントを作成します。
- FormField-フォーム入力フィールドをその属性と変更イベントハンドラーでラップします。
- EmailField-電子メール
FormFieldをラップし、それに電子メール検証ロジックを追加します。 - PasswordField-パスワード
FormFieldをラップし、パスワード検証ロジックを追加します。 また、パスワード強度メーターとその他の視覚的な手がかりをフィールドに添付します。 - JoinForm-フォームフィールドを格納する架空のJoinSupportTeamフォーム。
アプリケーションのsrcディレクトリ内にcomponentsディレクトリを作成して、すべてのコンポーネントを格納します。
FormFieldコンポーネント
src/componentsディレクトリに新しいファイルFormField.jsを作成し、それに次のコードスニペットを追加します。
src / components / FormField.js
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フィールドであるかどうかを確認し、値が空の場合は状態errors配列に検証エラーを追加します。
ただし、フィールドが必須フィールドではない場合、または必須であるが空ではない場合は、オプションのvalidatorプロパティで渡された検証関数に委任し、現在の入力値で呼び出し、スローされた検証エラーを追加します状態errors配列に(エラーがある場合)。
最後に、状態を更新し、更新後に呼び出されるコールバック関数を渡します。 コールバック関数は、オプションのonStateChangedプロパティで渡された関数を呼び出し、更新された状態を引数として渡します。 これは、コンポーネントの外部に状態変化を伝播するのに便利です。
レンダリングと小道具:ここでは、入力フィールドとそのラベルをレンダリングしています。 また、状態errors配列の最初のエラーを条件付きでレンダリングします(エラーがある場合)。 Bootstrapの組み込みクラスを使用して、検証ステータスを表示するように入力フィールドのクラスを動的に設定する方法に注目してください。 また、コンポーネントに含まれるすべての子ノードをレンダリングします。
コンポーネントのpropTypesに見られるように、このコンポーネントに必要な小道具はtype('text'または'password')、label、[X127X ] 、およびfieldId。 残りのコンポーネントはオプションです。
EmailFieldコンポーネント
src/componentsディレクトリに新しいファイルEmailField.jsを作成し、それに次のコードスニペットを追加します。
src / components / EmailField.js
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プロップに渡します。 電子メールの検証には、isemailパッケージのvalidate()メソッドを使用しています。
typeとvalidatorの小道具を除く他のすべての小道具が、EmailFieldコンポーネントからFormFieldコンポーネントに転送されることにも気付くかもしれません。
PasswordFieldコンポーネント
src/componentsディレクトリに新しいファイルPasswordField.jsを作成し、それに次のコードスニペットを追加します。
src / components / PasswordField.js
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;
このコンポーネントは、zxcvbnJavaScriptパスワード強度推定パッケージを使用しています。 このパッケージは、パスワード文字列を最初の引数として受け取り、パスワード強度を推定するためのいくつかのプロパティを持つオブジェクトを返すzxcvbn()関数をエクスポートします。 このチュートリアルでは、0から4までの整数であるスコアプロパティのみを扱います。これは、視覚的な強度バーの実装に役立ちます。
PasswordFieldコンポーネントで起こっていることの内訳は次のとおりです。
初期化:constructor()で、コンポーネントに渡された対応する小道具から、thresholdLangthとminStrengthの2つのインスタンスプロパティを作成しました。 thresholdLengthは、十分に長いと見なされる前の最小パスワード長です。 デフォルトは7であり、これより低くすることはできません。 minStrengthは、パスワードが十分に強力であると見なされる前の最小zxcvbnスコアです。 その値の範囲は0-4です。 指定しない場合、デフォルトで3になります。
また、パスワードフィールドの内部状態を初期化して、現在のpasswordとパスワードstrengthを保存しました。
パスワード変更の処理:基になるFormFieldコンポーネントのvalidator小道具に渡されるパスワード検証関数を定義しました。 この関数は、パスワードの長さがthresholdLengthより長く、指定されたminStrengthの最小zxcvbn()スコアを持つことを保証します。
また、stateChanged()関数を定義しました。この関数は、FormFieldコンポーネントのonStateChangedプロップに渡されます。 この関数は、FormFieldコンポーネントの更新された状態を取得し、それを使用してPasswordFieldコンポーネントの新しい内部状態を計算および更新します。
コールバック関数は、内部状態の更新後に呼び出されます。 コールバック関数は、PasswordFieldコンポーネントのオプションのonStateChangedプロパティで渡された関数を呼び出し、更新されたFormField状態を引数として渡します。
レンダリングと小道具:ここでは、入力ヒント、パスワード強度メーター、およびパスワードのいくつかの要素とともに、基になるFormFieldコンポーネントをレンダリングしました。長さカウンター。
パスワード強度メーターは、状態に基づいて現在のpasswordのstrengthを示し、パスワードの長さが0の場合、動的にinvisibleになるように構成されます。 メーターは、強度レベルごとに異なる色を示します。
パスワード長カウンターは、パスワードが十分に長い場合を示します。 パスワードがthresholdLengthより長くない場合はパスワードの長さを示し、それ以外の場合はthresholdLengthの後にplus(+)を示します。
PasswordFieldコンポーネントは、コンポーネントのpropTypesで定義されているように、minStrengthとthresholdLengthの2つの追加オプションフィールドを受け入れます。
JoinFormコンポーネント
src/componentsディレクトリに新しいファイルJoinForm.jsを作成し、それに次のコードスニペットを追加します。
src / components / JoinForm.js
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の3つのフォームフィールドの有効性を保持するために状態を初期化しました。 最初はすべてfalseまたはinvalidです。
また、各フィールドの状態変更監視関数を定義して、それに応じてフォームの状態を更新しました。 監視機能は、フィールドにerrorsがないかどうかをチェックし、そのフィールドのフォームの内部状態をtrueまたはvalidに更新します。 次に、これらの監視機能が各フォームフィールドコンポーネントのonStateChanged支柱に割り当てられ、状態の変化を監視します。
最後に、フォームがレンダリングされます。 fullnameフィールドに検証関数を追加して、スペースで区切られ、アルファベット文字のみを含む少なくとも2つの名前が提供されていることを確認してください。
Appコンポーネント
この時点まで、ブラウザーはボイラープレートReactアプリケーションをレンダリングします。 次に、srcディレクトリのApp.jsファイルを変更して、AppComponent内にJoinFormをレンダリングします。
App.jsファイルは次のスニペットのようになります。
src / 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を利用するには、次の2つの変更が必要になります。
- ファイルの名前を
src/App.cssからsrc/App.scssに変更します。 src/App.jsのインポート行を編集して、名前が変更されたファイルを参照します。
src/App.cssファイルの名前を変更した後、src/App.jsファイルを次のように更新します。
src / App.js
import './App.scss';
ファイルを保存して閉じます。
次に、App.scssファイルの既存のコンテンツを次のコードに置き換えて、アプリケーションをフォーマットします。
src / 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ディレクティブを使用して、さまざまなパスワード強度レベルで強度メーターの塗りつぶし色を動的に生成しました。
最終的なアプリ画面は次のようになります。
検証エラーがある場合、画面は次のようになります。
エラーがない場合、すべてのフィールドが有効な場合、画面は次のようになります。
結論
このチュートリアルでは、ReactアプリケーションのzxcvbnJavaScriptライブラリに基づいてパスワード強度メーターを作成しました。 zxcvbnライブラリの詳細な使用ガイドとドキュメントについては、GitHubのzxcvbnリポジトリを参照してください。 このチュートリアルの完全なコードサンプルについては、GitHubのpassword-strength-react-demoリポジトリを確認してください。 また、CodeSandboxでこのチュートリアルのライブデモを入手することもできます。
この記事のAngularJSバージョンに興味がある場合は、AngularJSのパスワード強度メーターをご覧ください。