모바일 앱에서 한번 로그인하게 되면 일반적으로 로그인 상태를 유지하게 된다.
로그인한 적이 있으면 로그아웃을 하지 않는다면 로그인을 두번하지 않아도 되도록
해줘야하는데로그인 정보
를 어떻게안전하게 저장
할 수 있을지가 문제다.
flutter 에서 디바이스 내부에 정보를 저장하기 위해서 일반적으로 shared_preferences
패키지를 사용한다.
그러나 이러한 영역에 로그인 상태를 유지를 위해 사용하는 정보들을 그대로 저장하게 되면 보안에 취약한 앱
이 된다.
Android에서의 문제
Android는 루팅
을 통하여 루트 권한을 얻음으로 생산자, 판매자측에서 걸어 놓은 제약을 해제하면
Shared Preference
같은 쉽게 사용할 수 있는 내부 저장소들은 간단한 루팅
과정만으로
ADB
(Android Debug Bridge)를 사용해서 저장되어있는 내용들을 쉽게 볼 수 있다고 한다.
iOS에서의 문제
iOS는 탈옥(Jailbreaking)
을 통하여 iOS의 샌드박스 제한을 풀어 타 회사에서 사용하는 서명되지 않은 코드를 실행할 수 있게 된다.
이러한 보안 문제를 해결하기 위하여 FlutterSecureStorage
를 사용하였다.
민감한 정보들을 저장할 때 flutter_secure_storage
라는 패키지를 사용하면
Android
에서는 keystore
영역에, iOS
에서는 keychain
라는 내부 저장소 영역
을 사용하게 된다.
Android
의 keystore
은 소스코드 내부 어딘가가 아닌 시스템만이 접근할 수 있는 컨테이너에 저장
하기 때문에 루팅을 하여도 접근 할 수 없다. 그래서 앱에서 임의로 발급한 암호화 키만 저장하고 이 키를 이용해 정보를 암호화해 로컬 DB에 저장하고, 저장된 정보를 사용할 때는 복호화해 사용한다.
혹시 이 글을 읽는 분들중에 아래의 내용에는 로그인까지 고려하여 구현하다보니 코드가 길지만 주석을 참고하시면 좋을 것 같다. 참고로 서버와 API 통신할 때 TextField에 입력된 데이터를 직렬화를 활용하여 전달하는 것도 추가로 구현하였다.
dependencies:
flutter:
sdk: flutter
flutter_secure_storage : ^5.0.2 // flutter_secure_storage 관련 패키지
dio: ^4.0.6 // http 통신을 위한 패키지
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('로그인 하기'),
),
],
),
);
}
}
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();
},
),
],
),
);
}
}
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,
};
}
공유해주셔서 감사합니다 https://phrazle-wordle.com/