Lesson 8

認証

Lesson 8 Chapter 1
ユーザー管理用のモデル

作成したアプリケーション内において、特定のユーザーのみ使用できるようにしたり、ユーザーごとに異なるデータを保有する場合には、ユーザーを特定するための「認証」を行う必要があります。

ただ、認証機能の実装にはログイン/ログアウトの処理や、認証方法の設定、パスワードの暗号化など様々な処理が必要となるため、CakePHPではそれら認証用の機能をひとまとめにした 「Authentication」というプラグインを用意しています。

Lesson8ではこの「Authentication」プラグインを使って、アプリケーションにログイン/ログアウトを使用する認証機能を実装していきます。

モデルについて

ログイン認証を行うにあたって、まずはユーザー情報とそのパスワードなどを保存しておくためのテーブルが必要となるのですが、これは前回の Lesson7 で 「usersテーブル」としてbakeコマンドを使うために作成していました。

usersテーブルの構成ですが、「ユーザーネーム」と「パスワード」でログイン認証を行うことを前提に、「username」と「passward」カラムを持たせています。
また、 passward カラムはパスワードを保存するにあたって一般的とされる varchara(255) として作成しています。

認証プラグインのインストール

CakePHP4では、冒頭で紹介した「Authentication」という「認証に必要な機能をまとめたプラグイン」を使用することで、ユーザー認証機能を効率的に実装することができます。「Authentication」プラグインのインストールには CakePHPのインストールにも使用した「composer」を使います。コマンドライン上で以下のコマンドを実行してください。

composer require "cakephp/authentication"

自動的に最新のauthenticationプラグインがインストールされます。

8_1_1.png

パスワードをハッシュ化する

ユーザー作成時に設定したパスワードは、何もしなければそのままの文字列(平文)でテーブルに保存されてしまいます。この状態だと、テーブルのデータが何らかの理由で第三者へと流出した場合、パスワード情報などが簡単に漏洩してしまうなどのリスクがあります。

そのため、パスワードを保存する際には「ハッシュ化」という方法で平文から不可逆(元に戻せない)な「ハッシュ値」へと変更してから保存し、データが流出した場合でもパスワード情報を割り出せないようにします。

ハッシュ化には「ハッシュ関数」というものが使われ、与えられたテキストのデータを特定のルールに従って作られる「ハッシュ値」へと変換されます。

また、「ハッシュ関数」は与えられたテキストが同じであれば、何度実行しても同じ「ハッシュ値」を返します。そのため、認証時にはユーザーが入力したパスワードをハッシュ化し、そのハッシュ値とテーブルに保存されているハッシュ値が一致するかどうか確認することでユーザーが正しいパスワードを入力したかを確認することが可能となります。

「Authentication」プラグインには上記のハッシュ化を行う機能も用意されており src/Model/Entity/User.php に以下のuse文とメソッドを追加することで実装することが出来ます。早速編集を行いましょう。

User.php
namespace App\Model\Entity;

use Authentication\PasswordHasher\DefaultPasswordHasher; // use文を追加
use Cake\ORM\Entity;
class User extends Entity
{
    // bake で生成されたコードがあります
    ...

    // クラスの最後に_setPasswordメソッドを追加
    protected function _setPassword(string $password) : ?string
    {
        if (strlen($password) > 0) {
            return (new DefaultPasswordHasher())->hash($password);
        }
    }
}

use文によってDefaultPasswordHasherクラスが呼ばれ、メソッドにはパスワードのテキストが引数として与えられます。if 文では、strlen()メソッドによってパスワードの文字数が数えられ、0以上であった場合に DefaultPasswordHasher のhashメソッドによってハッシュ化が行われ、ハッシュ値が返されるという内容となります。

salt の変更

