それでは、Emailとパスワードを入力したら、Loginできるように実装したいと思います。
環境設定ファイルの作成
config.dartの作成
環境設定の情報等を保存するconfigファイルをlibの直下に作成し、下記のように記述します。
class Config {
static const domain = 'https://your.domain';
static const baseUrl = '$domain/api';
static const String loginUrl = '$baseUrl/login';
static const String xsrfUrl = '$domain/sanctum/csrf-cookie';
static const String logoPath = 'assets/images/my_logo.png';
}
- 1行目:class Configで定数群をまとめています。
- 2〜5行目:static constを使い、値の変更を防いでいます。
- 5行目:Laravel JetStreamが使用するXSRFトークンを取得するためのエンドポイントです。
/sanctum/csrf-cookie はLaravel Sanctumのルートで、セッションベースの認証時にCSRF保護を提供します。 - 6行目:logoのパスをConfigで定義。こうすることで、Configファイルを書き換えれば、ロゴを簡単に変更できます。
logoのパス変更
Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: SizedBox(
width: 150.0,
height: 150.0,
child: Image.asset(
Config.logoPath,
errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) {
return const FlutterLogo(); // エラー時のフォールバックとしてFlutterロゴを表示
},
),
),
),
もし、logoPathを間違えて記入したり、画像ファイルがない場合は、Flutterロゴが表示されるように修正しています。
.gitignoreに追加
Gitにアップされないように.gitignoreに追加します。
#Config files
config.dart
CSRFトークンについて
CSRFトークンとは
CSRFトークンについて少しお話します。
CSRF(Cross-Site Request Forgery)トークンとは、Webアプリケーションのセキュリティ対策の1つです。
簡単に説明すると、Webサイト間で悪意のあるデータの受け渡しが行われることを防ぐためのトークンのことです。
具体的には、WebフォームがあるWebページを表示したときに、そのページだけが知っている秘密の文字列を生成します。そして、そのページからデータが送信されるときには、その秘密の文字列を一緒に送信するようにします。
サーバー側では、その秘密の文字列が送信されてこないと、そのデータを受け付けません。これによって、他のサイトから勝手にデータを送信されることを防ぐことができます。
つまり、CSRFトークンを使うことで、その Web サイトにログインしているユーザー以外が勝手にデータを送信することを阻止できるようになるのです。
以上が、CSRFトークンのしくみの基本的な説明です。Webアプリケーションのセキュリティ対策として重要な技術なので、しっかり理解しておきましょう。
LaravelにおけるCSRFトークンの扱い方
LaravelにおけるCSRFトークンの扱い方は下記のようになっています。
デフォルトでCSRF保護が有効になっている
Laravelはデフォルトで全てのPOST、PUT、DELETEリクエストに対してCSRF保護を適用します。これにより、CSRFからアプリケーションを保護できます。
トークンはセッションに紐付けて発行される
LaravelはセッションIDを利用して不正なリクエストかどうかをチェックしています。トークンはセッションに紐付けて発行され、リクエスト送信時に正当なユーザーからのリクエストかどうか検証します。
Bladeテンプレートでトークンを簡単に埋め込める
Bladeテンプレートの@csrfディレクティブを使用することで、フォームにCSRFトークンを簡単に埋め込むことができます。
AJAXリクエストの場合はヘッダーにセットする必要がある
AxiosやjQueryなどを利用したAJAXリクエストの場合は、ヘッダーのX-CSRF-TOKENやX-XSRF-TOKENにトークンをセットする必要があります。
このように、LaravelはCSRF対策をしっかり行っており、簡単に実装できるようになっています。
LaravelにおけるCSRFトークンの取得方法
Laravel(Sanctum)でCSRFトークンを取得するには、先程、Configクラスで設定したxsrfUrlにて取得することができます。
そして、xsrfUrlのヘッダー情報は、下記のようになっています。
つまり、ヘッダー情報リストの0番目を取得する必要があるのです。そこで、下記のようなコードで取得しました。
final res = await dio.get(Config.xsrfUrl);
final cookies = res.headers['set-cookie'];
if (cookies != null) {
for (var cookie in cookies) {
if (cookie.contains('XSRF-TOKEN')) {
var xsrfToken = cookie.split(';')[0].split('=')[1];
break;
}
}
}
DBの選択
DBの意義
Flutterアプリでトークン情報をDBに保存する意義は大きく分けて次の3つです。
アクセストークンの状態保持
認証後に取得したアクセストークンやリフレッシュトークンは一定期間有効な状態を保持する必要があります。これをアプリ再起動後も参照できるようDBに保存しておきます。
定期的なトークン更新
トークンには有効期限があるので、期限切れ前にDBから確認してトークンの更新を行います。
セキュリティ向上
平文のトークン情報がアプリ内や転送中に漏洩するリスクを下げるため、暗号化してからDBへ保存します。
以上から、アプリ内で扱う秘匣性の高いトークン情報を安全に保管・更新することがDB保存の主要な意義です。脆弱性低減と状態維持の両面でメリットがあります。
DBの選択
- SQLite
- 軽量で使いやすい組み込みデータベースです。Flutterにデフォルトで含まれているので設定不要で利用できます。
- Hive
- NoSQL風の軽量かつ高速なキーバリューストアです。Flutter向けに最適化されています。
- Firebase Realtime Database
- irebaseが提供するリアルタイムデータベースサービスです。Flutterとの親和性が高く、リアルタイム同期機能が魅力的です。
- Drift
- Drift(旧称:moor)は、FlutterとDartアプリケーション向けのリアクティブな永続化ライブラリです。SQLiteをベースにしており、データベース操作をより簡単で効率的に行うための高レベルのAPIを提供します。
- Isart Database
- Isarは、高速なNoSQLデータベースで、Flutter及びDartに最適化されています。
- Shared Prefrence
- キーバリューのペアを使って軽量なデータを保存。設定情報などの簡単なデータの保存に適している。
- flutter_secure_storage
- キーと値のペアを安全にデバイスに保存することができます。
- iOSでは Keychain を、Androidでは Keystore を利用してデータを暗号化し、安全に保存します。
- 敏感な情報(例えばユーザー認証トークンや個人情報)をアプリ内で安全に保管するのに適しています。
- 簡単なAPIを提供し、データの読み書きが容易です。
上記から、好みのDBを使用してください。
Hiveデータベースのインストール
Isarは、Hiveの後継パッケージなので、採用しようと思ったのですが、2023年12月現在、まだWeb版に対応していないので、今回はHiveを採用します。
インストール方法は、「Hiveデータベースのインストール」を参照してください。
モデルの作成
Authモデル及びUserモデルの作成
認証後のTokenデータ保存するためのAuthモデルや、ユーザー基本情報を保存するUserモデルを作成します。freezedとHiveで作成します。
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive/hive.dart';
part 'auth.freezed.dart';
part 'auth.g.dart';
@freezed
abstract class Auth with _$Auth {
@HiveType(typeId: 0)
const factory Auth({
@HiveField(0) String? token,
@HiveField(1) String? xsrfToken,
@HiveField(2) User? user,
@HiveField(3) @Default(false) bool isLogin,
}) = _Auth;
factory Auth.fromJson(Map<String, dynamic> json) => _$AuthFromJson(json);
}
@freezed
abstract class User with _$User {
@HiveType(typeId: 1)
const factory User({
@HiveField(0) int? id,
@HiveField(1) String? uuid,
@HiveField(2) String? name,
@HiveField(3) String? yomi,
@HiveField(4) String? tel,
@HiveField(5) String? email,
@HiveField(6) String? photo,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
そしてbuild_runnerを実行します。
flutter pub run build_runner build --delete-conflicting-outputs
Authモデル及びUserモデルの解説
こちらのコードはFlutterの開発で使用されるfreezedとhiveパッケージを使用して、データモデルを定義しています。具体的には、認証情報(Auth)とユーザー情報(User)の2つのクラスが定義されています。それぞれのクラスはfreezedアノテーションを使用して不変(immutable)なデータ構造を提供し、hiveアノテーションを使用してローカルデータベースに保存するための構造を定義しています。
Auth クラス
- @freezed: freezedアノテーションは、不変の値を持つクラスを生成するために使用されます。これにより、データが変更された場合に新しいインスタンスが生成されることを保証します。
- @HiveType(typeId: 0): HiveはFlutterの軽量なキーバリューストアライブラリで、HiveTypeアノテーションはHiveで使用されるクラスを識別するために使用されます。typeIdはこのクラスの識別子です。
- const factory Auth(…): Authクラスのコンストラクタです。const factoryを使用することで、同じ値のインスタンスが複数回生成されないようにします。
- @HiveField(x): 各フィールドHiveFieldアノテーションを付けることで、Hiveがどのフィールドをどのように保存するかを指定します。 x はフィールドの識別子です。
- factory Auth.fromJson(…): JSONからAuthオブジェクトを生成するファクトリメソッドです。これはデータをネットワークから取得する際などに便利です。
User クラス
- UserクラスもAuthクラスと同様にfreezedとHiveTypeアノテーションを使用して定義されています。
- 各フィールドには異なるHiveField識別子が割り当てられており、これによりHiveがデータを適切に保存できるようになります。
- factory User.fromJson(…): これもJSONからUserオブジェクトを生成するためのメソッドです。
このコードは、Flutterアプリケーションでユーザーの認証状態と個人情報を管理するための強力なベースを提供します。freezedを使用することでコードのボイラープレートを減らし、hiveを使用することでデータを効率的にローカルに保存できます。
プロバイダーの作成
authProviderの作成
それでは、上記のAuthモデルを管理するためのauthProviderを作成し、LoginやLogioutに関するメソッドも記述します。
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../config.dart';
import '../models/auth.dart';
// Dioインスタンスは別のクラスで管理する
final dioProvider = Provider<Dio>((ref) => Dio());
final authProvider = StateNotifierProvider<AuthNotifier, Auth>((ref) {
final dio = ref.watch(dioProvider);
return AuthNotifier(dio);
});
class AuthNotifier extends StateNotifier<Auth> {
final Dio dio;
final Box<Auth> _box;
AuthNotifier(this.dio) : _box = Hive.box<Auth>('auth'), super(const Auth()) {
initialize();
}
Future<void> initialize() async {
try {
final storedAuth = _box.get('auth');
if (storedAuth?.token != null) {
state = storedAuth!;
}
} catch (e) {
if (kDebugMode) {
print('認証情報の初期化エラー: $e');
}
}
}
Future<void> login(String email, String password) async {
try {
if (!kIsWeb) {
await _setCsrfToken();
}
const url = Config.loginUrl;
var res = await dio.post(url, data: {'email': email, 'password': password});
String? token = res.data['token'];
_updateAuthState(state.copyWith(token: token, isLogin: true));
} catch (e) {
if (kDebugMode) {
print('ログインエラー: $e');
}
rethrow;
}
}
Future<void> _setCsrfToken() async {
try {
final res = await dio.get(Config.xsrfUrl);
final cookies = res.headers['set-cookie'];
if (cookies != null) {
for (var cookie in cookies) {
if (cookie.contains('XSRF-TOKEN')) {
var xsrfToken = cookie.split(';')[0].split('=')[1];
_updateAuthState(state.copyWith(xsrfToken: xsrfToken));
break;
}
}
}
} catch (e) {
if (kDebugMode) {
print("CSRFトークン取得エラー: $e");
}
}
}
void _updateAuthState(Auth newState) {
state = newState;
_saveToHive();
}
Future<void> _saveToHive() async {
await _box.put('auth', state);
}
Future<void> logout() async {
await _box.delete('auth');
state = const Auth();
}
}
authProviderの解説
1. Dioインスタンスの管理
- DioインスタンスはProviderを用いてアプリケーション全体で管理されるようにしました。これにより、ネットワークリクエストに関する設定やインターセプターの追加などがアプリケーション全体で一貫して行えるようになります。
- AuthNotifierのコンストラクタで、このDioインスタンスを受け取り、それを使用してHTTPリクエストを行います。
2. 初期化メソッド
- initializeメソッドでは、Hiveのauthボックスから認証情報を取得し、トークンが存在する場合は状態を更新します。これはアプリ起動時に自動的に呼び出され、保存された認証情報をロードするために使われます。
3. ログイン処理
- loginメソッドでは、まずWebプラットフォームでない場合にCSRFトークンを設定するために_setCsrfTokenを呼び出します。
- その後、Dioを使用してログインリクエストを送信し、レスポンスからトークンを取得します。
- 取得したトークンは_updateAuthStateメソッドを使用してstateとHiveに保存します。
4. CSRFトークン設定
- _setCsrfTokenメソッドは、CSRFトークンを取得し、それを現在の状態に反映させます。このメソッドはloginメソッド内から呼び出されます。
5. 状態更新とHiveへの保存の統合
- _updateAuthStateメソッドは、与えられた新しいAuth状態で現在の状態を更新し、その新しい状態をHiveに保存します。これにより、状態の更新と保存を一元的に管理できるようになります。
6. ログアウト処理
- logoutメソッドでは、Hiveから認証情報を削除し、状態をリセットします。
これらの変更により、コードの効率性、保守性、および可読性が向上しています。状態管理とデータ永続化のロジックがより整理され、エラーハンドリングが強化されています。また、Dioインスタンスをアプリケーション全体で共有することで、ネットワークリクエストに関連する設定や処理を一元的に管理できるようになりました。
main関数の修正
アプリ起動時に、Hiveが利用できるようにmain関数を下記のように修正します。
import 'package:hive_flutter/adapters.dart'; //追加
import 'models/auth.dart'; //追加
// ...
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
Hive.registerAdapter(AuthImplAdapter());
Hive.registerAdapter(UserImplAdapter());
// Authボックスを開く
if (!Hive.isBoxOpen('auth')) {
await Hive.openBox<Auth>('auth');
}
// Userボックスを開く
if (!Hive.isBoxOpen('user')) {
await Hive.openBox<User>('user');
}
runApp(const ProviderScope(child: MyApp()));
}
修正したmain関数の解説
上記は、Flutterアプリケーションのmain関数で実行される初期化手順を定義しています。この手順はアプリケーションの起動時に一度だけ実行され、Hiveデータベースのセットアップや、アプリケーションのルートウィジェットの設定を行います。以下は各ステップの詳細な説明です:
1. WidgetsFlutterBindingの初期化
WidgetsFlutterBinding.ensureInitialized();
- これはFlutterアプリケーションの実行前に必要な初期化を行います。特に、バックグラウンドでのデータフェッチやHiveデータベースのようなプラグインを使用する際に、これらのプラグインがFlutterエンジンと正しく連携するために必要です。
2. Hiveの初期化
await Hive.initFlutter();
- Hive.initFlutter()はHiveデータベースの初期化を行います。これにより、アプリケーション内でHiveを使用してデータを永続化できるようになります。
3. アダプターの登録
Hive.registerAdapter(AuthImplAdapter()); Hive.registerAdapter(UserImplAdapter());
- Hiveはデフォルトで基本的なデータタイプ(例えばint、String)のみをサポートしています。カスタムクラス(この場合はAuthとUser)をHiveで保存するためには、それぞれのクラスに対応するアダプターを登録する必要があります。
- これらのアダプターは、カスタムクラスのオブジェクトをHiveが理解できる形式に変換し、またその逆の変換も行います。
4. Hiveボックスの開き方
if (!Hive.isBoxOpen('auth')) { await Hive.openBox<Auth>('auth'); } if (!Hive.isBoxOpen('user')) { await Hive.openBox<User>('user'); }
- これらのコードは、’auth’と’user’という名前のHiveボックス(データベース内のテーブルに相当)を開きます。Hive.isBoxOpenでボックスが既に開かれているかをチェックし、開かれていなければHive.openBoxで開きます。
- このステップにより、アプリケーションはAuthとUserのデータを保存し、読み出す準備が整います。
5. アプリケーションの実行
runApp(const ProviderScope(child: MyApp()));
- 最後に、runApp関数を使用してアプリケーションのルートウィジェット(この場合はMyApp)を設定し、アプリケーションを起動します。
- ProviderScopeはhooks_riverpodパッケージの一部で、アプリケーション全体で状態を管理するために使用されます。これにより、異なるウィジェット間で状態を共有しやすくなります。
これらの手順により、Flutterアプリケーションは適切に初期化され、データの永続化、状態管理、UIの表示が可能になります。
コメント