FlutterでJWT認証を行う
自作したログインを使ってみる
FlutterでFirebaseとSupabaseでしかログインはやったことなくて、そろそろ自作した認証機能でやってみたいな〜と思い、Node.jsを使ってJWT認証やってみました。
これがサンプル
JWTのAPIはこれで作る📦まずは必要なパッケージを追加
- 後で、riverpodで状態管理するページがあるので入れておく
2. APIと通信するパッケージを追加する
3. トークンの保存にこれが必要
ログインページを作る
HTTP通信を行なって、status code200で、アクセストークンが取得できていれば、HomePageへログインすることができます。
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:jwt_auth/home_page.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
// ignore: library_private_types_in_public_api
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
TextEditingController nameController = TextEditingController();
TextEditingController passwordController = TextEditingController();
// ログインをするメソッド
Future<void> login() async {
// ログインAPIにリクエストを送る
final response = await http.post(
Uri.parse('http://localhost:3001/login'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'username': nameController.text,
'password': passwordController.text,
}),
);
// レスポンスを確認する
if (response.statusCode == 200) {
final Map<String, dynamic> responseData = json.decode(response.body);
print("Response data: $responseData");
// accessTokenがあればログイン成功
if (responseData['accessToken'] != null) { // ここを'accessToken'に変更
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('token', responseData['accessToken'] as String); // ここも'accessToken'に変更
// ignore: use_build_context_synchronously
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const HomePage()),
);
print('Login Success');
// ログイン失敗
} else if (responseData['message'] != null) {
// ignore: use_build_context_synchronously
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Error'),
content: Text(responseData['message'] as String),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
),
],
),
);
} else {
print('Login Error: Unexpected response'); // エラーメッセージを追加
}
} else {
print('HTTP Error with code: ${response.statusCode}');
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Login')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: nameController,
decoration: InputDecoration(labelText: 'Name'),
),
TextField(
controller: passwordController,
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
),
ElevatedButton(
onPressed: login,
child: Text('Login'),
),
],
),
),
);
}
}
ログイン後のページ
こちらがログインできたら表示されるページです。ログアウトするときは、アクセストークンを削除して画面遷移をします。
import 'package:flutter/material.dart';
import 'package:jwt_auth/login_page.dart';
import 'package:shared_preferences/shared_preferences.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
// ログアウトをするメソッド
Future<void> logout(BuildContext context) async {
// accessTokenを削除して、ログイン画面に戻る
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove('token');
// ignore: use_build_context_synchronously
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const LoginPage(),
),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home Page'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () => logout(context),
),
],
),
body: const Center(
child: Text('You are logged in!'),
),
);
}
}
ログイン状態を維持する
トークンを保持していれば、ログイン後のページへ画面遷移させるロジックです。後でriverpodでリファクタリングします。
import 'package:flutter/material.dart';
import 'package:jwt_auth/home_page.dart';
import 'package:jwt_auth/login_page.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// [StatefulWidget]を使う場合
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// accessTokenを取得する
SharedPreferences prefs = await SharedPreferences.getInstance();
var token = prefs.getString('token');
runApp(MyApp(token: token));
}
class MyApp extends StatelessWidget {
final String? token;// main()で取得したtokenを受け取る
MyApp({Key? key, this.token}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: token == null ? const LoginPage() : const HomePage(),// tokenがあればHomePage、なければLoginPageを表示
);
}
}
riverpodを使用したコードはこちらです。FutureProviderで非同期処理を行なって、shared_preferencesに保存しているアクセストークンを取得して、ログインしているかいないか判定して画面遷移を行います。
import 'package:flutter/material.dart';
import 'package:jwt_auth/home_page.dart';
import 'package:jwt_auth/login_page.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final tokenProvider = FutureProvider<String?>((ref) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString('token');
});
void main() {
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final tokenAsyncValue = ref.watch(tokenProvider);
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: tokenAsyncValue.when(
data: (token) => token == null ? const LoginPage() : const HomePage(),
loading: () => const CircularProgressIndicator(),
error: (e, stack) => Text('Error: $e'),
),
);
}
}
最後に
JWTとはなんだったかというと、IBMの日本語のサイトによりますと、
JSON Web トークン (JWT) は、JSON オブジェクトとしてフォーマットされた認証情報を安全に送信するために使用されます。
JWT は発行者によってデジタル署名されるため、パスワードを Db2®に公開することなく、署名を検証することで認証目的で使用できます。 JWT 内のクレームは、ユーザーの ID Db2を識別します。
一般には、アプリケーションを介してユーザーがログインするときに JWT を生成するのは ID プロバイダー (IDP) 製品です。ただし、個々のアプリケーション自体で JWT を作成することも可能です。 Db2 は JWT を検証できますが、JWT を生成する方法は提供しません。
JWT公式のサイト
IBMのサイト
Discussion