次にハッシュ化を行う際に使用される「salt」と呼ばれるランダムな数字の編集を行います。これは、ハッシュ化が行われる際に「元のテキスト」と「saltの値」の2つの値が使われているのですが、CakePHPではこの「salt」のデフォルト値が固定となっているため、仮に変えないままでいると容易に「salt」の値を推測されてしまい、パスワードを特定されるリスクが高まります。そのため、デフォルトの「salt」の値を書き換える作業が必要となる訳です。

それでは、編集を行うために todo4/config/app_local.phpを開き、検索窓で「SECURITY_SALT」を検索してください。

   'Security' => [
    'salt' => env('SECURITY_SALT', '5e41925ec......ランダムな数字'),
],

このような文を見つけることが出来るかと思います。この中でenvメソッドの第二引数に取られているランダムな英数字を別のランダムな英数字に書き換えます。特に文字数などは決まっていませんが一般には64文字程度とされ、英数字のみとなります。

ハッシュ化の確認

ハッシュ化が行われるかの確認のために、MysqlのコマンドラインでUsersテーブルを一度見てみましょう。

select * from users;

8_1_2.png

前回のレッスンで作成したユーザーのpasswardがハッシュ化されずにそのままであることが分かります。

次に、ブラウザでユーザー情報の編集画面へと移動し、パスワードを変更して保存をします。

8_1_3.png

無事に編集が完了した後、再度MySQlのコマンドラインでselect文を流します。すると、passwardの値がハッシュ化された値になっていることが確認できると思います。これでハッシュ化の実装は完了です。

Lesson 8 Chapter 2
認証インターフェイスの実装

ここからの Chapter2 では、インストールした「authenticationプラグイン」に含まれている認証用の「インターフェイス」や、「ミドルウェア」を実装し、アプリケーションにリクエストが送られてきた場合に「自動的にユーザーが認証されているかを確認し、その結果をコントローラへ渡す」という処理が行われるようにしていきます。

まず、リクエストがコントローラに到達するまでの流れについてですが、CakePHPにおいてアプリケーションがリクエストを受けてから最初に起動するのは「webroot/index.php」というファイルであり、ここでアプリケーションの起点となる「src/Application.php」が呼び出され、アプリケーションの初期設定や、使用するミドルウェアの設定が行われます。その後にミドルウェアが展開され、最終的にコントローラへとリクエストが渡ります。

文字だけでは分かりにくいので、認証機能がどこで設定され、どのように実行されていくのかを中心に、リクエストがコントローラへと渡るまでの流れを以下の図で示します。

8_2_1.png

クライアントからリクエストが届き、webroot/index.phpが src/Apllicationクラスのインスタンスを作成。Apllicationクラス内のミドルウェア設定に「認証ミドルウェア」(AuthenticationMiddleware)を追加しておくことで、リクエスト処理時に「認証ミドルウェア」が実行され、ユーザーの認証が行われます。その認証結果はリクエストへと返され、コントローラへと渡り、指定のアクションが呼び出される前に src/Controller/AppControllerクラスが「認証結果」を確認します。その結果によってログインを求めるか、そのまま指定のアクションの実行に移るか、となります。

それでは全体の流れを確認したところで早速 src/Application.php に設定を記述していきましょう。以下の内容に従って編集を加えて下さい。
Application.php
//use文に以下の6行を追加
use Authentication\AuthenticationService;
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Authentication\Middleware\AuthenticationMiddleware;
use Cake\Routing\Router;
use Psr\Http\Message\ServerRequestInterface;

~~~
// classにサービスプロバイダインターフェイスを追加
// implements AuthenticationServiceProviderInterfaceを付け足します
class Application extends BaseApplication implements AuthenticationServiceProviderInterface

~~~

// middlewareメソッド内にて
// ->add(new RoutingMiddleware($this)) の後に以下を追加
->add(new AuthenticationMiddleware($this))

~~~

