12
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Laravelで独自認証を利用する

Last updated at Posted at 2018-12-10

Laravelで独自認証ロジックを追加する

Laravelで独自認証ロジックを追加する方法を探してみたところ、バージョンの古いものは見つかるのだけど、新しいもの(2018/12現在は、Laravel 5.7)にそのまま入れてもうまくいかなかったので、こうやったらうまくいったというのをメモ。

何をしたかったか

APIを実装するにあたって、認証をBearer Tokenを使って、認証後はLaravelの標準処理に任せたかった。

やろうと試みたこと(実装にあたって試みたこと)

  • config/auth.php のguards、providersに適当なクラスを入れ込んで確認
    どうやってもセッションで認証をしようとする。

  • app/Providers/AuthServiceProvider.php
    追加はなんとなく出来たが、カスタマイズがしにくく、さらにroutes/api.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>を使って、初期状態で非表示になるように変更

12
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?