🙄

初めてFlutterを触ってみた- Flutter × Laravel 認証- ③

2024/10/16に公開

初めてFlutterを触ってみた③

過去の記事の続きになります。

この記事では、Laravelとの連携方法に焦点を当て、FlutterアプリからAPIを介してトークンベースの認証を実装する方法を解説します。

Laravel Sanctumについて

APIトークンとは

Laravel Sanctumは、シンプルなトークンベースの認証システムを提供します。トークンを用いることで、クライアント(Flutterなど)がサーバー(Laravel)と安全にやり取りすることができます。

トークンは、ユーザーがログインするときに生成され、各リクエストでそのトークンを使ってユーザーを認証します。これにより、ユーザーがすでにログインしているかのように動作することができます。APIトークン認証は、SPA(シングルページアプリケーション)やモバイルアプリとの連携に適しています。

ルーティングの設定

api.phpディレクトリを作成して、APIルートを定義していきます。

$ php artisan install:api
routes/api.php
// 認証なしルート
Route::post('/register', [RegisterController::class, 'store']);
Route::post('/login', [AuthController::class, 'store']);

// 認証済みルート
Route::group(['middleware' => 'auth:sanctum'], function () {
    Route::post('/logout', [AuthController::class, 'destroy']);
    Route::get('/user', function (Request $request) {
        return $request->user();
    });
});
  • 認証なしルート: これらのエンドポイントは、認証されていないユーザーでもアクセスできます。たとえば、新規ユーザー登録やログイン用のエンドポイントです。
  • 認証済みルート auth:sanctumミドルウェアを適用することで、トークン認証を行います。このルートには、ログイン後のユーザーのみがアクセスできます。認証に失敗した場合、401 Unauthorized エラーが返されます。

トークンの作成と削除

Sanctumを使ってトークンの作成や削除を行う際の具体例を示します。

トークンの作成

ユーザーがログインした際にトークンを発行します。

$token = $user->createToken('MyAppToken')->plainTextToken;
return response()->json(['token' => $token]);

トークンの削除

ユーザーがログアウトする際には、トークンを削除します。

$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Logged out successfully']);

上記より、今回は以下のようなAuthControllerを作成しました。

AuthController.php
<?php

namespace App\Http\Controllers\Api\Auth;

use App\Enums\Status;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Auth\LoginRequest;
use App\Models\User;
use App\Utils\TwoFactorAuth;
use Illuminate\Http\Request;

class AuthController extends Controller
{
    public function store(LoginRequest $request)
    {
        $request->authenticate();

        $user = User::where('email', $request->email)
            ->where('status', Status::ACTIVE)    
            ->first();

        if (!$user) {
            return response()->json(['message' => '該当ユーザーが見つかりませんでした。'], 404);
        }
        
        $token = $request->user()->createToken('auth_token:user'. $user->id)->plainTextToken;

        TwoFactorAuth::sendCode($user);

        return response()->json(['token' => $token]);
    }

    public function destory(Request $request)
    {
        $request->user()->tokens()->delete();
        return response()->json(null, 200);
    }
}

storeメソッドの流れ

  1. 認証処理の実行
    $request->authenticate()メソッドで、メールアドレスとパスワードによるユーザー認証を行います。
  2. ユーザーのステータス確認
    Userモデルを使用して、指定したメールアドレスとStatus::ACTIVEのステータスを持つユーザーが存在するかをチェックします。該当しない場合は、404エラーを返します。
  3. トークンの発行
    ログインに成功した場合、ユーザーIDを含むユニークなトークンを生成します。plainTextTokenを使用して、クライアントにトークンを返します。
  4. 二段階認証コードの送信
    TwoFactorAuth::sendCode()メソッドを使って、ユーザーに二段階認証コードを送信します。

destoryメソッドの流れ

  1. トークンの削除
    tokens()->delete()メソッドで、ユーザーが持つすべてのトークンを削除します。これにより、ログアウト後は再認証が必要になります。

Flutter側のAPI接続について

