1. 스플래시(SplashPage)
스플래시 페이지란 해당 앱의 로딩을 위한 로고 화면 같은 것이다.
보통 제일 처음 들어갈 때 나오는데, 주로 데이터를 처리하는 과정을 기다릴 때 나온다.
이 코드에서는 세션을 확인하여 LoginPage 로 보내거나, MainPage 로 보내는 역할을 한다.
class SplashPage extends ConsumerWidget {
const SplashPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.read(sessionProvider).autoLogin();
return Scaffold(
body: Center(
child: Image.asset(
'assets/splash.gif',
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
),
),
);
}
}
autoLogin 을 살펴보면 이런 클래스가 나온다.
SessionGM
은 이런 데이터를 처음 갖고 있고 그냥 new 되기 때문에 초기 값은 false 가 된다.import 'package:flutter/cupertino.dart';
import 'package:flutter_blog/main.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SessionGM {
int? id;
String? username;
String? accessToken;
bool? isLogin;
SessionGM({this.id, this.username, this.accessToken, this.isLogin = false});
final mContext = navigatorKey.currentContext!;
Future<void> autoLogin() async {
Future.delayed(
Duration(seconds: 3),
() {
Navigator.popAndPushNamed(mContext, "/post/list");
},
);
}
}
final sessionProvider = StateProvider<SessionGM>((ref) {
return SessionGM();
});
그렇다면 이
SessionGM
을 static 으로 만들어도 될까?
답은 yes 다. Spring서버와 달리 앱은 혼자 사용하기 때문에 static 으로 생성할 수도 있다.mixin class Session {
int? id;
String? username;
String? accessToken;
bool? isLogin;
}
class SessionGM extends Session{
final mContext = navigatorKey.currentContext!;
Future<void> login() async {}
Future<void> join() async {}
Future<void> logout() async {}
Future<void> autoLogin() async {
Future.delayed(
Duration(seconds: 3),
() {
Navigator.popAndPushNamed(mContext, "/post/list");
},
);
}
}
mixin 클래스를 만들어서도 관리할 수 있다.
2. 통신
토큰이 유효한지 찾는 녀석.
서비스 명이 아닌 findMatchAccessToken,verifyJwt 같은 정확한 역할을 이름으로 가져야함.
(그러나 autoLogin 으로 진행함)
class UserRepository {
Future<Map<String, dynamic>> autoLogin(String accessToken) async {
final response = await dio.post(
"/auto/login",
options: Options(headers: {"Authorization": "Bearer $accessToken"}),
);
Map<String, dynamic> one = response.data;
return one;
}
}
Option 을 사용하여 헤더를 넣어줄 수 있다.
그리고 response 의 헤더가 200이 아닌 경우 터지게 된다. 따라서 예외를 잡아주어야한다.
try-catch 내부에 넣는 경우위의 코드를 try 내부에 넣게 되어 catche 에 가게 되면 설정한 응답 false 데이터가 오지 않는다.
따라서 dio의 설정을 추가해야한다.
validateStatus: (status) => true
를 추가하여 응답 코드가 200이 아닐 때에도 에러가 나지 않도록 하는것.final dio = Dio(
BaseOptions(
baseUrl: baseUrl, // 내 IP 입력
contentType: "application/json; charset=utf-8",
validateStatus: (status) => true, // 200 이 아니어도 예외 발생안하게 설정
),
);
2-1. 로그인 통신 코드
일반 로그인 요청을 보낸 경우 이러한 데이터가 응답된다.
{
"success": true,
"response": {
"id": 1,
"username": "ssar",
"imgUrl": "/images/1.png"
},
"status": 200,
"errorMessage": null
}
그러나 토큰도
Headers
에 담겨 오기 때문에 리턴 값이 2개가 되어야 한다.(json 데이터와, header 의 Authorization 을 받아야함)
기존의 코드는 응답된 json 데이터만 리턴한다. 따라서 바꿔 주어야함.
// 변경 전
Future<Map<String, dynamic>> login(String username, String password) async {
final response = await dio.post("/login", data: {
"username": username,
"password": password,
});
Map<String, dynamic> one = response.data;
return one;
}
// 변경 후 (플러터는 리턴값이 2개가 될 수 있다.)
Future<(Map<String, dynamic>, String)> login(
String username, String password) async {
final response = await dio.post("/login", data: {
"username": username,
"password": password,
});
String accessToken = response.headers["Authorization"]![0];
Map<String, dynamic> body = response.data;
return (body, accessToken);
}
Authorization
은 하나만 응답이 가능 함.
쿠키는 응답이 ;
로 구분되어 여러개 응답이 가능하다. (List 로 받는다.)그런 이유때문인가 header 의 0번지에 JWT 가 들어있다.
3. 로그인 코드
로그인 코드가 해야하는 역할은 다음과 같다.
1. 통신 2. 성공 (1) SessionGM 값 변경 (2) 휴대폰 하드 저장 (3) dio 에 토큰 세팅 (4) 화면 이동 3. 실패 처리
로그인이 성공했을 경우 토큰을 핸드폰 하드에 저장을해야 한다.
쉐어드프리페어먼스라는게 있는데 여기에 넣어두면 다른앱들도 쓸 수 있다. 따라서 시큐어 스토리지를 사용하게 되는데
어플리케이션 Secure Storage를 쉽게 사용할 수 있도록 도와주는 라이브러리를 쓸 수 있다.
이는 해당되는 앱만 사용이 가능하도록 하는것.
dependencies:
flutter_secure_storage: ^8.0.0
전역변수로 만들어져 있다.
const secureStorage = FlutterSecureStorage();
로그인 메서드 실행 코드
Future<void> login(String username, String password) async {
// 1. 통신 {success:뭐시기, status:뭐시기, errorMassage: 뭐시기, response:오브젝트}
var (body, accessToken) = await UserRepository().login(username, password);
// 2. 성공 or 실패 처리
if (body["success"]) {
// (1) SessionGM 값 변경
this.id = body["response"]["id"];
this.username = body["response"]["username"];
this.accessToken = accessToken;
this.isLogin = true; //상태를 바꿨으나 read 만 가능한 provider 라 화면이 다시 그려지진 않음
// (2) 휴대폰 하드 저장
await secureStorage.write(key: "accessToken", value: accessToken);
// (3) dio 에 토큰 세팅
dio.options.headers["Autorization"] = accessToken;
// (4) 화면 이동
Navigator.pushNamed(mContext, "/post/list");
} else {
ScaffoldMessenger.of(mContext).showSnackBar(
SnackBar(content: Text("${body["errorMessage"]}")),
);
}
}
로그인 폼
혹시 모를 공백 처리를 위해 trim 추가
CustomElevatedButton(
text: "로그인",
click: () {
ref
.read(sessionProvider)
.login(_username.text.trim(), _password.text.trim());
},
),