// middlewareメソッドの後に以下メソッドを追加
// AuthenticationMiddleware 内で呼び出されます
public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
{
    //ログインしていなければログイン画面へリダイレクト
    $authenticationService = new AuthenticationService([
        'unauthenticatedRedirect' => Router::url('/users/login'),
        'queryParam' => 'redirect',
    ]);

    // ユーザー認証に使用するフィールドとしてusernameとpasswordを設定
    $authenticationService->loadIdentifier('Authentication.Password', [
        'fields' => [
            'username' => 'username',
            'password' => 'password',
        ]
    ]);

    //  authenticatorsをロードし、セッションを開始
    $authenticationService->loadAuthenticator('Authentication.Session');
    // 入力した username と password をチェックする為のフォームデータを設定します
    $authenticationService->loadAuthenticator('Authentication.Form', [
        'fields' => [
            'username' => 'username',
            'password' => 'password',
        ],
        'loginUrl' => Router::url('/users/login'),//ログイン画面のURLを設定
    ]);

    return $authenticationService;
}

上記の内容ですが、最初にuse文で使用するクラスを指定し、次にApplicationクラスにAuthenticationServiceProviderInterfaceを実装しています。このインターフェイスにはgetAuthenticationService()メソッドのみが定義されており、Applicationクラスが getAuthenticationService()メソッドを実装することを強制しています。

次に認証用ミドルウェア(AuthenticationMiddleware)の追加を行い、最後にインターフェイスに強制されている getAuthenticationService()メソッドを実装しています。このメソッドは、ログイン画面のURLや認証に使用するフィールドなど、アプリケーションごとに変更が入る部分を指定できるようになっています。

これらの記述をしておくことで、リクエストがコントローラに到達する前に認証用ミドルウェアが発動し、getAuthenticationService()メソッドが呼び出され、「認証機能」としての処理を自動で行うようになります。

続いて、src/Contorller/AppController.php に、「Authentication」コンポーネントをロードする記述を加えておきます。この認証用コンポーネントは、認証結果の確認などのメソッドを持っており、次の Chapter3 で作成するログインアクション内でも呼び出して使用していきます。

AppController.php
//initializeメソッド内にて
//$this->loadComponent('Flash');の下に以下の行を追加
$this->loadComponent('Authentication.Authentication');

以上でリクエストがコントローラへと到達する前に、自動で「認証機能」を作動させるための設定は終了です。

更に詳しく「認証機能」の処理を追っていくと、認証ミドルウェア内で別のクラスやメソッドが呼ばれており、「認証」を実行したり、その結果をリクエストに返したりなどをしています。しかし、authenticationプラグインを使って「ログイン認証」を行うにあたっては、それらの処理を追ったり、1から作成したりする必要はなく、上記の設定をするのみで容易に「認証」を実行する仕組みを入れられるようになっています。

Lesson 8 Chapter 3
ログイン画面の作成

アクションの作成

ここまでで、「認証機能」を発動させるための仕組みは実装出来ましたが、ユーザーが「username」と「passward」を入力し、ログインを実行するための「認画面とそのアクションがまだのため、それぞれ作成していきます。

まずは、src/Controller/UsersController.phpにログインアクションを追加します。以下、2つのメソッドをUsersControllerクラスに追加してください。AppController.phpで追加した「Authentication」コンポーネントも使用しています。

userController.php
public function beforeFilter(\Cake\Event\EventInterface $event)
{
    parent::beforeFilter($event);
    //以降の処理はリクエスト処理前に行われる
    
    //loginアクションを認証の対象外とする
    $this->Authentication->addUnauthenticatedActions(['login']);
}

public function login()
{
    // POST,GETを問わずユーザーの認証結果を取得
    $this->request->allowMethod(['get', 'post']);
    $result = $this->Authentication->getResult();
    // 認証結果がある かつ 認証結果がtrueならば
    if ($result && $result->isValid()) {
        // tasks/indexへとリダイレクト
        $redirect = $this->request->getQuery('redirect', [
            'controller' => 'Tasks',
            'action' => 'index',
        ]);
    
        return $this->redirect($redirect);
    }
    // ユーザーがログインに失敗した場合は、エラーを表示します
    if ($this->request->is('post') && !$result->isValid()) {
        $this->Flash->error(__('username か passwordに誤りがあります'));
    }
}

