🐌

FlutterでJWT認証を行う

2023/08/31に公開

自作したログインを使ってみる

FlutterでFirebaseとSupabaseでしかログインはやったことなくて、そろそろ自作した認証機能でやってみたいな〜と思い、Node.jsを使ってJWT認証やってみました。

これがサンプル
https://github.com/sakurakotubaki/FlutterJWT
JWTのAPIはこれで作る
https://zenn.dev/joo_hashi/articles/7516812c1a389f

📦まずは必要なパッケージを追加

  1. 後で、riverpodで状態管理するページがあるので入れておく

https://pub.dev/packages/flutter_riverpod
2. APIと通信するパッケージを追加する

https://pub.dev/packages/http
3. トークンの保存にこれが必要

https://pub.dev/packages/shared_preferences

ログインページを作る

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公式のサイト

https://jwt.io/

IBMのサイト

https://www.ibm.com/docs/ja/db2/11.5?topic=authentication-json-web-tokens-jwt

Discussion