import 'package:logger/logger.dart';
Logger().d("로그인 성공");
사용하여 이렇게 로그에 출력할 수 있음.
로그아웃
시큐어 스토리지에
accessToken
를 삭제하고, isLogin 을 false 로 바꾼다. Future<void> logout() async {
await secureStorage.delete(key: "accessToken");
this.id = null;
this.username = null;
this.accessToken = accessToken;
this.isLogin = false;
Navigator.pushNamed(mContext, "/post/list");
}
그리고 로그아웃 버튼에서 해당 메서드를 호출하면 된다.
TextButton(
onPressed: () {
ref.read(sessionProvider).logout();
},
child: const Text(
"로그아웃",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black54,
),
),
),
자동 로그인
1. 시큐어 스토리지에서 accessToken 꺼내기 2. api 호출 3. 세션 값 갱신 4. 정상이면 /post/list 로 이동
Future<void> autoLogin() async {
// 1. 시큐어 스토리지에서 accessToken 꺼내기
String? accessToken = await secureStorage.read(key: "accessToken");
Logger().d("accessToken? , ${accessToken}");
if (accessToken == null) {
Navigator.popAndPushNamed(mContext, '/login');
} else {
// 2. api 호출
Map<String, dynamic> body = await UserRepository().autoLogin(accessToken);
// 3. 세션 값 갱신
this.id = body["response"]["id"];
this.username = body["response"]["username"];
this.accessToken = accessToken;
this.isLogin = true;
await secureStorage.write(key: "accessToken", value: accessToken);
dio.options.headers["Autorization"] = accessToken;
// 4. 화면 이동
Navigator.pushNamed(mContext, "/post/list");
}
}
}
3,4 는 기본 로그인과 동일 합니다.
이것을 테스트 하고 싶다면 플러터 에뮬레이터에서 에뮬레이터를 종료 시키지 않고
탭을 끈 뒤 다시 들어가 보면 됩니다.
Share article