20200113のlaravelに関する記事は4件です。

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日間読んだら、さすがに疲れた...... 明日から仕事? 先生、本当ですか... 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ヘルパ関数の自作 by Laravel

helperでフラッシュメッセージ関数作成したんで備忘録

helpers.php
<?php
if (! function_exists('set_message')) {
    function set_message($msg = null, $is_success = true) {
        session()->flash('message', $msg);
        session()->flash('is_success', $is_success);
    }
}
composer.json
"autoload": {
    "classmap": [
        "database/seeds",
        "database/factories"
    ],
    "psr-4": {
        "App\\": "app/"
    },
    "files": [
        "app/helpers.php"//追加
    ]
},
console
$ composer dump-autoload

レイアウトに設定しておく。

layout.blade.php
@if (session('message'))                                               
    @if (session('is_success'))                                        
        <div class="alert alert-success">{{ session('message') }}</div>
    @else                                                              
        <div class="alert alert-danger">{{ session('message') }}</div> 
    @endif                                                             
    <?php
    session()->flash('message', null);                        
    session()->flash('is_succes', null);
    ?>
@endif                                                                 

使用例

xxxController
//どこからでも使用可能
public function add() {
    $item_id = session('id');
    if (isset($item_id)) {
        if ((new Cart)->addDb($item_id, 1))
            //正常メッセージ
            set_message('商品をカートに入れました');
        } else {
            //エラーメッセージ
            set_message('在庫が足りません', false);
        }
    }
    session()->forget('id');
    return $this->index();
}

参考サイト
Laravel 5へ自作のヘルパー関数を追加するベストプラクティス

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel の環境構築でつまづいた

はじめに

macOSでの話です。

Laravel公式のインストールサイト(日本語訳)通りに進んでいたが、

composer global require laravel/installer

の入力が上手くいかず、下記のエラーが出て数時間つまづいたので誰かの役に立ったらと思い書きます。

Your requirements could not be resolved to an installable set of packages.

Problem 1

Installation request for laravel/installer ^2.1 -> satisfiable by laravel/installer[v2.1.0].
laravel/installer v2.1.0 requires ext-zip * -> the requested PHP extension zip is missing from your system.

どのようにエラーを取ったか

同じ症状で悩んでいる人が teratailにいたので質問していたのでその回答通りにやってみました。(pecl install zip は peclコマンドが使えなかったのでスルーした。)

brew で phpを入れるのは、https://laracasts.com/series/laravel-6-from-scratch/episodes/1 の動画サイトを1~3まで見ればわかります。

同じ症状で悩んでいる人書いてくれている

brew link php

を実行する。

そうすると、

Linking /usr/local/Cellar/php/7.4.1... 
Error: Could not symlink bin/pear
Target /usr/local/bin/pear
already exists. You may want to remove it:
  rm '/usr/local/bin/pear'

To force the link and overwrite all conflicting files:
  brew link --overwrite php

To list all files that would be deleted:
  brew link --overwrite --dry-run php

というエラーが出てくるので

 brew link --overwrite php

を実行、そして再起動してcomposer global require laravel/installer
を実行して完了です。

補足

・・・composerのダウンロード
https://kohkimakimoto.github.io/getcomposer.org_doc_jp/doc/00-intro.html
これを手順に

composerの使い方。composer.jsonやcomposer.pharを使わなきゃいけなくなった時このサイトを勝つよすると良い。https://laboradian.com/php-composer/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel FormRequestの500エラー

正常時
index.blade.php -> Controller@add(add_flag) -> Model@addバリセーフ-> index.blade.php
index.blade.php -> Controller@edit(edit_flag) -> Model@updateバリセーフ -> index.blade.php
バリデーションアウト時
index.blade.php -> Controller@add(add_flag) -> Model@addバリアウト -> index.blade.php
index.blade.php -> Controller@edit(edit_flag) -> Model@updateバリアウト -> 500エラー
改善例
index.blade.php -> Controller@edit(edit_flag) -> Model@updateバリアウト -> Controller@reEdit -> index.blade.php

index.blade.phpとFormRequestを使いまわして、
追加処理画面と編集画面を兼用した場合の話。

FormRequestを設定して@add@editの引数のバリデーションしたら、
@editの方だけが500エラーがでて動かないことがありました。
その対処法の一つです。

FormRequestを分けます。
エラーの出なかった@addはそのまま。
エラーの出た@update用をUpdateRequest等とします。

UpdateRequest
public function rules()
{
//@addのものと同じ
}
//以下追加
protected function failedValidation(Validator $validator)
{
    $this->merge(['validated' => 'true']);
    throw new HttpResponseException(
        //編集 -> バリデーションチェックNG -> リダイレクト
        redirect(route('Controller.reEdit'))->withErrors($validator)->withInput()->with('request', $this->request)
    );
}

なんてことはありません。
エラーを拾ってバイバス用の@reEditメソッドへ飛ばしているだけです汗

web.php
Route::get('/index', 'Controller@index')->name('Controller.index');
//getメソッドとすることで正常動作
Route::get('/reedit', 'Controller@reEdit')->name('Controller.reEdit');
Controller@reEdit
public function reEdit(Request $request) {
    $old_input = $request->session()->get('_old_input');
    //$usersが複数形なのはindex.blade.phpの引数に合わせてます
    //違和感バリバリですみません・・
    $users = new Model;
    //バリデーションアウトなデータを直してもらうために
    $users->id = $old_input['id'];
    $users->name = $old_input['name'];
    $users->tel = $old_input['tel'];
    $is_edit = true;
    return view('Controller.index', compact('is_edit', 'users'));
}

以上です。

追加画面と編集画面を分けていればおそらくこんなことにはならない。
コードの好みの問題かなあ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む