본문 바로가기
📱Mobile/🔥Flutter

flutter secure storage, 자동로그인, android KeyStore example

by 후눅스 2021. 1. 7.
반응형

 

flutter

flutter secure storage를 사용한 자동로그인을 해보도록 하겠다. 계정은 하나만 사용하지않고 여러계정을 사용하도록 하겠다.

디버깅을 많이 해보지 않았다. 대충 이런 식이다 라고만, 이렇게 사용하는 식이라는것만 대충 보고 넘어가면 될꺼같다.

 

 

flutter secure storage

flutter에서 각 플랫폼(android, ios)의 내부저장소(keychain or keystore)를 사용할 수 있게 해주는 라이브러리다!

iOS에서는 사용해보지 않았지만 Keychain이라는 것을 사용하고 있는것 같다.

android keystore는 사용해본적이 있다.

안드로이드는 루팅하기 쉬운 운영체제여서 Shared Preference 같은 쉽게 사용할 수 있는 내부 저장소들은  간단한 루팅과정만 거쳐도 adb를 통해 저장되어있는 내용들을 쉽게 볼 수 있다고 한다.

하지만 안드로이드 keystore는 소스코드 내부 어딘가가 아니라, 시스템만이 접근할 수 있는 컨테이너에 저장하기 때문에 루팅을 하여도 접근하지 못한다고 한다.

 

splash

 

login
main
register

패키지 이름은 flutter_securestorage 로 만들었다.

 

!! 참고 KeyStore는 Android 4.3 (API 레벨 18)에서 도입되었습니다. 플러그인은 이전 버전에서는 작동하지 않습니다.

안드로이드 시작시 꼭 해줘야 하는 설정이다. minSdkVersion 을 최소 18로 바꾸어주자.

 

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.0
  flutter_secure_storage: ^3.3.5	//추가
  fluttertoast: ^7.1.6		//추가

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter_secure_storage와 fluttertoast를 추가해준다.

패키지

util.dart

import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';

const USER_NICK_NAME = 'USER_NICK_NAME';
const STATUS_LOGIN = 'STATUS_LOGIN';
const STATUS_LOGOUT = 'STATUS_LOGOUT';

void showToast(String message) {
  Fluttertoast.showToast(
    fontSize: 13,
    msg: '   $message   ',
    backgroundColor: Colors.black,
    toastLength: Toast.LENGTH_SHORT,
    gravity: ToastGravity.BOTTOM,
  );
}

 

login_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_securestorage/views/register_page.dart';
import 'package:flutter_securestorage/common/util.dart';
import 'main_page.dart';

class LoginPage extends StatefulWidget {
  @override
  _LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  bool isLoading = false;
  TextEditingController _userEmailCtrl;
  TextEditingController _userPasswordCtrl;

  @override
  void initState() {
    super.initState();
    _userEmailCtrl = TextEditingController(text: '');
    _userPasswordCtrl = TextEditingController(text: '');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('login Page'),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(20.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Expanded(
                flex: 3,
                child: SizedBox(),
              ),
              _emailWidget(),
              SizedBox(
                height: 8,
              ),
              _passwordWidget(),
              _loginButton(context),
              Expanded(
                flex: 3,
                child: SizedBox(),
              ),
              _registerButton(),
            ],
          ),
        ),
      ),
    );
  }

  Widget _emailWidget() {
    return TextFormField(
      controller: _userEmailCtrl,
      decoration: InputDecoration(
        prefixIcon: Icon(Icons.email),
        labelText: "Email",
        border: OutlineInputBorder(),
      ),
    );
  }

  Widget _passwordWidget() {
    return TextFormField(
      controller: _userPasswordCtrl,
      decoration: InputDecoration(
        prefixIcon: Icon(Icons.vpn_key_rounded),
        labelText: "Password",
        border: OutlineInputBorder(),
      ),
    );
  }

  Widget _loginButton(context) {
    return SizedBox(
      width: MediaQuery.of(context).size.width,
      child: FlatButton(
        color: Colors.purple,
        textColor: Colors.white,
        disabledColor: Colors.purple,
        disabledTextColor: Colors.black,
        padding: EdgeInsets.all(8.0),
        splashColor: Colors.blueAccent,
        onPressed: () => isLoading ? null : _loginCheck(),
        child: Text(
          isLoading ? 'loggin in.....' : 'login',
          style: TextStyle(
            fontSize: 20.0,
            color: Colors.white,
          ),
        ),
      ),
    );
  }

  Widget _registerButton() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text('Don\'t have an account ?'),
        SizedBox(
          width: 20,
        ),
        InkWell(
          onTap: () => Navigator.pushReplacement(context, MaterialPageRoute(builder: (BuildContext context) => RegisterPage())),
          child: Text('register'),
        ),
      ],
    );
  }

  void _loginCheck() async {
    print('_userEmailCtrl.text : ${_userEmailCtrl.text}');
    print('_userPasswordCtrl.text : ${_userPasswordCtrl.text}');
    final storage = FlutterSecureStorage();
    String storagePass = await storage.read(key: _userEmailCtrl.text);
    if(storagePass != null && storagePass != '' && storagePass == _userPasswordCtrl.text){
      print('storagePass : $storagePass');
      String userNickName = await storage.read(key: '${_userEmailCtrl.text}_$storagePass');
      storage.write(key: userNickName, value: STATUS_LOGIN);
      print('로그인 성공');
      Navigator.pushReplacement(context, MaterialPageRoute(builder: (BuildContext context) => MainPage(nickName: userNickName)));
    } else {
      print('로그인 실패');
      showToast('아이디가 존재하지 않거나 비밀번호가 맞지않습니다.');
    }
  }
}

 