Flutterを使用してLaravelのAPIと通信する際には、httpパッケージを使ってAPIリクエストを行います。Laravel Sanctumを使って発行されるトークンを利用し、認証付きリクエストを送る方法について説明します。また、トークンの管理にはshared_preferencesパッケージを使用します。

Laravelから受け取るトークンの役割

FlutterアプリとLaravelサーバー間で安全にデータをやり取りするため、Laravel側で発行されたトークンを使って各リクエストを認証します。以下がその役割です:

  • トークンの生成
    ユーザーがログインした際に、サーバー側でトークンが発行され、クライアントに返されます。このトークンはFlutterアプリがリクエストを送信する際に使用し、ユーザーの認証を行います。

  • トークンの使用
    クライアントは各リクエストにトークンを含めることで、サーバーに自分が認証済みのユーザーであることを証明します。サーバー側は受け取ったトークンを検証し、正しいトークンであればリクエストを許可します。

  • トークンの管理
    トークンはクライアント側で安全に管理する必要があります。ログイン後に取得したトークンをローカルストレージ(例:SharedPreferences)に保存し、APIリクエスト時に利用します。ログアウト時にはトークンを削除して無効化します。

FlutterでのServiceUtilsについて

以下のServiceUtilsクラスでは、トークンの管理とAPIリクエストの共通処理をまとめています。

utils/service_utils.dart
import 'package:flutter_sample/src/constants/app_const.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';

class ServiceUtils {
  
  static final String baseApiUrl = AppConst.baseApiUrl;

  // リクエストヘッダーを取得
  static Map<String, String> getHeaders(String? token) {
    final header =  token != null
      ? {
          'Authorization': 'Bearer $token',
          'Content-Type': 'application/json',
          'Accept': 'application/json',
      } : {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
      };
    return header;
  }

  // POSTリクエスト
  static postRequest(String uri, String? token, String? data) async {
    final url = Uri.parse('$baseApiUrl/$uri');
    final headers = getHeaders(token);
    final response = await http.post(
      url,
      headers: headers,
      body: data,
    );
    return response;
  }

  ...

  // Tokenを取得
  static Future<String?> getToken() async {
    final prefs = await SharedPreferences.getInstance();
    final token = prefs.getString('auth_token');
    return token;
  }

  // Tokenを保存
  static Future<void> setToken(String token) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('auth_token', token);
  }

  // Tokenを削除
  static Future<void> removeToken() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove('auth_token');
  }

}
  • getHeadersメソッド
    APIリクエストに必要なヘッダーを生成します。トークンが指定されている場合は、Authorizationヘッダーを追加して認証付きリクエストを行います。認証が不要なリクエストの場合、Content-TypeとAcceptのヘッダーのみが設定されます。
  • postRequestメソッド
    サーバーに対してPOSTリクエストを送信します。uriにはAPIのエンドポイントを指定し、tokenには認証用トークンを渡します。dataにはリクエストのボディに含めるデータ(JSON形式)を指定します。HTTPレスポンスを返すので、レスポンスの内容をチェックして次の処理を行います。
  • getTokenメソッド
    SharedPreferencesを使用してローカルストレージに保存されているトークンを取得します。ログイン済みのユーザーの認証情報を取得するために利用されます。
  • setTokenメソッド
    ログイン時などにサーバーから受け取ったトークンをローカルストレージに保存します。これにより、後続のリクエストでトークンを使用して認証できます。
  • removeTokenメソッド
    ログアウト時にトークンをローカルストレージから削除します。これにより、セキュリティを確保し、再度ログインしない限り認証ができない状態にします。

Flutterでのログインリクエスト

Flutterアプリでログインを行い、サーバーからトークンを受け取る方法を示します。ログインに成功すると、サーバーから受け取ったトークンをローカルに保存します。

services/auth_service.dart
import 'dart:convert';
import 'package:auth_demo/src/const.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';

