Laravel+vueでなんか作る(Laravel基礎編③~承認・認証~)


Udemyの講座Vue 3 and Laravel: A Practical Guide with Dockerの備忘録。
インストラクターの英語には若干耳慣れない訛りがあるけど、それを言いだしたらアメリカ英語やイギリス英語も「そういう訛り」なわけで、いろんな英語を聴くのもリスニング学習よな~~と思いつつ聴いている

今回はLogin、User、Logoutなどの認証と承認に関わるメソッドを作るよ!

Laravel Sanctum

こう……フロントエンドのSPAからの……なんかクッキーとかJWTトークンとか、ログインとかログアウトとか、ああいうのを可能にするツールらしい。知らんけど。
なお後述するが、「ああいうの」を専門用語で「認証(Authentication)」と「承認(Authorization)」と言う。
ちなみに「Sanctum」とはラテン語で「聖所」の意味らしい。元中二病患者としてはこういうのゾクゾクしますな。

―――――というわけで †Sanctumをインストール† します――――

  • composer require laravel/sanctumする
  • php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"する
  • Dockerのバックエンドに入り、php artisan migrateする。
  • UserクラスでHasApiTokensUseする。
User
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasFactory, Notifiable, HasApiTokens;
    protected $guarded = [];
    protected $hidden = ["password"];
}

認証 ( Authentication )

認証 ( Authentication )というのは、アクセスしてきたユーザーが
本人か確認する手続きのこと。

Login

コントローラ内でLoginメソッドを作る

AuthController
public function login(Request $req){
        if(!Auth::attempt($req->only("email","password"))){
            return response([
                "error" => "invalid credentials"
            ],Response::HTTP_UNAUTHORIZED);
        }
        $user = Auth::user();
        $token = $user->createToken("token")->plainTextToken;
        return response([
            "jwt"=>$token
        ]);
    }

急に知らないクラスやそのプロパティがうじゃうじゃ出てきてビックリしたが、なんとなく各自役割は推察できないでもない。
たぶんAuthクラスと言うのが認証に関わるクラスで、これのattempt()というクラスを使うと認証ができるんだろう。デフォルトではemailとpasswordを使うっぽい。ここには一家言ないのでデフォルトの処理に従うことにする。
attempt()に成功するとAuth::user()でユーザーが取り出せるのだと思われる。まったく便利な機能だぜ……。

承認 ( Authorization )

認証に成功したユーザーに、各メソッドにアクセスする権限を行うことを「承認」と言うらしい。
これはRoutingのレイヤーで行うようだ。

routes\api
Route::middleware('auth:sanctum')->group(
    function () {
        Route::get("user",[AuthController::class,"user"]);
});

group()内の関数に追記したルートが、tokenを持っているとアクセス可能になるっぽい。

AuthController
    public function user(Request $req){
        return $req->user();
    }

そしてgroup()内のメソッドでは、$req->user();でアクセス中のユーザーが取り出せるっぽい。マジか?! Sanctumすげぇな……。

Cookie

しかし現在の認証方法にはそもそも問題がある。
どうも認証に関わるコードをフロントエンドにストアするのはセキュリティ上よろしくないそうだ。耳にタコが出来るほど聞いた割にはいまいち原理が理解出来ていない「jwtトークンのセキュリティホール」問題だ。
これをクリアするため、トークンをクッキーにストアする。なんでトークンにストアするとセキュリティ上の問題点がクリアされるのかは個人的には謎のままである。

AuthController
public function login(Request $req){
        if(!Auth::attempt($req->only("email","password"))){
            return response([
                "error" => "invalid credentials"
            ],Response::HTTP_UNAUTHORIZED);
        }
        $user = Auth::user();
        $jwt = $user->createToken("token")->plainTextToken;
//トークンの変数名を「jwt」に変えたよ
        $cookie = cookie(
            "jwt",$jwt, 60 * 24
        );
//cookie関数の引数は
//「トークン名、トークンにする値、トークンの寿命(分単位)」だよ
        return response([
            "jwt"=>$token,
            "user"=>$user
        ])->withCookie($cookie);
//withCookieでクッキーを添付できるよ
    }

なんとLaravelにはクッキー作成用の関数がアウトオブザボックスで付いてるらしい。マジか……。

さらにHttp/Middleware/Authenticate.phpに以下を追記する。

public function handle($req, Closure $next, ...$guards)
    {
        if($jwt = $req->cookie("jwt")){
            $req->headers->set(
                "Authorization",
                "Bearer ".$jwt
            );
        }
        $this->authenticate($req, $guards);

        return $next($req);
    }

どうやらcookieから拾ってきた「jwt」を$reqのヘッダにくっつけて、next()の処理に回してるっぽい。なるほどね。

  • 今までの処理 → jwtトークンをフロントエンドからの通信のヘッダにくっつけて直でコントローラに入れる
  • 現在の処理 → トークンはcookieにくっつけ、ミドルウェアを通過する際リクエストのヘッダに追加し、コントローラに入れる

というわけか。ここら辺の処理はミドルウェアを積み重ねていくexpress.jsとかに感覚が似てる気がする(などと適当なことを言う)。

なお追記したhundle()関数は、Vendor\Framework\Src\Illuminate\Auth\Middleware\Authenticate内の同名の関数を雛形にしてるよ

cors

SPAとAPIを通信させる際に、承認関係で問題になるのがcors。
PostmanからはログインできるけどSPAのフロントエンドからはなぜかログインできません! みたいなときは、大抵corsが問題になっている。

Laravelではconfig/corsを一行いじれば一応SPAからのログインはできるっぽい。
でもherokuとかawsにデプロイしたとき絶対何か不具合が起きそうだよな……。

config/cors
    'paths' => ['api/*', 'sanctum/csrf-cookie'],

    'allowed_methods' => ['*'],
    'allowed_origins' => ['*'],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => true,
//ここをfalseからtrueにしてる

];

2021年7月22日追記
config/corssupports_credentialsをtrueにする上記の方法だけでは、フロントエンドにcookieを渡すことが出来なくなってしまいました。一体なぜ!?
Chromeで通信を調べたところ、Set-Cookieが弾かれていたようです。

という訳で、セッションの設定を変更します。

config/session
//前略
    'same_site' => 'none',

さらに、.envに以下の設定を入れる。

SESSION_SECURE_COOKIE=true

これでcookieをセットできるようになりました!

Logout

AuthController
use Illuminate\Support\Facades\Cookie;

// ~ 中 略 ~
public function logout(){
        $cookie = Cookie::forget("jwt");
        return response([
            "message" => "success"
        ])->withCookie($cookie);
    }

ようは「即時期限切れのクッキーをwithCookie()で添付する」という処理らしい。フーン。

これでログインとログアウトができるようになりました。