토큰 만료 시, 토큰을 재발급 받고 이전 요청을 이어가기 위해 본 로직을 짰다.
부족한 점이 있을 수 있다. Dio를 통한 로직임을 참고
API 클래스는 kReleaseMode와 환경 변수(dotenv)를 사용하여 앱이 현재 개발 모드인지 배포 모드인지를 판단.
개발 모드와 배포 모드에서 사용할 서버의 기본 URL이 환경 변수를 통해 다르게 설정됨.
.env 파일에서 여러 환경 변수를 읽어와서 서버의 기본 URL, 포트 번호 등을 설정합니다. 만약 필요한 환경 변수가 누락되었다면 예외를 발생시킴.
네트워크 요청을 보내기 위해 Dio 라이브러리의 인스턴스를 생성.
Dio는 Flutter에서 HTTP 요청을 쉽게 다룰 수 있게 해주는 라이브러리.
인증 과정을 처리하기 위해 AuthInterceptor 클래스의 인스턴스를 Dio의 인터셉터로 추가.
이 인터셉터는 토큰을 관리하고, 만료된 경우 새로운 토큰을 재발급 받는 역할을 합함.
API 클래스 내부
API() {
/* kReleaseMode를 사용하여 현재 모드가 개발 모드인지 배포 모드인지 확인 */
String? baseUrlDev = dotenv.env['DEV_BASE_API_KEY'];
String? baseUrlProd = dotenv.env['BASE_API_KEY'];
basePort = dotenv.env['BASE_PORT'] ?? '';
baseBackOfficePort = dotenv.env['BACK_OFFICE_PORT'] ?? '';
bool homeDebugMode = dotenv.env['HOME_DEBUG_MODE'] == 'true';
if (baseUrlProd == null) {
throw "BASE_API_KEYs are missing from .env file!";
}
baseUrl = (kReleaseMode || homeDebugMode)
? (baseUrlProd ?? '')
: (baseUrlDev ?? '');
/* 인터셉터 */
dio = Dio(
BaseOptions(
baseUrl: baseUrl,
headers: {
'Content-Type': 'application/json',
},
),
);
dio.interceptors.add(AuthInterceptor(reissueToken));
}
모든 요청에 액세스 토큰을 Authorization 헤더에 추가.
이미 Authorization 헤더가 설정되어 있다면, 추가하지 않는다.
요청이 401(Unauthorized) 오류로 실패하면, 리프레시 토큰을 사용하여 새 액세스 토큰을 재발급.
새 토큰을 성공적으로 받아오면, 실패한 요청을 새 토큰으로 다시 시도.
refreshToken을 사용하여 새로운 accessToken과 refreshToken을 받아온다.
새 토큰을 받아오는 데 성공하면, 이를 저장소에 저장하고 실패한 요청을 새 토큰으로 재시도.
이 구조를 통해, 앱은 사용자의 인증 상태를 적절히 관리하고, 필요할 때마다 토큰을 자동으로 재발급 받아 API 요청의 인증을 유지할 수 있다.
또한, 개발과 배포 환경에서 서로 다른 API 엔드포인트를 사용할 수 있도록 환경을 구성할 수 있다.
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class AuthInterceptor extends InterceptorsWrapper {
final FlutterSecureStorage storage = const FlutterSecureStorage();
final Dio dio = Dio();
final String reissueTokenEndpoint;
AuthInterceptor(this.reissueTokenEndpoint);
// 이미 'Authorization' 헤더가 설정되어 있다면, 저장된 토큰을 추가하지 않도록 조건을 설정
Future<void> onRequest(
RequestOptions options, RequestInterceptorHandler handler) async {
if (options.headers['Authorization'] == null) {
var accessToken = await storage.read(key: 'accessToken');
if (accessToken != null) {
options.headers['Authorization'] = 'Bearer $accessToken';
}
}
handler.next(options);
}
Future<void> onError(DioError err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401) {
var refreshToken = await storage.read(key: 'refreshToken');
try {
var response = await dio.post(
reissueTokenEndpoint,
data: {'refresh_token': refreshToken},
options: Options(
headers: {
'Content-Type': 'application/json',
},
),
);
var newAccessToken = response.headers['access_token']?.first;
var newRefreshToken = response.data['refresh_token'];
if (newAccessToken != null) {
await storage.write(key: 'accessToken', value: newAccessToken);
if (newRefreshToken != null) {
await storage.write(key: 'refreshToken', value: newRefreshToken);
}
err.requestOptions.headers['Authorization'] =
'Bearer $newAccessToken';
return handler.resolve(await dio.fetch(err.requestOptions));
} else {
// TODO: newAccessToken이 null이면 적절한 에러 처리.
return handler.next(err);
}
} catch (e) {
// 리프레시 토큰 요청에서 에러 발생
return handler.next(err);
}
}
handler.next(err);
}
}