テンプレートの作成

続いてログイン画面のビューテンプレートを作成します。これはbakeで自動生成されていないので手動で作る必要があります。templates/Users/login.phpを作成して編集をしていきましょう。

login.php
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="utf-8">
</head>

<div class="users form">
    <?= $this->Flash->render() ?>
    <h3>Login</h3>
    <?= $this->Form->create() ?>
    <fieldset>
        <legend><?= 'username と password を入力してログインして下さい' ?></legend>
        <?= $this->Form->control('username', ['required' => true]) ?>
        <?= $this->Form->control('password', ['required' => true]) ?>
    </fieldset>
    <?= $this->Form->submit('Login'); ?>
    <?= $this->Form->end() ?>
</div>

動作確認

ここまで出来たら一度、動作確認をしてみましょう。

  • users/loginにアクセスしログイン画面を表示

    8_3_1.png

  • ログイン前にusers/addなどにアクセスしloginへリダイレクトされることを確認

    8_3_2.png

  • 適当なusernameとpasswardを入力しエラーのフラッシュが出ることを確認

    8_3_3.png

  • 正しいusernameとpasswardを入力しログイン
  • tasks/ もしくはログイン前にアクセスしようとした画面にリダイレクトされる

ここまででログイン機能は終了です、次のチャプターでログアウト機能を作成していきます。

Lesson 8 Chapter 4
ログアウトアクションの作成

logoutアクション

src/Controller/UsersController.phpを開き、loginメソッドの下にlogoutメソッドを追加します。

    public function logout()
    {
        $result = $this->Authentication->getResult();
        if ($result && $result->isValid()) {
            $this->Authentication->logout();
            $this->Flash->success('ログアウトしました');
            return $this->redirect(['controller' => 'Users', 'action' => 'login']);
            
        }
    }

処理の内容としてはログインアクションとほぼ同様のものとなります。追加出来たら、users/logoutに直接アクセスしてみましょう。ログアウトとなり、ログイン画面に戻ります。

8_4_1.png

Lesson 8 Chapter 5
ログインを必要としないビューの設定

最後に、ログイン無しでも一覧画面と詳細画面のみアクセスできるように制御をしてみましょう。

src/Controller/AppController.phpを開き、以下のメソッドを追加します。

    public function beforeFilter(\Cake\Event\EventInterface $event)
    {
        parent::beforeFilter($event);
        //アプリケーション内のすべてのコントローラーを対象に
        //アクション名を指定して、認証チェックを外します。
        $this->Authentication->addUnauthenticatedActions(['index', 'view']);
    }

これでindexとviewアクションについてはログイン無しでも閲覧が許可されます。しかし、tasksコントローラーでは詳細画面のアクションをdetailという名前で作成していました。同様の機能なのでviewで統一してしまって良いのですが、今回はアプリケーション全体ではなく、TasksControllerのみを対象にdetailアクションの認証を外してみましょう。

src/Controller/TasksController.phpを開き、同様のメソッドを追加しましょう。

  public function beforeFilter(\Cake\Event\EventInterface $event)
  {
      parent::beforeFilter($event);
      // tasksコントローラーを対象に、指定したアクションの認証チェックを無しにします。
      $this->Authentication->addUnauthenticatedActions(['detail']);
  }

こちらでは'detail'のみを指定しています。

ここまで完了したら、動作確認としてログアウトした状態でtasksとusersの一覧・詳細画面を開いてみましょう。無事に表示され、ほかの新規作成や編集画面はログイン画面へとリダイレクトされていれば成功です。

以上で認証機能についてのレッスンは終了です。コードの内容を完全に把握というよりは、ログインに使用するフィールドを設定したり、認証を外すアクションを指定したりと、アプリケーションの内容に応じて編集を行う必要がある部分を中心に把握しておきましょう。