Help us understand the problem. What is going on with this article?

LaravelのJWT認証でいろいろカスタマイズ

仕事で考えないといけなくなったので、いろいろ試してみました。

作るもの

以下の条件を満たす認証機能を作ります。

  • JWT 認証
  • ユーザテーブルが、Laravel 標準のテーブル定義で ない
    • デフォルトの users テーブルは使いません
  • パスワードのハッシュ化が Laravel 標準の方法で ない
    • デフォルトの bcrypt ではなく SHA256 を使います

なお、Laravel 標準が使える環境ならば、そうしたほうが良いと思います。
この記事は やむを得ず そうせざるを得ない場合のために書かれています。

前提

バージョン

  • Laravel 6.9.0
  • PHP 7.4.1
  • MariaDB 10.3.11

扱わないこと

以下については、この記事では触れません。

  • Laravel 標準の認証機能について
  • JWT そのものについて
  • JWT 認証の是非について

手順

非標準のユーザテーブル

Laravel 標準では users テーブルを使用しますが、今回は、以下のようなテーブル t_user を作成します。

ユーザテーブル.png

Eloquent モデル更新

Eloquent モデル App\User を編集します。テーブル名や主キーのカラム名を設定します。また、タイムスタンプ( created_atupdated_at )は存在しないので無効にします。

namespace App;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;

/* ここから追加 */
    // テーブル名
    protected $table = 't_user';

    // 主キーのカラム名(デフォルトは id なので)
    public $primaryKey = 'user_id';

    // タイムスタンプ無効化(デフォルトで有効なので)
    public $timestamps = false;
/* ここまで追加 */
}

JWT認証

Laravel では標準の認証機能が存在しますが、今回は JWT 認証を行いたいと思います。
JWT 認証のライブラリは、 tymon/jwt-auth を使います。

インストール

composer でインストールします。
バージョン番号まで指定しないと、かなり古いバージョン(0.5.12)がインストールされるので注意が必要です。1

composer require tymon/jwt-auth 1.0.0-rc5

設定ファイルの配置

以下のコマンドを実行して、 config/jwt.php を作成します。

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

秘密鍵の生成

以下のコマンドを実行して、秘密鍵を生成します。

php artisan jwt:secret

.env に秘密鍵が設定されます。

JWT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Eloquent モデルの更新

Eloquent モデル App\User を編集します。

コントラクト JWTSubject にメソッド getJWTIdentifier()getJWTCustomClaims() が定義されているので、それらを実装します。

namespace App;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

/* 追加 */
use Tymon\JWTAuth\Contracts\JWTSubject;

/* implements を追加 */
class User extends Authenticatable implements JWTSubject
{
    use Notifiable;

    // テーブル名
    protected $table = 't_user';

    // 主キーのカラム名(デフォルトは id なので)
    public $primaryKey = 'user_id';

    // タイムスタンプ無効化(デフォルトで有効なので)
    public $timestamps = false;

/* ここから追加 */
    // JWT の sub に含める値。主キーを使う
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    // JWT のクレームに追加する値。今回は特になし
    public function getJWTCustomClaims()
    {
        return [];
    }
/* ここまで追加 */
}

ガードの設定

config/auth.php を編集します。

'defaults' => [
    // web を api へ変更
    // 'guard' => 'web',
    'guard'     => 'api',
    'passwords' => 'users',
],
'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        // token を jwt へ変更
        // 'driver' => 'token',
        'driver' => 'jwt',
        'provider' => 'users',
        // これは変更不要?
        // 'hash' => false,
    ],
],

ルーティングの追加

routes/api.php に以下を追加します。

Route::group([
    'middleware' => 'api',
    'prefix' => 'auth'
], function ($router) {
    Route::post('login', 'Api\AuthController@login')->name('login');
    Route::post('logout', 'Api\AuthController@logout');
    Route::post('refresh', 'Api\AuthController@refresh');
    Route::post('me', 'Api\AuthController@me');
});

