[Flutter]토큰 재발급 Interceptor 설정(feat.Dio)

임효진·2024년 3월 13일
0

Flutter

목록 보기
7/22

토큰 만료 시, 토큰을 재발급 받고 이전 요청을 이어가기 위해 본 로직을 짰다.
부족한 점이 있을 수 있다. Dio를 통한 로직임을 참고

API 클래스

개발 모드와 배포 모드 구분:

API 클래스는 kReleaseMode와 환경 변수(dotenv)를 사용하여 앱이 현재 개발 모드인지 배포 모드인지를 판단.
개발 모드와 배포 모드에서 사용할 서버의 기본 URL이 환경 변수를 통해 다르게 설정됨.

환경 변수:

.env 파일에서 여러 환경 변수를 읽어와서 서버의 기본 URL, 포트 번호 등을 설정합니다. 만약 필요한 환경 변수가 누락되었다면 예외를 발생시킴.

Dio 라이브러리 사용:

네트워크 요청을 보내기 위해 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));
  }

Interceptor

토큰 추가:

모든 요청에 액세스 토큰을 Authorization 헤더에 추가.
이미 Authorization 헤더가 설정되어 있다면, 추가하지 않는다.

401 오류 처리:

요청이 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);
  }
}
profile
핫바리임

0개의 댓글