[flutter] dio 토큰 인터셉터 구축

해달·2024년 5월 6일

기능 구현

  1. 스플래쉬 띄워놓기
  2. 메인화면에서 유저 프로필 데이터 받아와서 bool 타입의 변수지정
  3. 스플래시 제거 후 Main app bool 타입 변수 전달
  4. bool 값으로 초기화면 정하기
    • 로그인화면 or 프로덕트 화면

1. main storage 토큰 확인

  const storage = FlutterSecureStorage();
  String? accessToken = await storage.read(key: 'accessToken');
  String? refreshToken = await storage.read(key: 'refreshToken');

// token 인터셉터 달기
  final tokenInterceptor = TokenInterceptor(AuthApiService.dio, storage);
  AuthApiService.dio.interceptors.add(tokenInterceptor);


// mainApp 실행 시 넘겨줄 bool 값
  bool isAuthenticated = false;

  if (accessToken != null) {
    isAuthenticated = await AuthApiService.getProfile(accessToken: accessToken);
  }

2. getProfile

  • bool 타입으로 값 받아오기

accessToken 이용해서 user Data 받아오기
(엑세스토큰 : 유효기간 12시간)

로직
유효 : 유저 데이터 받아옴


유효하지 않을 경우
1. 인터셉터 로직 실행

  static Future<bool> getProfile({required String accessToken}) async {
    try {

      Response response = await dio.get(
        '$baseUrl/auth/profile',
        options: Options(headers: {
          'Content-Type': 'application/json',
          'authorization': 'Bearer $accessToken',
        }),
      );

      return true;
    } catch (e) {
      print('getProfile failed: $e');

      return false;
    }
  }

3. interceptor

인터셉터 에러 로직

  1. 엑세스토큰 만료 에러 시 리프레쉬 토큰으로 엑세스토큰 다시 세팅
  2. 토큰 새로 받아왔다면 (리프레쉬토큰 유효)
  3. 기존요청 헤더에 엑세스 토큰 재 세팅
  4. 기존요청 재시도
  5. handler.resolve(response)
    - response는 현재 요청 또는 응답에 대한 Dio의 Response 객체
    - handler.resolve(response)는 Interceptor에서 다음 단계로 넘어가기 위해 사용되는 메서드
    이 메서드는 현재 Interceptor를 종료하고, 다음 Interceptor로 제어를 넘기는 역할
    • handler.resolve(response)를 호출하면 현재 Interceptor에서 다음 단계로 진행
  6. handler.next(err)
    • err은 발생한 DioException 객체
    • 이 메서드는 현재 Interceptor에서 다음 단계로 예외를 전달하는 역할
    • 즉, 현재 Interceptor에서 예외 처리를 완료했거나 처리할 수 없는 경우, 예외를 다음 단계로 전달하여 다음 Interceptor나 Dio의 예외 처리 로직이 실행될 수 있도록 함

onError ChatGPT 설명

  1. 함수가 실행되면, 먼저 DioException 객체와 ErrorInterceptorHandler를 인자로 받습니다.
  1. DioException은 Dio에서 발생한 예외를 나타내며, ErrorInterceptorHandler는 예외 처리를 담당하는 핸들러입니다.
    함수 내부에서는 먼저 받은 예외 객체에서 HTTP 응답 코드가 401(Unauthorized)인지 확인합니다. 이 때, 해당 응답의 데이터에 '토큰이 만료되었습니다.' 메시지가 포함되어 있는지도 함께 확인합니다.
  1. 만약 응답 코드가 401이고, 메시지가 '토큰이 만료되었습니다.'인 경우에는 refreshToken() 메서드를 호출하여 새로운 액세스 토큰을 갱신합니다.

4.새로운 액세스 토큰이 정상적으로 갱신되었다면, 해당 토큰을 이용하여 요청을 다시 보내기 위해 예외가 발생한 요청의 헤더에 새로운 액세스 토큰을 추가합니다.

  1. _retry() 메서드를 사용하여 새로운 헤더를 적용한 새로운 요청을 보냅니다. 이 때, 해당 요청이 성공하면 handler.resolve(response)를 호출하여 요청을 성공적으로 처리한 후에 종료합니다.
  1. 그러나 새로운 요청이 실패하면(DioException이 발생하면), 해당 예외를 다음 단계로 전달하기 위해 handler.next(e)를 호출합니다.
  1. 모든 예외 처리가 끝나면, super.onError(err, handler)를 호출하여 예외를 다음 단계로 전달합니다. 만약 이 함수에서 예외를 처리하지 않았거나 처리할 수 없는 경우에는 이 부분이 실행됩니다.
  
  Future onError(DioException err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode == 401) {
      if (err.response?.data['message'] == '토큰이 만료되었습니다.') {
        final newAccessToken = await refreshToken();

        // 새로운 액세스 토큰이 있으면 요청 재시도
        if (newAccessToken != '') {
          err.requestOptions.headers['authorization'] =
              'Bearer $newAccessToken';
          try {
            final response = await _retry(err.requestOptions);
            return handler.resolve(response);
          } on DioException catch (e) {
            handler.next(e);
          }
        }
      }
    }

    super.onError(err, handler);
  }

retry

요청이 실패한 경우에 요청을 다시 시도하는 데 사용되는 함수

  1. 새로운 RequestOptions를 생성하여 이전 요청에 사용된 것과 같은 설정으로 다시 요청을 보낸다.
  // retry
  Future<Response<dynamic>> _retry(RequestOptions requestOptions) async {
    final options = Options(
      method: requestOptions.method,
      headers: requestOptions.headers,
    );

    // Retry the request with the new `RequestOptions` object.
    return dio.request<dynamic>(requestOptions.path,
        data: requestOptions.data,
        queryParameters: requestOptions.queryParameters,
        options: options);
  }
}

refreshToken

  1. 스토리지에서 리프래쉬토큰 꺼내온다
  2. 만료되지 않았으면 새 엑세스토큰을 받아온다
  3. 스토리지에 새 엑세스토큰 세팅
  4. 새 엑세스 토큰 리턴
  5. 실패했을 경우 스토리지 모든 토큰 제거
  Future<String> refreshToken() async {
    const storage = FlutterSecureStorage();
    final refreshToken = await storage.read(key: 'refreshToken');

    try {

      Response response = await dio.post(
        '$baseUrl/auth/refresh',
        data: {'refreshToken': refreshToken},
        options: Options(headers: {'Content-Type': 'application/json'}),
      );

      if (response.statusCode == 201) {
        final newAccessToken = response.data['accessToken'];
        await storage.write(key: 'accessToken', value: newAccessToken);

        return newAccessToken;
      }
    } catch (e) {
      storage.delete(key: 'accessToken');
      storage.delete(key: 'refreshToken');
      print('refresh token failed: $e');
    }
    return '';
  }

플러터 인터셉터 달면서 작성한 코드 내용 토대로 정리해놓기

reference

0개의 댓글