コントローラの追加

以下のコマンドを実行して、コントローラの雛形を作ります。

php artisan make:controller Api/AuthController

作成された App\Http\Controllers\Api\AuthController を編集します。

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class AuthController extends Controller
{
    /**
     * コンストラクタ
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth:api', ['except' => ['login']]);
    }

    /**
     * ログイン
     * 認証に成功したら、トークンを返却する
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function login()
    {
        $credentials = request(['email', 'password']);

        if (! $token = auth()->attempt($credentials)) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

        return $this->respondWithToken($token);
    }

    /**
     * ユーザ情報の取得
     * 認証されたユーザの情報を返却する
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function me()
    {
        return response()->json(auth()->user());
    }

    /**
     * ログアウト
     * ログアウトし、トークンを無効にする
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function logout()
    {
        auth()->logout();

        return response()->json(['message' => 'Successfully logged out']);
    }

    /**
     * トークンのリフレッシュ
     * 古いトークンを新しいトークンでリフレッシュする
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function refresh()
    {
        return $this->respondWithToken(auth()->refresh());
    }

    /**
     * トークン返却
     * トークンに関する情報を配列化してからJSON形式で返却する
     *
     * @param  string $token
     *
     * @return \Illuminate\Http\JsonResponse
     */
    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => auth()->factory()->getTTL() * 60
        ]);
    }
}

非標準のパスワードのハッシュ化

Laravel 標準では bcrypt によるパスワードのハッシュ化が行われます。2

今回は SHA256 によるハッシュ化を行います。

ハッシュドライバ

ハッシュドライバ App\Extensions\SHA256Hasher を作成します。ハッシュドライバでは、ハッシュ作成やチェックなどの処理を行います。

メソッドを4つ実装していますが、実際に必要なのは make()check() のみです。3

namespace App\Extensions;

use Illuminate\Contracts\Hashing\Hasher as HasherContract;

class SHA256Hasher implements HasherContract
{
    // ハッシュ作成
    // $value のハッシュを返す
    // password_hash() に相当する
    public function make($value, array $options = [])
    {
        return hash('sha256', $value);
    }

    // ハッシュのチェック
    // $value のハッシュと、与えられたハッシュが一致するかをチェックする
    // password_verify() に相当する
    public function check($value, $hashedValue, array $options = [])
    {
        return $this->make($value) === $hashedValue;
    }

    // ハッシュの再計算が必要かのチェック
    // password_needs_rehash() に相当する
    // SHA256 では不要なので、常に false を返す
    public function needsRehash($hashedValue, array $options = [])
    {
        return false;
    }

    // ハッシュの情報取得
    // password_get_info() に相当する
    // SHA256 では不要なので、常に null を返す
    public function info($hashedValue)
    {
        return null;
    }
}

サービスプロバイダ追加

サービスプロバイダ App\Providers\SHA256ServiceProvider を作成します。

ドライバ名が sha256 の場合は、先程作成したハッシュドライバを使うように Hash::extend() で指定しています。4

namespace App\Providers;

use App\Extensions\SHA256Hasher;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Hash;

class SHA256ServiceProvider extends ServiceProvider implements DeferrableProvider
{
    public function boot()
    {
        Hash::extend('sha256', function ($app) {
            return new SHA256Hasher();
        });
    }
}

また、 config/app.phpSHA256ServiceProvider を追加します。

'providers' => [
    /* 省略 */

    Illuminate\Hashing\HashServiceProvider::class,

    /* 省略 */

    App\Providers\EventServiceProvider::class,
    App\Providers\RouteServiceProvider::class,
    App\Providers\SHA256ServiceProvider::class, // 追加
],

ハッシュ設定の変更

config/hashing.php を編集します。
driverbcrypt から sha256 へ変更します。5

