올해초 입사를하고 업무 외에도 듣고 싶었던 강의가 몇개 있었는데 적응하느라 들어보지도 못하고 정신 없이 보냈던것같다. 이제는 조금 여유가 생겨서 강의를 조금씩 들어보고 있는데 아무래도 강의 초반부분 이라서 로그인,사용자 인증에 관한 내용에 대해 많이 배우는것 같다. 자동 로그인 과정에서 유용하게 사용할 secure storage 에 대해 적어보겠다.
secure storage
앱 내에서 민감한 데이터를 안전하게 보관하고 액세스하는 데 사용되는 중요한 도구로 , 주로 사용자 정보, 비밀번호, 토큰 및 기타 민감한 정보를 안전하게 저장하기 위해 활용된다.
secure storage를 이용하게 되면 데이터를 내부 저장소 영역에 저장을 할 수 있게 된다. 앱 내에서 민감한 데이터를 일반적인 데이터베이스나 파일 시스템에 저장하는 것은 보안상 취약점을 가질 수 있기 때문에 Secure storage는 이러한 데이터를 안전하게 암호화하여 iOS는 keychain , Android는 keystore영역에 저장하여 외부에서 액세스하기 어렵게 만든다.
dependencies:
flutter_secure_storage: ^replace-with-latest-version
우선 Secrue Storage를 사용하기 위해서는 pubspec.yaml에 flutter_secure_storage를 추가해야 한다. 추가로 android > app > build.gradle 에서 minSdkVersion을 18이상으로 해야한다.
사용방법 및 기능
secure storage 의 기능들을 간단하게 알아보겠다.
final storage = FlutterSecureStorage();
// 데이터 저장
await storage.write(key: 'myKey', value: 'myValue');
// 데이터 검색
String? value = await storage.read(key: 'myKey');
// 데이터 삭제
await storage.delete(key: 'myKey');
토큰 유무에 따른 자동로그인
최근 공부했던 JWT Dio를 공부했던 내용들을 바탕으로 api서버가 있다고 가정하고, 서버로 username:password 값을 base64로 인코딩후 Header에 'authorization' : "Basic $token" 형태로 전송하여 로그인 완료시 토큰이 생성되면 토큰 유무에 따른 자동로그인 과정을 secure storage를 이용하여 간단한 로직을 코드로 구현해보겠다.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
final storage = FlutterSecureStorage();
final dio = Dio();
class SecureStorageScreen extends StatefulWidget {
@override
_SecureStorageScreenState createState() => _SecureStorageScreenState();
}
class _SecureStorageScreenState extends State<SecureStorageScreen> {
final storage = FlutterSecureStorage();
String? authToken;
@override
void initState() {
super.initState();
_loadAuthToken();
}
Future<void> _loadAuthToken() async {
final accessToken = await storage.read(key: 'ACCESS_TOKEN');
final refreshToken = await storage.read(key: 'REFRESH_TOKEN');
final dio = Dio();
// Refresh Token을 이용해 Access Token 발급 로직
// Refresh Token 만료시 자동로그인 안됨 , 다시 로그인 페이지
try {
final resp = await dio.post(
'http://$testUrl/token',
options: Options(headers: {'authorization': 'Bearer $refreshToken'}),
);
} catch (e) {
// 토큰이 없으면 로그인 페이지로 이동
Navigator.pushReplacement(
context, MaterialPageRoute(builder: (context) => LoginPage()));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Login 완료 , Token 유효'),
ElevatedButton(
onPressed: () async {
// 토큰을 삭제하고 로그인 페이지로 이동
await storage.delete(key: 'ACCESS_TOKEN');
await storage.delete(key: 'REFRESH_TOKEN');
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) => LoginPage()));
},
child: Text('로그아웃'),
),
],
),
),
);
}
}
class LoginPage extends StatelessWidget {
var email = '';
var password = '';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('로그인 페이지'),
),
body: Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 100),
child: Column(
children: [
TextFormField(
onChanged: (value) {
email = value;
},
),
const SizedBox(
height: 16.0,
),
TextFormField(
onChanged: (value) {
password = value;
},
),
const SizedBox(
height: 16.0,
),
ElevatedButton(
onPressed: () async {
// email : password
final rawString = '$email:$password';
// 토큰 발급을 위하여 string을 base64로 인코딩 코드
Codec<String, String> stringToBase64 = utf8.fuse(base64);
String token = stringToBase64.encode(rawString);
final resp = await dio.post(
'http://testUrl/login',
options:
Options(headers: {'authorization': 'Basic $token'}),
);
// 로그인 성공시
var accessToken =
"your_access_token"; // resp.data.accessToken : api서버를 통해 발급받은 accessToken
var refreshToken =
"your_refresh_token"; // resp.data.refreshToken : api서버를 통해 발급받은 refreshToken
await storage.write(key: 'ACCESS_TOKEN', value: accessToken);
await storage.write(
key: 'REFRESH_TOKEN', value: refreshToken);
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => SecureStorageScreen()));
},
child: Text('로그인'),
),
],
),
),
),
);
}
}
이렇게 간단하게 구현해보았다. 최근 배웠던 내용들을 한번 더 정리하는 시간을 가진것 같아서 만족한다. 특히 JWT에 대해서 이론으로만 공부했어서 어떻게 사용해야할지 감이 잘 오지 않았는데 간단하지만 코드를 작성해보니 JWT가 어떻게 생성되고 어떻게 안전하게 보관되는지 등에 대한 감을 잡을 수 있었다.
참고
https://www.inflearn.com/course/%ED%94%8C%EB%9F%AC%ED%84%B0-%EC%8B%A4%EC%A0%84/dashboard
https://velog.io/@jakob1/FlutterSecureStorage%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%83%81%ED%83%9C-%EC%9C%A0%EC%A7%80
안녕하세요? 해당 코드를 보고 jwt 로그인에 대해서 감을 잡을 수 있었습니다.
다만 질문이 있습니다. accessToken 는 발급받은 이후에 어디서 사용하나요??
현재 refreshToken을 이용해서 로그인하는 것이 맞나요?