仕事で考えないといけなくなったので、いろいろ試してみました。
作るもの
以下の条件を満たす認証機能を作ります。
- JWT 認証
- ユーザテーブルが、Laravel 標準のテーブル定義で ない
- デフォルトの
users
テーブルは使いません
- デフォルトの
- パスワードのハッシュ化が Laravel 標準の方法で ない
- デフォルトの
bcrypt
ではなくSHA256
を使います
- デフォルトの
なお、Laravel 標準が使える環境ならば、そうしたほうが良いと思います。
この記事は やむを得ず そうせざるを得ない場合のために書かれています。
前提
バージョン
- Laravel 6.9.0
- (2020/09/23追記) 7.x でも動きます。8.x でも動くとは思いますが、確認していません。
- 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)がインストールされるので注意が必要です。1
(2020/09/23追記)正式に 1.0 になったため、バージョン番号を指定する必要は無くなりました。
composer require tymon/jwt-auth
設定ファイルの配置
以下のコマンドを実行して、 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.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
へ変更します。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
ヘッダ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日間読んだら、さすがに疲れた...... 明日から仕事? 先生、本当ですか... ↩