return [

    /*
    |--------------------------------------------------------------------------
    | Default Hash Driver
    |--------------------------------------------------------------------------
    |
    | This option controls the default hash driver that will be used to hash
    | passwords for your application. By default, the bcrypt algorithm is
    | used; however, you remain free to modify this option if you wish.
    |
    | Supported: "bcrypt", "argon", "argon2id"
    |
    */

    // bcrypt から sha256 へ変更
    // 'driver' => 'bcrypt',
    'driver' => 'sha256',

    /* 省略 */
];

動作確認

ログイン

リクエストを送信すると、JWT が返却されます。

リクエスト

POST /api/auth/login?email=test@example.com&password=password HTTP/1.1
Host: test.example.com

レスポンス

成功時

{
    "access_token": "xxxxxxxxxx",
    "token_type": "bearer",
    "expires_in": 3600
}

失敗時

{
    "error": "Unauthorized"
}

ユーザ情報取得

ログインで取得した JWT を Authorization Bearer ヘッダ6に指定して、リクエストします。

リクエスト

POST /api/auth/me HTTP/1.1
Host: test.example.com
Accept: application/json
Authorization: Bearer xxxxxxxxxx

レスポンス

成功時

{
    "user_id": 1,
    "user_name": "山田太郎",
    "email": "test@example.com",
    "password": "password",
    "create_date": "2020-01-12 21:32:17",
    "update_date": "2020-01-02 18:23:36"
}

失敗時

{
    "message": "Unauthenticated."
}

注意点
Accept ヘッダ7application/json を指定する必要があります。

まとめ

ここまで書いて、以前チラ見した 【Laravel】JWTを使って認証システムを構築する を見たら、ほぼ同じことが(しかもより詳しく)書いてあることに気づいたのでした。

ただ、JWT 認証の事例が基本的なものばかりだったので、 JWT 認証でも全然カスタマイズできるよ ということを書いておきたいなと思いました。8

JWT 認証そのものにも、まだまだ調べないといけないことが多いのですが、一旦、今回はここまでということで...9

参考


  1. 現在入手可能な最新版は 1.0.0-rc51.0.0-rc5 で Laravel 6 に対応したため、6 を使う場合は、このバージョンを使う必要がある。それにしても、いつになったら 1.0.0 になるのだろうか... 

  2. 他にも Argon2 がデフォルトで選択可能です。詳しくは、ドキュメントの ハッシュ を参照。 

  3. Laravel は password_hash() をはじめとする 関数群 を使う想定のようです。SHA256 で使う hash() には password_get_info() などに相当する機能がないので、適当に実装しています。 

  4. いろいろな記事を見ると、 config/app.php で既存のプロバイダをコメントアウトして、新しく作成したプロバイダを追加しているようです。コメントアウトしなくても、 カスタムガードの追加Auth::extend() )と同様に追加できないのか? というのが、そもそものきっかけでした。結果、 HashManager (ファサード Hash の実体)の親クラス Illuminate\Support\Managerextend() が存在しており、 Hash::extend() で追加できることがわかりました。 

  5. Illuminate\Support\Manager\extend() を実行すると、配列 customCreators にドライバ名とクロージャが登録されます。 HashManager (ファサード Hash の実体)の driver() を呼び出すと、まず getDefaultDriver() が実行されます。 getDefaultDriver()config/hashing.phpdriver に設定されている値を取得します。次いで、取得した値をもとに Manager\createDriver() が実行されます。 createDriver() は、配列 customCreators にドライバ名が登録されているかを確認します。登録されていたら、それに紐付けられているクロージャを呼び出します。こうして、SHA256 によるハッシュ化が行われるわけです。 

  6. MDN HTTP認証 

  7. MDN Accept 

  8. それぞれのパーツが(しつこいくらい)抽象化されているので、パーツの中をカスタマイズしても、全体としてみればちゃんと動くようになっているわけですね。なかなかソースコードを追うのは大変でしたが、理解したら「なるほど〜」と唸らされました。 

  9. Laravelのソースコードを3日間読んだら、さすがに疲れた...... 明日から仕事? 先生、本当ですか... 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away