FlutterSecureStorage를 활용한 로그인 상태 유지

Jocy·2022년 7월 21일
8
post-thumbnail

왜? FlutterSecureStorage를 사용하게 되었는가?

모바일 앱에서 한번 로그인하게 되면 일반적으로 로그인 상태를 유지하게 된다.
로그인한 적이 있으면 로그아웃을 하지 않는다면 로그인을 두번하지 않아도 되도록
해줘야하는데 로그인 정보를 어떻게 안전하게 저장할 수 있을지가 문제다.

flutter 에서 디바이스 내부에 정보를 저장하기 위해서 일반적으로 shared_preferences 패키지를 사용한다.
그러나 이러한 영역에 로그인 상태를 유지를 위해 사용하는 정보들을 그대로 저장하게 되면 보안에 취약한 앱이 된다.

무슨 문제로 보안에 취약한 앱이 되는 것일까?

  • Android에서의 문제
    Android는 루팅을 통하여 루트 권한을 얻음으로 생산자, 판매자측에서 걸어 놓은 제약을 해제하면
    Shared Preference 같은 쉽게 사용할 수 있는 내부 저장소들은 간단한 루팅 과정만으로
    ADB(Android Debug Bridge)를 사용해서 저장되어있는 내용들을 쉽게 볼 수 있다고 한다.

  • iOS에서의 문제
    iOS는 탈옥(Jailbreaking) 을 통하여 iOS의 샌드박스 제한을 풀어 타 회사에서 사용하는 서명되지 않은 코드를 실행할 수 있게 된다.

이러한 보안 문제를 해결하기 위하여 FlutterSecureStorage를 사용하였다.

FlutterSecureStorage란? 무엇인가?

민감한 정보들을 저장할 때 flutter_secure_storage 라는 패키지를 사용하면
Android에서는 keystore 영역에, iOS에서는 keychain라는 내부 저장소 영역을 사용하게 된다.

Androidkeystore은 소스코드 내부 어딘가가 아닌 시스템만이 접근할 수 있는 컨테이너에 저장 하기 때문에 루팅을 하여도 접근 할 수 없다. 그래서 앱에서 임의로 발급한 암호화 키만 저장하고 이 키를 이용해 정보를 암호화해 로컬 DB에 저장하고, 저장된 정보를 사용할 때는 복호화해 사용한다.

FlutterSecureStorage에 로그인 정보 저장 구현

혹시 이 글을 읽는 분들중에 아래의 내용에는 로그인까지 고려하여 구현하다보니 코드가 길지만 주석을 참고하시면 좋을 것 같다. 참고로 서버와 API 통신할 때 TextField에 입력된 데이터를 직렬화를 활용하여 전달하는 것도 추가로 구현하였다.

pubspec.yaml 파일에 패키지 추가

dependencies:
  flutter:
    sdk: flutter
  flutter_secure_storage : ^5.0.2 // flutter_secure_storage 관련 패키지
	dio: ^4.0.6 // http 통신을 위한 패키지

Main.dart

import 'package:flutter/material.dart';
import 'package:dio/dio.dart'; // DIO 패키지로 HTTP 통신
import 'dart:convert'; // JSON Encode, Decode를 위한 패키지
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; // flutter_secure_storage 패키지 

import 'models/model.dart';

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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Secure Storage',
      home: LoginPage(),
    );
  }
}

class LoginPage extends StatefulWidget {
  LoginPage({Key? key}) : super(key: key);

  
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  var username = TextEditingController(); // id 입력 저장
  var password = TextEditingController(); // pw 입력 저장

  static final storage = FlutterSecureStorage(); // FlutterSecureStorage를 storage로 저장 
	dynamic userInfo = ''; // storage에 있는 유저 정보를 저장
  
  //flutter_secure_storage 사용을 위한 초기화 작업
  
