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保護のルーティングプレフィックス変更
<?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のミドルウェアを追加
<?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ホワイトリストとセッションドライバー設定
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の送受信を許可
<?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
<?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
<?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とします
ルーティング設定
<?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部分が下記のようになっているか
'api' => [
EnsureFrontendRequestsAreStateful::class,
'throttle:60,1',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
④php artisan serve停止して再度起動してみる
⑤フロント側のhttpクライアント(axios)にwithcredentials: trueが設定されていない
⑥php artisan route:listでルーティングを確認し叩くAPIのURLに間違いはないか