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;
このコンポーネントは、zxcvbn
JavaScriptパスワード強度推定パッケージを使用しています。 このパッケージは、パスワード文字列を最初の引数として受け取り、パスワード強度を推定するためのいくつかのプロパティを持つオブジェクトを返す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アプリケーションのzxcvbn
JavaScriptライブラリに基づいてパスワード強度メーターを作成しました。 zxcvbn
ライブラリの詳細な使用ガイドとドキュメントについては、GitHubのzxcvbnリポジトリを参照してください。 このチュートリアルの完全なコードサンプルについては、GitHubのpassword-strength-react-demoリポジトリを確認してください。 また、CodeSandboxでこのチュートリアルのライブデモを入手することもできます。
この記事のAngularJSバージョンに興味がある場合は、AngularJSのパスワード強度メーターをご覧ください。