  void initState() {
    super.initState();

    // 비동기로 flutter secure storage 정보를 불러오는 작업
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _asyncMethod();
    });
  }

  _asyncMethod() async {
    // read 함수로 key값에 맞는 정보를 불러오고 데이터타입은 String 타입
    // 데이터가 없을때는 null을 반환
    userInfo = await storage.read(key:'login');

    // user의 정보가 있다면 로그인 후 들어가는 첫 페이지로 넘어가게 합니다.
    if (userInfo != null) {
      Navigator.pushNamed(context, '/main');
    } else {
      print('로그인이 필요합니다');
    }
  }
	
	// 로그인 버튼 누르면 실행
  loginAction(accountName, password) async {
    try {
      var dio = Dio();
      var param = {'account_name': '$accountName', 'password': '$password'};

      Response response = await dio.post('로그인 API URL', data: param);

      if (response.statusCode == 200) {
        final jsonBody = json.decode(response.data['user_id'].toString());
				// 직렬화를 이용하여 데이터를 입출력하기 위해 model.dart에 Login 정의 참고
        var val = jsonEncode(Login('$accountName', '$password', '$jsonBody'));
        
				await storage.write( 
          key: 'login',
          value: val,
        );
        print('접속 성공!');
        return true;
      } else {
        print('error');
        return false;
      }
    } catch (e) {
      return false;
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
        body: Column(
	        children: [
						// 아이디 입력 영역
	          TextField(
	            controller: username,
	            decoration: InputDecoration(
	              labelText: 'Username',
	            ),
	          ),
						// 비밀번호 입력 영역
	          TextField(
	            controller: password,
	            decoration: InputDecoration(
	              labelText: 'Password',
	            ),
	          ),
						// 로그인 버튼
	          ElevatedButton(
	            onPressed: () async {
	              if (await loginAction(username.text, password.text) ==
	                  true) {
	                print('로그인 성공');
	                Navigator.pushNamed(context, '/service'); // 로그인 이후 서비스 화면으로 이동
	              } else {
	                print('로그인 실패');
	              }
	            },
	            child: Text('로그인 하기'),
	          ),
	        ],
       ),
    );
  }
}

Logout.dart

import 'package:flutter/material.dart';
import 'dart:convert'; // JSON Encode, Decode를 위한 패키지
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; // flutter_secure_storage 패키지 

class ServicePage extends StatefulWidget {
  const ServicePage({Key? key}) : super(key: key);

  
  State<ServicePage> createState() => _ServicePageState();
}

class _ServicePageState extends State<ServicePage> {
  static final storage = FlutterSecureStorage();
  dynamic userInfo = '';

  logout() async {
    await storage.delete(key: 'login');
    Navigator.pushNamed(context, '/');
  }

  checkUserState() async {
    userInfo = await storage.read(key: 'login');
    if (userInfo == null) {
      print('로그인 페이지로 이동');
      Navigator.pushNamed(context, '/'); // 로그인 페이지로 이동
    } else {
      print('로그인 중');
    }
  }

  
  void initState() {
    super.initState();

    // 비동기로 flutter secure storage 정보를 불러오는 작업
    WidgetsBinding.instance.addPostFrameCallback((_) {
      checkUserState();
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Main'),
        actions: [
          IconButton(
            icon: Icon(Icons.logout),
            tooltip: 'logout',
            onPressed: () {
              logout();
            },
          ),
        ],
      ),      
    );
  }
}

models/model.dart

class Login {
  final String accountName;
  final String password;
  final String user_id;

  Login(this.accountName, this.password, this.user_id);

  Login.fromJson(Map<String, dynamic> json)
      : accountName = json['accountName'],
        password = json['password'],
        user_id = json['user_id'];

  Map<String, dynamic> toJson() => {
        'accountName': accountName,
        'password': password,
        'user_id': user_id,
      };
}
profile
Software Engineer

1개의 댓글

comment-user-thumbnail
2023년 4월 13일

공유해주셔서 감사합니다 https://phrazle-wordle.com/

답글 달기