序章

パスワードは、ほとんどの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 まだインストールしていない場合は、システムに次のように入力します。

  1. npm install -g create-react-app

インストールが完了したら、次のコマンドを使用して新しいReactアプリケーションを起動します。

  1. 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.

次に、アプリケーションに必要な依存関係をインストールします。 次のコマンドを実行して、必要な依存関係をインストールします。

  1. yarn add zxcvbn isemail prop-types node-sass bootstrap

このコマンドは、次の依存関係をインストールします。

  • zxcvbn -前述のパスワード強度推定ライブラリ。
  • isemail -電子メール検証ライブラリ。
  • prop-types -コンポーネントに渡される意図されたタイプのプロパティのランタイムチェック。
  • node-sass -SassファイルをCSSにコンパイルするために使用されます。

お気づきかもしれませんが、 bootstrap デフォルトのスタイルを取得するためのアプリケーションの依存関係としてのパッケージ。 アプリケーションにBootstrapを含めるには、 src/index.js ファイルを作成し、次の行を他のすべての前に追加します import 声明:

src / index.js
import 'bootstrap/dist/css/bootstrap.min.css';

最後に、アプリケーションを開始します。

  1. yarn start

これでアプリケーションが開始され、開発を開始できます。 ライブリロード機能を備えたブラウザタブが開いていることに注意してください。 これにより、開発時にアプリケーションの変更と同期が保たれます。

この時点で、アプリケーションビューは次のスクリーンショットのようになります。

ステップ2—コンポーネントの構築

このアプリケーションは、氏名、電子メール、およびパスワードのフォームを使用します。 また、フィールドで軽量のフォーム検証を実行します。 このステップでは、次のReactコンポーネントを作成します。

  • FormField-フォーム入力フィールドをその属性と変更イベントハンドラーでラップします。

  • EmailField-メールをラップします FormField それに電子メール検証ロジックを追加します。

  • PasswordField-パスワードをラップします FormField パスワード検証ロジックを追加します。 また、パスワード強度メーターとその他の視覚的な手がかりをフィールドに添付します。

  • JoinForm-フォームフィールドを格納する架空のJoinSupportTeamフォーム。

作成する components 内部のディレクトリ src すべてのコンポーネントを格納するアプリケーションのディレクトリ。

The FormField 成分

新しいファイルを作成する FormField.js の中に src/components ディレクトリに次のコードスニペットを追加します。

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 prop、現在の入力値で呼び出し、スローされた検証エラーを状態に追加します errors 配列(エラーがある場合)。

最後に、状態を更新し、更新後に呼び出されるコールバック関数を渡します。 コールバック関数は、オプションで渡された関数を呼び出します onStateChanged prop、更新された状態を引数として渡します。 これは、コンポーネントの外部に状態変化を伝播するのに便利です。

レンダリングと小道具:ここでは、入力フィールドとそのラベルをレンダリングしています。 また、状態の最初のエラーを条件付きでレンダリングします errors 配列(エラーがある場合)。 Bootstrapの組み込みクラスを使用して、検証ステータスを表示するように入力フィールドのクラスを動的に設定する方法に注目してください。 また、コンポーネントに含まれるすべての子ノードをレンダリングします。

コンポーネントに見られるように propTypes、このコンポーネントに必要な小道具は次のとおりです。 type ('text' また 'password'), label, placeholder、 と fieldId. 残りのコンポーネントはオプションです。

The EmailField 成分

新しいファイルを作成する EmailField.js の中に src/components ディレクトリに次のコードスニペットを追加します。

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 小道具。 あなたは使用しています validate() の方法 isemail 電子メール検証用のパッケージ。

また、を除く他のすべての小道具に気付くかもしれません typevalidator 小道具はから転送されます EmailField コンポーネントに FormField 成分。

The PasswordField 成分

新しいファイルを作成する PasswordField.js の中に src/components ディレクトリに次のコードスニペットを追加します。

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() パスワード文字列を最初の引数として受け取り、パスワード強度を推定するためのいくつかのプロパティを持つオブジェクトを返す関数。 このチュートリアルでは、scoreプロパティのみに関心があります。これはからの整数です。 04、これは視覚的な強度バーを実装するのに役立ちます。

これがで起こっていることの内訳です PasswordField 成分:

初期化constructor()、2つのインスタンスプロパティを作成しました。 thresholdLangthminStrength、コンポーネントに渡された対応するプロップから。 The thresholdLength は、十分に長いと見なされる前のパスワードの最小の長さです。 デフォルトは 7 低くすることはできません。 The minStrength 最小です zxcvbn パスワードが十分に強力であると見なされる前にスコアを付けます。 その値の範囲は 0-4. デフォルトは 3 指定されていない場合。

また、パスワードフィールドの内部状態を初期化して、現在のパスワードフィールドを保存しました password とパスワード strength.

パスワード変更の処理:に渡されるパスワード検証関数を定義しました validator 基礎となる小道具 FormField 成分。 この関数は、パスワードの長さが thresholdLength 最小値もあります zxcvbn() 指定されたスコア minStrength.

また、 stateChanged() 関数は、に渡されます onStateChanged の小道具 FormField 成分。 この関数は、の更新された状態を取得します FormField コンポーネントを使用して、の新しい内部状態を計算および更新します。 PasswordField 成分。

コールバック関数は、内部状態の更新後に呼び出されます。 コールバック関数は、オプションで渡された関数を呼び出します onStateChanged の小道具 PasswordField コンポーネント、更新されたものを渡す FormField その引数として状態。

レンダリングと小道具:ここでは、基になるレンダリングを行いました FormField 入力ヒントパスワード強度メーター、およびパスワード長カウンターのいくつかの要素と一緒のコンポーネント。

パスワード強度メーターは、 strength 現在の password 状態に基づいており、動的に構成されています invisible パスワードの長さが 0. メーターは、強度レベルごとに異なる色を示します。

パスワード長カウンターは、パスワードが十分に長い場合を示します。 パスワードがパスワードより長くない場合は、パスワードの長さが表示されます thresholdLength、それ以外の場合は、 thresholdLength 続いて plus(+).

The PasswordField コンポーネントは、2つの追加のオプションフィールドを受け入れます。 minStrengththresholdLength、コンポーネントで定義されているように propTypes.

The JoinForm 成分

新しいファイルを作成する JoinForm.js の中に src/components ディレクトリに次のコードスニペットを追加します。

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;

The JoinForm コンポーネントは、フォームを構成するフォームフィールドコンポーネントをラップします。 3つのフォームフィールドの有効性を保持するために状態を初期化しました。 fullname, email、 と password. それらはすべてです false、 また invalid、最初は。

また、各フィールドの状態変更監視関数を定義して、それに応じてフォームの状態を更新しました。 ウォッチ機能は、存在しないかどうかをチェックします errors フィールドで、そのフィールドのフォームの内部状態を次のように更新します true、 また valid. これらの監視機能は、 onStateChanged 状態の変化を監視するための各フォームフィールドコンポーネントの小道具。

最後に、フォームがレンダリングされます。 検証関数をに追加したことに注意してください fullname スペースで区切られ、英字のみを含む少なくとも2つの名前が指定されていることを確認するフィールド。

The App 成分

この時点まで、ブラウザーはボイラープレートReactアプリケーションをレンダリングします。 次に、を変更します App.js のファイル src レンダリングするディレクトリ JoinFormAppComponent.

The 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.csssrc/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);
    }
  }
}

アプリケーションに必要なスタイルを追加することに成功しました。 で生成されたCSSコンテンツの使用に注意してください .strength-meter:before.strength-meter:after パスワード強度メーターにギャップを追加するための疑似要素。

あなたもSassを使用しました @for さまざまなパスワード強度レベルで強度メーターの塗りつぶし色を動的に生成するディレクティブ。

最終的なアプリ画面は次のようになります。

検証エラーがある場合、画面は次のようになります。

エラーがない場合、すべてのフィールドが有効な場合、画面は次のようになります。

結論

このチュートリアルでは、に基づいてパスワード強度メーターを作成しました zxcvbn ReactアプリケーションのJavaScriptライブラリ。 詳細な使用ガイドとドキュメントについては zxcvbn ライブラリについては、GitHubのzxcvbnリポジトリを参照してください。 このチュートリアルの完全なコードサンプルについては、GitHubのpassword-strength-react-demoリポジトリを確認してください。 また、CodeSandboxでこのチュートリアルのライブデモを入手することもできます。

この記事のAngularJSバージョンに興味がある場合は、AngularJSのパスワード強度メーターをご覧ください。