JWT 로그인 하기(2)

로그인, 로그아웃, 자동 로그인 하는법
HootJem's avatar
Oct 15, 2024
JWT 로그인 하기(2)

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()); }, ),
 
notion image
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

[HootJem] 개발 기록 블로그