Laravelで独自認証ロジックを追加する
Laravelで独自認証ロジックを追加する方法を探してみたところ、バージョンの古いものは見つかるのだけど、新しいもの(2018/12現在は、Laravel 5.7)にそのまま入れてもうまくいかなかったので、こうやったらうまくいったというのをメモ。
何をしたかったか
APIを実装するにあたって、認証をBearer Tokenを使って、認証後はLaravelの標準処理に任せたかった。
やろうと試みたこと(実装にあたって試みたこと)
-
config/auth.php のguards、providersに適当なクラスを入れ込んで確認
どうやってもセッションで認証をしようとする。 -
app/Providers/AuthServiceProvider.php
追加はなんとなく出来たが、カスタマイズがしにくく、さらにroutes/api.phpでコントロールがしにくかった。
最終的に実装した方法
色々やってみた結果、以下の実装に落ち着いた。
- app/Guards/BearerGuard.phpを作成
- app/Http/Middleware/Guard/Provider/Modelを作成
- app/Providers/AuthCustomServiceProvider.phpを作成
- app/Models/Auth/User.phpを作成
- app/Http/Kernel.phpにMiddlewareクラスを追加
- app/Exceptions/Handler.phpの処理を変更
Guard
認証方法が独自(特定のリクエストヘッダーに設定されているトークンを使って認証)なので、新しいGuardを作成する。
app/Guards/BearerGuard.php
<?php
namespace App\Guards;
use Log;
use App\Models\Auth\User;
use Illuminate\Http\Request;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Auth\GuardHelpers as GuardHelpers;
class BearerGuard implements Guard
{
use GuardHelpers;
/** @var \Illuminate\Http\Request Request instance */
protected $request;
/** @var \Illuminate\Contracts\Auth\UserProvider Provider instance */
protected $provider;
/** @var \App\Models\Auth\User User Model instance */
protected $user;
/**
* Create a new authentication guard.
*
* @param \Illuminate\Contracts\Auth\UserProvider $provider
* @param \Illuminate\Http\Request $request
* @return void
*/
public function __construct(UserProvider $provider, Request $request)
{
$this->request = $request;
$this->provider = $provider;
$this->user = null;
}
/**
* Determine if the current user is authenticated.
*
* @return bool
*/
public function check()
{
return !is_null($this->user());
}
/**
* Determine if the current user is a guest.
*
* @return bool
*/
public function guest()
{
return !$this->check();
}
/**
* Get the currently authenticated user.
*
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user()
{
if (!is_null($this->user)) {
return $this->user;
}
$user = new User();
// Login with Bearer Token
$token = $this->request->bearerToken();
if (!is_null($token) && $this->user = $this->provider->retrieveByToken($user->getRememberTokenName(), $token)) {
$this->fireAuthenticatedEvent($this->user);
}
return $this->user;
}
/**
* Get the ID for the currently authenticated user.
*
* @return string | null
*/
public function id()
{
if ($user = $this->user()) {
return $this->user()->getAuthIdentifier();
}
return null;
}
/**
* Validate a user's credentials.
*
* @param array $credentials credentials user input
* @return bool
*/
public function validate(array $credentials = [])
{
return new \Exception('Not implemented');
}
/**
* Set the current user.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user User info
* @return void
*/
public function setUser(Authenticatable $user)
{
$this->user = $user;
}
/**
* Attempt to authenticate a user using the given credentials.
*
* @param array $credentials
* @param bool $remember
* @return bool
*/
public function attempt(array $credentials = [], $remember = false)
{
return new \Exception('Not implemented');
}
/**
* Determine if the user matches the credentials.
*
* @param mixed $user
* @param array $credentials
* @return bool
*/
protected function hasValidCredentials($user, $credentials)
{
return new \Exception('Not implemented');
}
/**
* Fire the authenticated event if the dispatcher is set.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @return void
*/
protected function fireAuthenticatedEvent($user)
{
if (isset($this->events)) {
$this->events->dispatch(
new Events\Authenticated(
$this, $user
));
}
}
}
Middleware
routes/api.phpを使って分離したかった&認証エラーのときにリダイレクトせずに401を返したかったので、Middlewareを新たに作成。
app/Http/Middleware/AuthenticateViaApi.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Factory as Auth;
class AuthenticateViaApi
{
/** @var \Illuminate\Contracts\Auth\Factory The authentication factory instance. */
protected $auth;
/**
* @var string Application Token name
*/
const APPLICATIN_TOKEN = 'x-application-token';
/**
* Create a new middleware instance.
*
* @param \Illuminate\Contracts\Auth\Factory $auth
* @return void
*/
public function __construct(Auth $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string[] ...$guards
* @return mixed
*/
public function handle($request, Closure $next, ...$guards)
{
$this->authenticate($request, $guards);
return $next($request);
}
/**
* Determine if the user is logged in to any of the given guards.
*
* @param \Illuminate\Http\Request $request
* @param array $guards
* @return void
*/
protected function authenticate($request, array $guards)
{
if (empty($guards)) {
$guards = [null];
}
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
return $this->auth->shouldUse($guard);
}
}
abort(401);
}
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function redirectTo($request)
{
return response()->json('Unauthorized', 401);
}
}
Provider
config/api.php 内のproviderなどを有効にするための処理を入れ込む
app/Providers/AuthCustomServiceProvider.php
<?php
namespace App\Providers;
use App\Extensions\UserCustomProvider;
use App\Guards\BearerGuard;
use App\Models\Auth\User;
use Auth;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthCustomServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
];
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
$this->app->bind('App\Models\Auth\User', function ($app) {
return new User();
});
// add custom guard provider
Auth::provider('auth.bearer', function ($app, array $config) {
return new UserCustomProvider($app->make('App\Models\Auth\User'));
});
// add custom guard
Auth::extend('bearer', function ($app, $name, array $config) {
$userProvider = app(UserCustomProvider::class);
$request = $app->make('request');
return new BearerGuard($userProvider, $request);
});
}
}
ユーザープロバイダーとモデルを関連付ける
app/Extensions/UserCustomProvider.php
<?php
namespace App\Extensions;
use Hash;
use Illuminate\Support\Str;
use Illuminate\Contracts\Auth\UserProvider;
use App\Models\Auth\User;
class UserCustomProvider implements UserProvider
{
/** @var object User Model */
private $model;
/**
* Create a new user provider.
*
* @param \App\Models\Auth\User User Model
*/
public function __construct(\App\Models\Auth\User $userModel)
{
$this->model = $userModel;
}
/**
* Retrieve a user by the given credentials.
*
* @param array $credentials
* @return \Illuminate\Contracts\Auth\Authenticatable | null
*/
public function retrieveByCredentials(array $credentials)
{
return new \Exception('Not implemented');
}
/**
* Validate a user against the given credentials.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param array $credentials Request credentials
* @return bool
*/
public function validateCredentials(\Illuminate\Contracts\Auth\Authenticatable $user, Array $credentials)
{
return new \Exception('Not implemented');
}
/**
* Retrieve a user by their unique identifier.
*
* @param mixed $identifier
* @return \Illuminate\Contracts\Auth\Authenticatable | null
*/
public function retrieveById($identifier)
{
return $this->model->where($this->model->getAuthIdentifierName(), $identifier)->first();
}
/**
* Retrieve a user by by their unique identifier and "remember me" token.
*
* @param mixed $identifier
* @param string $token
* @return \Illuminate\Contracts\Auth\Authenticatable | null
*/
public function retrieveByToken($identifier, $token)
{
return $this->model->where($this->model->getAuthIdentifierName(), $identifier)
->where($this->model->getRememberTokenName(), $token)
->first();
}
/**
* Update the "remember me" token for the given user in storage.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param string $token
* @return void
*/
public function updateRememberToken(\Illuminate\Contracts\Auth\Authenticatable $user, $token)
{
$this->model->where($this->model->getAuthIdentifierName(), $user->getAuthIdentifier())
->update([$this->model->getRememberTokenName() => $token]);
}
}
Model
ユーザー情報を保存するモデルを作成。Token認証だけ考えるので、シンプルに。
app/Models/Auth/User.php
<?php
namespace App\Models\Auth;
use Carbon\Carbon;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract
{
use Authenticatable, Authorizable, CanResetPassword;
// Specify table name
protected $table = 'users';
// Stop automatic updating created_at and updated_at
public $timestamps = false;
// Specify format for the columns type of "Date / Time"
protected $dateFormat = 'U';
private $username;
private $password;
private $rememberTokenIdentifierName = 'token';
protected $fillable = [
'id',
'token',
];
protected $hidden = [
];
public function __construct()
{
$this->hidden[] = $this->rememberTokenIdentifierName;
}
/**
* Fetch user by Credentials
*
* @param array $credentials
* @return Illuminate\Contracts\Auth\Authenticatable
*/
public function fetchUserByCredentials(Array $credentials)
{
return new \Exception('Not implemented');
}
/**
* {@inheritDoc}
* @see \Illuminate\Contracts\Auth\Authenticatable::getAuthIdentifierName()
*/
public function getAuthIdentifierName()
{
return 'id';
}
/**
* {@inheritDoc}
* @see \Illuminate\Contracts\Auth\Authenticatable::getAuthIdentifier()
*/
public function getAuthIdentifier()
{
return $this->{$this->getAuthIdentifierName()};
}
/**
* {@inheritDoc}
* @see \Illuminate\Contracts\Auth\Authenticatable::getAuthPassword()
*/
public function getAuthPassword()
{
return $this->password;
}
/**
* {@inheritDoc}
* @see \Illuminate\Contracts\Auth\Authenticatable::getRememberToken()
*/
public function getRememberToken()
{
return $this->{$this->getRememberTokenName()};
}
/**
* {@inheritDoc}
* @see \Illuminate\Contracts\Auth\Authenticatable::setRememberToken()
*/
public function setRememberToken($value)
{
$this->{$this->getRememberTokenName()} = $value;
}
/**
* {@inheritDoc}
* @see \Illuminate\Contracts\Auth\Authenticatable::getRememberTokenName()
*/
public function getRememberTokenName()
{
return $this->rememberTokenIdentifierName;
}
}
Kernel
routeに新しいMiddlewareを追加する。
app/Http/Kernel.php
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'auth.bearer' => \App\Http\Middleware\AuthenticateViaApi::class, // この行を追加して読み込ませる
];
Exceptions
JSONでリクエストを受けたときに、JSONでエラーを返せるように修正する。
app/Exceptions/Handler.php
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $exception
* @return \Illuminate\Http\Response
*/
public function render($request, Exception $exception)
{
$is_api = $request->is('api/*') || $request->expectsJson();
if ($this->isHttpException($exception)) {
if ($status_code = $exception->getStatusCode()) {
$message = "";
switch ($status_code) {
case 401:
$message = 'Unauthorized';
break;
case 403:
$message = 'Permission Denied';
break;
case 404:
$message = 'Not Found';
break;
case 410:
$message = 'Gone';
break;
default:
$message = 'Error Message Undefined';
}
return $is_api ?
response()->json(['status' => $status_code, 'error' => $message], $status_code) :
response($message, $status_code);
}
}
return parent::render($request, $exception);
}
使い方
このトークン認証を使うものは、別のところにまとめたかったので、app/Http/Controllers/Api 配下に配置。
app/Http/Controllers/Api/TestController.php
<?php
namespace App\Http\Controllers\Api;
use Log;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use Validator;
class TestController extends Controller
{
use AuthenticatesUsers;
public function test(Request $request)
{
$user = Auth::user();
return response()->json(['status' => 'successful']);
}
}
最後に
たぶん抜けてはいないと思いますが・・・不要だったはずの処理やコメントを削ったので、足りなかったらご指摘ください。
変更履歴
-
2018/12/10
ソースコードが長くて見づらかったので、<details></details>を使って、初期状態で非表示になるように変更