class AuthService {
  ...
  // ログインリクエスト
  Future<void> login(String email, String password) async {
    final requestData = jsonEncode({
      'email': email,
      'password': password,
    });

    final response = await ServiceUtils.postRequest('login', null, requestData);
    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      final token = data['token'];
      // トークンを保存
      await ServiceUtils.setToken(token);
    } else {
      final message = jsonDecode(response.body)['message'] ?? 'ログインに失敗しました';
      throw ApiException(message);
    }
  }
  ...

処理の流れ

  1. ログインデータの準備
    emailとpasswordをJSON形式にエンコードし、requestDataとして準備します。このデータがログインリクエストのボディに含まれ、サーバーに送信されます。
  2. APIリクエストの送信
    ServiceUtils.postRequestメソッドを使用して、loginエンドポイントにPOSTリクエストを送信します。リクエストにはユーザーのログイン情報が含まれ、サーバー側で認証処理が行われます。
  3. レスポンスの処理
    サーバーからのレスポンスを確認し、ステータスコードが200(成功)である場合は、レスポンスボディをデコードして取得します。これにより、サーバーが返したトークンを取得できます。
  4. トークンの保存
    サーバーから受け取ったトークンをServiceUtils.setTokenメソッドを使ってローカルストレージ(SharedPreferences)に保存します。これにより、以降の認証付きリクエストでトークンを利用できるようになります。
  5. エラーハンドリング
    ステータスコードが200以外の場合は、レスポンスからエラーメッセージを取得し、ApiExceptionをスローしてログイン失敗を通知します。エラーメッセージがない場合は、“ログインに失敗しました”というデフォルトメッセージを表示します。

ログアウトリクエスト

Flutterアプリでログアウトを行う際の処理を示します。サーバーにログアウトリクエストを送信し、成功した場合にはローカルに保存されたトークンを削除します。

services/auth_service.dart
  // ログアウトリクエスト
  Future<void> logout() async {
    final token = await ServiceUtils.getToken(); 
    
    if (token != null) {
      final response = await ServiceUtils.postRequest('logout', token, null);
      if (response.statusCode != 200) {
        final message = jsonDecode(response.body)['message'] ?? 'ログアウトに失敗しました';
        throw ApiException(message);
      }
      // トークンを削除
      await ServiceUtils.removeToken();
    }
  }

処理の流れ

  1. トークンの取得
    ServierUtils.getTokenメソッドを使用して、ローカルストレージに保存されているトークンを取得します。ログアウトリクエストには認証済みのトークンが必要であり、このトークンを使用してユーザーの認証状態をサーバー側で解除します。
  2. トークンの有無を確認
    取得したトークンがnullでない場合にのみ、ログアウトリクエストを実行します。トークンがnullの場合、ログインしていない、もしくはすでにログアウト済みであるため、何も処理を行いません。
  3. ログアウトリクエストの送信
    ServiceUtils.postRequestメソッドを使ってlogoutエンドポイントにPOSTリクエストを送信します。このリクエストには、取得したトークンが含まれており、サーバーはトークンを使ってログアウト処理を実行します。
  4. レスポンスの確認
    サーバーからのレスポンスを確認し、ステータスコードが200(成功)でない場合はエラーメッセージを取得して例外をスローします。エラーメッセージがない場合は、“ログアウトに失敗しました”というデフォルトメッセージを表示します。
  5. トークンの削除
    サーバーからログアウトが成功したレスポンスが返ってきた場合、ローカルストレージに保存されているトークンをServiceUtils.removeTokenメソッドで削除します。これにより、ローカルに認証情報が残らないようにします。

まとめ

今回は、Laravel Sanctumを使ったトークンベースの認証と、Flutterを利用したAPI接続の実装について解説しました。
次回は、Flutterの画面遷移や、これまで触れてこなかったFlutterライブラリについて、ゆるくお話しする予定です。
具体的なトピックはまだ決まっていませんが、気軽に読んでもらえたら嬉しいです!
Flutterに詳しい方や初心者の方からのアドバイスも大歓迎ですので、ぜひコメントいただけると嬉しいです!

Discussion