main_page.dart

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_securestorage/views/login_page.dart';
import 'package:flutter_securestorage/common/util.dart';

class MainPage extends StatefulWidget {
  String nickName;
  MainPage({this.nickName});

  @override
  _MainPageState createState() => _MainPageState(nickName: nickName);
}

class _MainPageState extends State<MainPage> {
  final storage = new FlutterSecureStorage();
  String nickName;

  _MainPageState({this.nickName});

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Main Page'),
      ),
      body: Center(
        child: Text(
            '${nickName == '' || nickName == null ? '' : 'Hello $nickName'}'),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.keyboard_return_rounded),
        onPressed: () => _logout(),
      ),
    );
  }

  void _logout() async {
    Map<String, String> allStorage = await storage.readAll();
    allStorage.forEach((k, v) async {
      if (v == STATUS_LOGIN) {
        await storage.write(key: k, value: STATUS_LOGOUT);
      }
    });
    Navigator.pushReplacement(context,
        MaterialPageRoute(builder: (BuildContext context) => LoginPage()));
  }
}

 

register_page.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_securestorage/views/login_page.dart';
import 'package:flutter_securestorage/common/util.dart';

import 'main_page.dart';

class RegisterPage extends StatefulWidget {
  @override
  _RegisterPageState createState() => _RegisterPageState();
}

class _RegisterPageState extends State<RegisterPage> {
  bool isLoading = false;
  TextEditingController _userNickNameCtrl;
  TextEditingController _userEmailCtrl;
  TextEditingController _userPasswordCtrl;

