- 投稿日:2020-01-13T22:44:29+09:00
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
を作成します。Eloquent モデル更新
Eloquent モデル
App\User
を編集します。テーブル名や主キーのカラム名を設定します。また、タイムスタンプ(created_at
とupdated_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)がインストールされるので注意が必要です。1composer 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=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxEloquent モデルの更新
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()
のみです。3namespace 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()
で指定しています。4namespace 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.php
にSHA256ServiceProvider
を追加します。'providers' => [ /* 省略 */ Illuminate\Hashing\HashServiceProvider::class, /* 省略 */ App\Providers\EventServiceProvider::class, App\Providers\RouteServiceProvider::class, App\Providers\SHA256ServiceProvider::class, // 追加 ],ハッシュ設定の変更
config/hashing.php
を編集します。
driver
をbcrypt
からsha256
へ変更します。5return [ /* |-------------------------------------------------------------------------- | 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
ヘッダ7 にapplication/json
を指定する必要があります。まとめ
ここまで書いて、以前チラ見した 【Laravel】JWTを使って認証システムを構築する を見たら、ほぼ同じことが(しかもより詳しく)書いてあることに気づいたのでした。
ただ、JWT 認証の事例が基本的なものばかりだったので、 JWT 認証でも全然カスタマイズできるよ ということを書いておきたいなと思いました。8
JWT 認証そのものにも、まだまだ調べないといけないことが多いのですが、一旦、今回はここまでということで...9
参考
jwt-auth
- APIを使うときはリクエストヘッダにAcceptを入れる(Laravel)
- aravel tymon/jwt-auth による JWT 認証
- Laravel5でSHA1のHasherを作成する
現在入手可能な最新版は
1.0.0-rc5
。1.0.0-rc5
で Laravel 6 に対応したため、6 を使う場合は、このバージョンを使う必要がある。それにしても、いつになったら1.0.0
になるのだろうか... ↩Laravel は
password_hash()
をはじめとする 関数群 を使う想定のようです。SHA256 で使うhash()
にはpassword_get_info()
などに相当する機能がないので、適当に実装しています。 ↩いろいろな記事を見ると、
config/app.php
で既存のプロバイダをコメントアウトして、新しく作成したプロバイダを追加しているようです。コメントアウトしなくても、 カスタムガードの追加 (Auth::extend()
)と同様に追加できないのか? というのが、そもそものきっかけでした。結果、HashManager
(ファサードHash
の実体)の親クラスIlluminate\Support\Manager
にextend()
が存在しており、Hash::extend()
で追加できることがわかりました。 ↩
Illuminate\Support\Manager\extend()
を実行すると、配列customCreators
にドライバ名とクロージャが登録されます。HashManager
(ファサードHash
の実体)のdriver()
を呼び出すと、まずgetDefaultDriver()
が実行されます。getDefaultDriver()
はconfig/hashing.php
のdriver
に設定されている値を取得します。次いで、取得した値をもとにManager\createDriver()
が実行されます。createDriver()
は、配列customCreators
にドライバ名が登録されているかを確認します。登録されていたら、それに紐付けられているクロージャを呼び出します。こうして、SHA256 によるハッシュ化が行われるわけです。 ↩それぞれのパーツが(しつこいくらい)抽象化されているので、パーツの中をカスタマイズしても、全体としてみればちゃんと動くようになっているわけですね。なかなかソースコードを追うのは大変でしたが、理解したら「なるほど〜」と唸らされました。 ↩
Laravelのソースコードを3日間読んだら、さすがに疲れた...... 明日から仕事? 先生、本当ですか... ↩
- 投稿日:2020-01-13T18:16:13+09:00
ヘルパ関数の自作 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(); }
- 投稿日:2020-01-13T16:28:55+09:00
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/
- 投稿日:2020-01-13T11:20:38+09:00
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.phpindex.blade.phpとFormRequestを使いまわして、
追加処理画面と編集画面を兼用した場合の話。FormRequestを設定して@addと@editの引数のバリデーションしたら、
@editの方だけが500エラーがでて動かないことがありました。
その対処法の一つです。FormRequestを分けます。
エラーの出なかった@addはそのまま。
エラーの出た@update用をUpdateRequest等とします。UpdateRequestpublic 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.phpRoute::get('/index', 'Controller@index')->name('Controller.index'); //getメソッドとすることで正常動作 Route::get('/reedit', 'Controller@reEdit')->name('Controller.reEdit');Controller@reEditpublic 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')); }以上です。
追加画面と編集画面を分けていればおそらくこんなことにはならない。
コードの好みの問題かなあ。