LoginSignup
7
12

More than 1 year has passed since last update.

Laravel7 + Sanctum でSPAログイン認証機能を実装

Last updated at Posted at 2021-03-24

2021/08/03 一部文言修正と追記しました。ソースに変更はありません ★フロント側からcsrf-cookieへのHTTPリクエストはGETメソッドで行います ★VueCLIを使用せずにLaravel側にVueをインストールして同一プロジェクトパターンにも使用できます ★401 unauthorizedが発生した際の対処方法いくつか紹介

まえおき

Laravel7 + VueCLI、Laravel7にVueインストールなどのパターンで使用できる認証です。
今回のSanctum SPA認証はCookieを使用した認証となっており
モバイルアプリの場合はAPIトークンを使った認証となっており今回の記事では対象外です

プロジェクト作成

Laravelインストール

composer create-project --prefer-dist laravel/laravel blog "7.*"

7.*で指定しないと最新の8で作成されてしまうので注意

Sanctum導入&設定

Sanctumインストール

composer require laravel/sanctum

設定ファイルとマイグレーションファイルをpublish

php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

config/sanctum.phpとdatabase/migrationsにファイルが生成されます

CSRF保護のルーティングプレフィックス変更

config/sanctum.php
<?php

return [

    'stateful' => explode(',', env(
        'SANCTUM_STATEFUL_DOMAINS',
        'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1'
    )),

    'expiration' => null,

    'middleware' => [
        'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
        'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
    ],

    'prefix' => 'api', // 追加
];

Sanctumではログイン時にCSRFクッキーを保存する必要があります
その際のルーティングのデフォルトは「http://localhost:8000/sanctum/csrf-cookie」となり
これを「http://localhost:8000/api/csrf-cookie」に変更しています
必要なければしなくても良いですが
axiosを使用したフロントでBASE_URLを設定する際に他のAPIと同じ様に
http://localhost:8000/apiをBASE_URLに設定できるので便利です

マイグレーション実行

php artisan migrate

apiミドルウェアにSanctumのミドルウェアを追加

Kernel.php
<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful; // 追加
class Kernel extends HttpKernel
{
    protected $middleware = [
        // \App\Http\Middleware\TrustHosts::class,
        \App\Http\Middleware\TrustProxies::class,
        \Fruitcake\Cors\HandleCors::class,
        \App\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    ];

    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            EnsureFrontendRequestsAreStateful::class,
            'throttle:60,1',
            \Illuminate\Routing\Middleware\SubstituteBindings::class, // 追加
        ],
    ];

    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,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
    ];
}

CORSホワイトリストとセッションドライバー設定

.env
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost

LOG_CHANNEL=stack

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=

BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=cookie // 変更
SESSION_LIFETIME=120
SESSION_DOMAIN=localhost // 追加
SANCTUM_STATEFUL_DOMAINS=localhost:8080 // 追加

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1

MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

SANCTUM_STATEFUL_DOMAINSとSESSION_DOMAINは本番環境にデプロイする際は変更する必要があります。
Laravel側にVueインストールして使用している場合はSANCTUM_STATEFUL_DOMAINSはphp artisan serveで動かしたポートに合わせて設定してください。(php artisan serveの場合はlocalhost:8000)
VueCLIのnpm run serveでフロント動かしている場合はソースのままでlocalhost:8080

Cookieの送受信を許可

config/cors.php
<?php

return [

    'paths' => ['api/*'],

    'allowed_methods' => ['*'],

    'allowed_origins' => ['*'],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => true, // 変更

];


クライアント側も許可しないといけないので注意ください
axiosを使用している場合はヘッダーにwithCredentials: trueを設定します

認証API作成

コントローラー作成

php artisan make:controller AuthController
AuthController
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Facades\Auth;
use App\User;

class AuthController extends Controller
{
    public function login(Request $request)
    {
        $request->validate([
            'email' => ['required'],
            'password' => ['required'],
        ]);

        $credentials = $request->only('email', 'password');

        if (Auth::attempt($credentials)) {
            return response()->json(['name' => Auth::user()->name], 200);
        }

        throw ValidationException::withMessages([
            'email' => ['ログインに失敗しました'],
        ]);
    }

    public function register(Request $request)
    {
        $request->validate([
            'name' => ['required', 'between:1,10', 'unique:users'],
            'email' => ['required', 'email', 'unique:users'],
            'password' => ['required', 'between:6,30', 'confirmed'],
        ]);

        User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => bcrypt($request->password)
        ]);
    }

    public function logout(Request $request)
    {
        Auth::logout();
    }
}

ダッシュボード作成(認証保護ルート)

ダッシュボードはログインしていないとアクセスさせないようにします
ログインしていない状態でアクセスして来た場合は401を返します
上記の様にルートを保護するのは後のルーティングの設定で行います

コントローラー作成

php artisan make:controller DashboardController
DashboardController
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class DashboardController extends Controller
{
    public function index(Request $request)
    {
        return response()->json(['user' => Auth::user()], 200);
    }
}

シンプルにログインしているユーザー情報を返しています
名前だけ返すにはAuth::user()->nameとします

ルーティング設定

api.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::post('/login', 'AuthController@login');
Route::post('/register', 'AuthController@register');
Route::post('/logout', 'AuthController@logout');

Route::group(['middleware' => ['auth:sanctum']], function () {
    Route::post('/dashboard', 'DashboardController@index');
});

auth:sanctumミドルウェアでルートを保護します

おまけ

CSRF-COOKIE初期化

ログインを行う際にCSRFの初期化を行わなければなりません
この記事で紹介している通りに設定したのであれば
「/api/csrf-cookie」にパラメーター無しでGETリクエストする必要があります
認証保護されたAPIと通信を行う度に毎回GETリクエストするわけではなく
あくまでもログイン時の1回のみとなり以降は自動でCookie内のCSRFTOKENが更新されます

ログイン失敗時にValidationExceptionを使用している理由

他のValidationエラーと同じ様にレスポンスを返すことができるので
フロントで422エラーハンドリングをする際に特別な処理を書かなくて済むためです

ログイン失敗

throw ValidationException::withMessages([
     'email' => ['invalid credentials'],
]);

バリデーションエラー

    $request->validate([
        'email' => ['required'],
        'password' => ['required'],
    ]);

上記2パターン共に別々に処理を書かずに1行でエラー取得が可能
フロントで上記エラーを取得

try{
    await axios.post('/login', { email: email, password: password })
}catch(error){
    this.errors = error.response.data.errors // エラー取得
}

正確に422エラーをキャッチするにはステータスコードで条件分岐させるのが良いです

401 Unauthorized

auth:sanctumで認証保護されたルートにログイン後に発生した場合下記を確認しましょう。
ログイン(login)、会員登録(register)はログインしていない状態のユーザーが使用する機能なので、
sanctumで認証保護する必要はありません。

①.envファイルのSESSION_DOMAINとSANCTUM_STATEFUL_DOMAINSが間違いないか
②.envファイルが反映されていない 解決法(php artisan config:cache)
③Kernel.phpのapi部分が下記のようになっているか

Kernel.php
 'api' => [
            EnsureFrontendRequestsAreStateful::class,
            'throttle:60,1',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

④php artisan serve停止して再度起動してみる
⑤フロント側のhttpクライアント(axios)にwithcredentials: trueが設定されていない
⑥php artisan route:listでルーティングを確認し叩くAPIのURLに間違いはないか

7
12
1

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
7
12