  @override
  void initState() {
    super.initState();
    _userNickNameCtrl = TextEditingController(text: '');
    _userEmailCtrl = TextEditingController(text: '');
    _userPasswordCtrl = TextEditingController(text: '');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Register Page'),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(20.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Expanded(
                flex: 3,
                child: SizedBox(),
              ),
              _nickNameWidget(),
              SizedBox(
                height: 8,
              ),
              _emailWidget(),
              SizedBox(
                height: 8,
              ),
              _passwordWidget(),
              _joinButton(context),
              _loginButton(context),
              Expanded(
                flex: 3,
                child: SizedBox(),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _nickNameWidget() {
    return TextFormField(
      controller: _userNickNameCtrl,
      decoration: InputDecoration(
        prefixIcon: Icon(Icons.account_circle_rounded),
        labelText: "nickName",
        border: OutlineInputBorder(),
      ),
    );
  }

  Widget _emailWidget() {
    return TextFormField(
      controller: _userEmailCtrl,
      decoration: InputDecoration(
        prefixIcon: Icon(
          Icons.email,
        ),
        labelText: "Email",
        border: OutlineInputBorder(),
      ),
    );
  }

  Widget _passwordWidget() {
    return TextFormField(
      controller: _userPasswordCtrl,
      decoration: InputDecoration(
        prefixIcon: Icon(Icons.vpn_key_rounded),
        labelText: "Password",
        border: OutlineInputBorder(),
      ),
    );
  }

  Widget _joinButton(context) {
    return SizedBox(
      width: MediaQuery.of(context).size.width,
      child: FlatButton(
        color: Colors.purple,
        textColor: Colors.white,
        disabledColor: Colors.purple,
        disabledTextColor: Colors.black,
        padding: EdgeInsets.all(8.0),
        splashColor: Colors.blueAccent,
        onPressed: () {
          isLoading ? null : _registCheck();
        },
        child: Text(
          isLoading ? 'regist in.....' : 'regist',
          style: TextStyle(
            fontSize: 20.0,
            color: Colors.white,
          ),
        ),
      ),
    );
  }

  Widget _loginButton(context) {
    return SizedBox(
      width: MediaQuery.of(context).size.width,
      child: FlatButton(
        color: Colors.purple,
        textColor: Colors.white,
        disabledColor: Colors.purple,
        disabledTextColor: Colors.black,
        padding: EdgeInsets.all(8.0),
        splashColor: Colors.blueAccent,
        onPressed: () => isLoading
            ? null
            : Navigator.pushReplacement(
                context,
                MaterialPageRoute(
                  builder: (BuildContext context) => LoginPage(),
                ),
              ),
        child: Text(
          'loginPage',
          style: TextStyle(
            fontSize: 20.0,
            color: Colors.white,
          ),
        ),
      ),
    );
  }

  void _registCheck() async {
    final storage = FlutterSecureStorage();
    print(' ');
    print('await storage.readAll() : ');
    print(await storage.readAll());
    print(' ');
    String userNickName = _userNickNameCtrl.text;
    String userEmail = _userEmailCtrl.text;
    String userPassword = _userPasswordCtrl.text;
    if (userNickName != '' && userEmail != '' && userPassword != '') {
      //final storage = FlutterSecureStorage();
      String emailCheck = await storage.read(key: userEmail);
      if (emailCheck == null) {
        storage.write(key: userEmail, value: userPassword);
        storage.write(key: '${userEmail}_$userPassword', value: userNickName);
        storage.write(key: userNickName, value: STATUS_LOGIN);
        Navigator.pushReplacement(context,
            MaterialPageRoute(builder: (BuildContext context) => MainPage(nickName: userNickName,)));
      } else {
        showToast('email이 중복됩니다.');
      }
    } else {
      showToast("입력란을 모두 채워주세요.");
    }
  }
}

 

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_securestorage/views/login_page.dart';
import 'package:flutter_securestorage/views/main_page.dart';
import 'package:flutter_securestorage/common/util.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: SplashPage(),
    );
  }
}

class SplashPage extends StatefulWidget {
  @override
  _SplashPageState createState() => _SplashPageState();
}

class _SplashPageState extends State<SplashPage> {
  @override
  void initState() {
    super.initState();
    Future.delayed(Duration(seconds: 2), () => _checkUser(context));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
          child: Icon(
        Icons.stream,
        size: 80,
        color: Colors.blue,
      )),
    );
  }

  void _checkUser(context) async {
    final storage = new FlutterSecureStorage();
    print('${await storage.readAll()}');
    Map<String, String> allStorage = await storage.readAll();
    String statusUser = '';
    if (allStorage != null) {
      allStorage.forEach((k, v) {
        print('k : $k, v : $v');
        if (v == STATUS_LOGIN) statusUser = k;
      });
    } else {
      Navigator.pushReplacement(
          context, MaterialPageRoute(builder: (context) => LoginPage()));
    }
    if (statusUser != null && statusUser != '') {
      Navigator.pushReplacement(
          context,
          MaterialPageRoute(
              builder: (context) => MainPage(nickName: statusUser)));
    } else {
      Navigator.pushReplacement(
          context, MaterialPageRoute(builder: (context) => LoginPage()));
    }
  }
}

 

함수가 몇가지 없다.

 

readAll 은 forEach로 값 체크하면 된다.

화면은 splash, login, main, register 4개이다.

 

splash 에서 자동로그인 되어있을 시 main 화면으로 바로이동.

splash 에서 자동로그인 안되어있을 시 login 화면으로 이동.

register 에서 회원가입 시 main 화면으로 이동.

자동로그인은 keyStore에 각 계정들이 값으로 가지고있을것이다.

 

키 : 아이디, 값 : 비밀번호

키 : 아이디 + '_' + 패스워드 : 닉네임

키 : 닉네임, 값 : 로그인 여부

 

너무 많으면 복잡해서 그냥 아이디 랑 문자랑 비밀번호랑 섞은 키에 닉네임값을 가지고 있기로 했다.

보통 이런 경우엔 아이디랑 비밀번호를 암호화해서 넣어야된다. 닉네임 키에는 로그인여부 값이 들어있다.

 

 

반응형