在 Dio 中使用 Interceptor for Flutter 刷新 Token

r4j*_*007 30 http dart firebase-authentication flutter

我试图在颤动中使用带有 Dio 的拦截器,我必须处理令牌过期。以下是我的代码

Future<Dio> getApiClient() async {
    token = await storage.read(key: USER_TOKEN);
    _dio.interceptors.clear();
    _dio.interceptors
        .add(InterceptorsWrapper(onRequest: (RequestOptions options) {
      // Do something before request is sent
      options.headers["Authorization"] = "Bearer " + token;
      return options;
    },onResponse:(Response response) {
        // Do something with response data
        return response; // continue
    }, onError: (DioError error) async {
      // Do something with response error
      if (error.response?.statusCode == 403) {
        // update token and repeat
        // Lock to block the incoming request until the token updated

        _dio.interceptors.requestLock.lock();
        _dio.interceptors.responseLock.lock();
        RequestOptions options = error.response.request;
        FirebaseUser user = await FirebaseAuth.instance.currentUser();
        token = await user.getIdToken(refresh: true);
        await writeAuthKey(token);
        options.headers["Authorization"] = "Bearer " + token;

        _dio.interceptors.requestLock.unlock();
        _dio.interceptors.responseLock.unlock();
        _dio.request(options.path, options: options);
      } else {
        return error;
      }
    }));
    _dio.options.baseUrl = baseUrl;
    return _dio;
  }
Run Code Online (Sandbox Code Playgroud)

问题是,Dio 没有使用新令牌重复网络调用,而是将错误对象返回给调用方法,这反过来又渲染了错误的小部件,有关如何使用 dio 处理令牌刷新的任何线索?

r4j*_*007 32

我通过以下方式使用拦截器解决了它:-

  Future<Dio> getApiClient() async {
    token = await storage.read(key: USER_TOKEN);
    _dio.interceptors.clear();
    _dio.interceptors
        .add(InterceptorsWrapper(onRequest: (RequestOptions options) {
      // Do something before request is sent
      options.headers["Authorization"] = "Bearer " + token;
      return options;
    },onResponse:(Response response) {
        // Do something with response data
        return response; // continue
    }, onError: (DioError error) async {
      // Do something with response error
      if (error.response?.statusCode == 403) {
        _dio.interceptors.requestLock.lock();
        _dio.interceptors.responseLock.lock();
        RequestOptions options = error.response.request;
        FirebaseUser user = await FirebaseAuth.instance.currentUser();
        token = await user.getIdToken(refresh: true);
        await writeAuthKey(token);
        options.headers["Authorization"] = "Bearer " + token;

        _dio.interceptors.requestLock.unlock();
        _dio.interceptors.responseLock.unlock();
        return _dio.request(options.path,options: options);
      } else {
        return error;
      }
    }));
    _dio.options.baseUrl = baseUrl;
    return _dio;
  }
Run Code Online (Sandbox Code Playgroud)


Jac*_*ack 27

我找到了一个简单的解决方案,如下所示:

this.api = Dio();

    this.api.interceptors.add(InterceptorsWrapper(
       onError: (error) async {
          if (error.response?.statusCode == 403 ||
              error.response?.statusCode == 401) {
              await refreshToken();
              return _retry(error.request);
            }
            return error.response;
        }));

Run Code Online (Sandbox Code Playgroud)

基本上发生的事情是它检查错误是否是 a 401or 403,这是常见的身份验证错误,如果是,它将刷新令牌并重试响应。我的实现refreshToken()如下所示,但这可能会因您的 API 而异:

Future<void> refreshToken() async {
    final refreshToken = await this._storage.read(key: 'refreshToken');
    final response =
        await this.api.post('/users/refresh', data: {'token': refreshToken});

    if (response.statusCode == 200) {
      this.accessToken = response.data['accessToken'];
    }
  }

Run Code Online (Sandbox Code Playgroud)

我使用Flutter Sercure Storage来存储 accessToken。我的重试方法如下所示:

  Future<Response<dynamic>> _retry(RequestOptions requestOptions) async {
    final options = new Options(
      method: requestOptions.method,
      headers: requestOptions.headers,
    );
    return this.api.request<dynamic>(requestOptions.path,
        data: requestOptions.data,
        queryParameters: requestOptions.queryParameters,
        options: options);
  }

Run Code Online (Sandbox Code Playgroud)

如果您想轻松地允许将添加access_token到请求中,我建议您在使用onError回调声明 dio 路由器时添加以下函数:

onRequest: (options) async {
          options.headers['Authorization'] = 'Bearer: $accessToken';
          return options;
        },
Run Code Online (Sandbox Code Playgroud)

  • 这个答案应该在最上面,谢谢兄弟 (2认同)

Agu*_*ana 21

我修改约翰·安德顿的答案。我同意在实际发出请求之前检查令牌是更好的方法。我们必须检查令牌是否过期,而不是发出请求并检查错误 401 和 403。

我修改了一下,增加了一些功能,这样这个拦截器就可以用了

  1. 如果访问令牌仍然有效,则将其添加到标头
  2. 如果访问令牌已过期,则重新生成访问令牌
  3. 如果刷新令牌已过期,则导航回登录页面
  4. 如果由于令牌无效(例如,被后端撤销)而出现错误,则导航回登录页面

它也适用于多个并发请求,如果您不需要将令牌添加到标头(例如在登录端点中),则该拦截器也可以处理它。这是拦截器

class AuthInterceptor extends Interceptor {
  final Dio _dio;
  final _localStorage = LocalStorage.instance; // helper class to access your local storage

  AuthInterceptor(this._dio);

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    if (options.headers["requiresToken"] == false) {
      // if the request doesn't need token, then just continue to the next interceptor
      options.headers.remove("requiresToken"); //remove the auxiliary header
      return handler.next(options);
    }

    // get tokens from local storage, you can use Hive or flutter_secure_storage
    final accessToken = _localStorage.getAccessToken();
    final refreshToken = _localStorage.getRefreshToken();

    if (accessToken == null || refreshToken == null) {
      _performLogout(_dio);

      // create custom dio error
      options.extra["tokenErrorType"] = TokenErrorType.tokenNotFound; // I use enum type, you can chage it to string
      final error = DioError(requestOptions: options, type: DioErrorType.other);
      return handler.reject(error);
    }

    // check if tokens have already expired or not
    // I use jwt_decoder package
    // Note: ensure your tokens has "exp" claim
    final accessTokenHasExpired = JwtDecoder.isExpired(accessToken);
    final refreshTokenHasExpired = JwtDecoder.isExpired(refreshToken);

    var _refreshed = true;

    if (refreshTokenHasExpired) {
      _performLogout(_dio);

      // create custom dio error
      options.extra["tokenErrorType"] = TokenErrorType.refreshTokenHasExpired;
      final error = DioError(requestOptions: options, type: DioErrorType.other);

      return handler.reject(error);
    } else if (accessTokenHasExpired) {
      // regenerate access token
      _dio.interceptors.requestLock.lock();
      _refreshed = await _regenerateAccessToken();
      _dio.interceptors.requestLock.unlock();
    }

    if (_refreshed) {
      // add access token to the request header
      options.headers["Authorization"] = "Bearer $accessToken";
      return handler.next(options);
    } else {
      // create custom dio error
      options.extra["tokenErrorType"] = TokenErrorType.failedToRegenerateAccessToken;
      final error = DioError(requestOptions: options, type: DioErrorType.other);
      return handler.reject(error);
    }
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) {
    if (err.response?.statusCode == 403 || err.response?.statusCode == 401) {
      // for some reasons the token can be invalidated before it is expired by the backend.
      // then we should navigate the user back to login page

      _performLogout(_dio);

      // create custom dio error
      err.type = DioErrorType.other;
      err.requestOptions.extra["tokenErrorType"] = TokenErrorType.invalidAccessToken;
    }

    return handler.next(err);
  }

  void _performLogout(Dio dio) {
    _dio.interceptors.requestLock.clear();
    _dio.interceptors.requestLock.lock();

    _localStorage.removeTokens(); // remove token from local storage

    // back to login page without using context
    // check this /sf/answers/3737808651/
    navigatorKey.currentState?.pushReplacementNamed(LoginPage.routeName);

    _dio.interceptors.requestLock.unlock();
  }

  /// return true if it is successfully regenerate the access token
  Future<bool> _regenerateAccessToken() async {
    try {
      var dio = Dio(); // should create new dio instance because the request interceptor is being locked

      // get refresh token from local storage
      final refreshToken = _localStorage.getRefreshToken();

      // make request to server to get the new access token from server using refresh token
      final response = await dio.post(
        "https://yourDomain.com/api/refresh",
        options: Options(headers: {"Authorization": "Bearer $refreshToken"}),
      );

      if (response.statusCode == 200 || response.statusCode == 201) {
        final newAccessToken = response.data["accessToken"]; // parse data based on your JSON structure
        _localStorage.saveAccessToken(newAccessToken); // save to local storage
        return true;
      } else if (response.statusCode == 401 || response.statusCode == 403) {
        // it means your refresh token no longer valid now, it may be revoked by the backend
        _performLogout(_dio);
        return false;
      } else {
        print(response.statusCode);
        return false;
      }
    } on DioError {
      return false;
    } catch (e) {
      return false;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

用法

  final dio = Dio();

  dio.options.baseUrl = "https://yourDomain.com/api";
  dio.interceptors.addAll([
      AuthInterceptor(dio), // add this line before LogInterceptor
      LogInterceptor(),
  ]);
Run Code Online (Sandbox Code Playgroud)

如果您的请求不需要标头中的令牌(例如在登录端点中),那么您应该像这样发出请求

await dio.post(
  "/login",
  data: loginData,
  options: Options(headers: {"requiresToken": false}), // add this line
);
Run Code Online (Sandbox Code Playgroud)

否则,只需发出常规请求,而不在 header 选项中添加 token,拦截器就会自动处理。

await dio.get("/user", data: myData);
Run Code Online (Sandbox Code Playgroud)


Dav*_*uel 11

Dio 4.0.2弃用了Interceptor锁。QueuedInterceptor应该使用。

\n

来自文档

\n
\n

拦截器的锁最初是为了同步拦截器的执行而设计的,但是锁有一个问题,一旦解锁,所有的请求都会立即运行,而不是顺序执行。现在 QueuedInterceptor 可以做得更好。

\n

QueuedInterceptor 提供了一种按顺序访问拦截器的机制。

\n
\n

AuthInterceptor使用以下实现的示例QueuedInterceptor

\n
/// Adds Authorization header with a non-expired bearer token.\n///\n/// Logic:\n/// 1. Check if the endpoint requires authentication\n///   - If not, bypass interceptor\n/// 2. Get a non-expired access token\n///   - AuthRepository takes care of refreshing the token if it is expired\n/// 3. Make API call (attaching token in Authorization header)\n/// 4. If response if 401 (e.g. a not expired access token that was revoked by backend),\n///    force refresh access token and retry call.\n///\n/// For non-authenticated endpoints add the following header to bypass this interceptor:\n/// `Authorization: None`\n///\n/// For endpoints with optional authentication provide the following header:\n/// `Authorization: Optional`\n/// - If user is not authenticated: the Authorization header will be removed\n///   and the call will be performed without it.\n/// - If the user is authenticated: the authentication token will be attached in the\n///   Authorization header.\nclass AuthInterceptor extends QueuedInterceptor {\n  AuthInterceptor({\n    required this.dio,\n    required this.authRepository,\n    this.retries = 3,\n  });\n\n  /// The original dio\n  final Dio dio;\n  final AuthRepository authRepository;\n\n  /// The number of retries in case of 401\n  final int retries;\n\n  @override\n  Future<void> onRequest(\n    final RequestOptions options,\n    final RequestInterceptorHandler handler,\n  ) async {\n    // Non-authenticated endpoint -> bypass this interceptor\n    if (options._requiresNoAuthentication()) {\n      options._removeAuthenticationHeader();\n      return handler.next(options);\n    }\n    // Get auth token\n    final authTokenRes = await authRepository.getAuthToken();\n    authTokenRes.fold(\n      success: (final authToken) {\n        // Add auth token in Authorization header\n        options._setAuthenticationHeader(authToken.token);\n        handler.next(options);\n      },\n      failure: (final e) async {\n        // Skip authentication header if it is optional and user is not authenticated\n        if (e is UserNoAuthenticatedException && options._hasOptionalAuthentication()) {\n          options._removeAuthenticationHeader();\n          return handler.next(options);\n        }\n        // Handle auth token errors\n        await _onErrorRefreshingToken();\n        final error = DioError(requestOptions: options, error: e);\n        handler.reject(error);\n      },\n    );\n  }\n\n  @override\n  Future<void> onError(final DioError err, final ErrorInterceptorHandler handler) async {\n    if (err.response?.statusCode != 401) {\n      return super.onError(err, handler);\n    }\n    // Check retry attempt\n    final attempt = err.requestOptions._retryAttempt + 1;\n    if (attempt > retries) {\n      return super.onError(err, handler);\n    }\n    err.requestOptions._retryAttempt = attempt;\n    await Future<void>.delayed(const Duration(seconds: 1));\n    // Force refresh auth token\n    final authTokenRes = await authRepository.getAuthToken(forceRefresh: true);\n    authTokenRes.fold(\n      success: (final authToken) async {\n        // Add new auth token in Authorization header and retry call\n        try {\n          final options = err.requestOptions.._setAuthenticationHeader(authToken.token);\n          final response = await dio.fetch<void>(options);\n          handler.resolve(response);\n        } on DioError catch (e) {\n          if (e.response?.statusCode == 401) {\n            await _onErrorRefreshingToken();\n          }\n          super.onError(e, handler);\n        }\n      },\n      failure: (final e) async {\n        // Handle auth token errors\n        await _onErrorRefreshingToken();\n        final error = DioError(requestOptions: err.requestOptions, error: authTokenRes.error);\n        return handler.next(error);\n      },\n    );\n  }\n\n  Future<void> _onErrorRefreshingToken() async {\n    await authRepository.signOut();\n  }\n}\n\nextension AuthRequestOptionsX on RequestOptions {\n  bool _requiresNoAuthentication() => headers[\'Authorization\'] == \'None\';\n\n  bool _hasOptionalAuthentication() => headers[\'Authorization\'] == \'Optional\';\n\n  void _setAuthenticationHeader(final String token) => headers[\'Authorization\'] = \'Bearer $token\';\n\n  void _removeAuthenticationHeader() => headers.remove(\'Authorization\');\n\n  int get _retryAttempt => (extra[\'auth_retry_attempt\'] as int?) ?? 0;\n\n  set _retryAttempt(final int attempt) => extra[\'auth_retry_attempt\'] = attempt;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

笔记:

\n
    \n
  • 就我而言,AuthRepositoryFirebaseAuth. Firebase SDK 负责在getAuthToken()调用时提供未过期的令牌。
  • \n
  • AuthRepository.getAuthToken()返回一个Future<Result<AuthToken, AuthException>>. 我的对象与ResultResult包中提供的对象类似。
  • \n
\n


Joh*_*ton 8

我认为更好的方法是在实际发出请求之前检查令牌。这样您的网络流量就会减少,响应速度也会更快。

编辑:遵循这种方法的另一个重要原因是因为它是一种更安全的方法,正如XY在评论部分中指出的那样

在我的示例中,我使用:

http: ^0.13.3
dio: ^4.0.0
flutter_secure_storage: ^4.2.0
jwt_decode: ^0.3.1
flutter_easyloading: ^3.0.0 
Run Code Online (Sandbox Code Playgroud)

这个想法是首先检查令牌的过期时间(访问和刷新)。如果刷新令牌已过期,则清除存储并重定向到登录页面。如果访问令牌已过期,则(在提交实际请求之前)使用刷新令牌刷新它,然后使用刷新的凭据提交原始请求。通过这种方式,您可以最大限度地减少网络流量,并且可以更快地获得响应。

我这样做了:

AuthService appAuth = new AuthService();

class AuthService {
  Future<void> logout() async {
    token = '';
    refresh = '';

    await Future.delayed(Duration(milliseconds: 100));

    Navigator.of(cnt).pushAndRemoveUntil(
      MaterialPageRoute(builder: (context) => LoginPage()),
      (_) => false,
    );
  }

  Future<bool> login(String username, String password) async {
    var headers = {'Accept': 'application/json'};
    var request = http.MultipartRequest('POST', Uri.parse(baseURL + 'token/'));
    request.fields.addAll({'username': username, 'password': password});
    request.headers.addAll(headers);
    http.StreamedResponse response = await request.send();

    if (response.statusCode == 200) {
      var resp = await response.stream.bytesToString();
      final data = jsonDecode(resp);
      token = data['access'];
      refresh = data['refresh'];
      secStore.secureWrite('token', token);
      secStore.secureWrite('refresh', refresh);
      return true;
    } else {
      return (false);
    }
  }

  Future<bool> refreshToken() async {
    var headers = {'Accept': 'application/json'};
    var request =
        http.MultipartRequest('POST', Uri.parse(baseURL + 'token/refresh/'));
    request.fields.addAll({'refresh': refresh});

    request.headers.addAll(headers);

    http.StreamedResponse response = await request.send();

    if (response.statusCode == 200) {
      final data = jsonDecode(await response.stream.bytesToString());
      token = data['access'];
      refresh = data['refresh'];

      secStore.secureWrite('token', token);
      secStore.secureWrite('refresh', refresh);
      return true;
    } else {
      print(response.reasonPhrase);
      return false;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

之后创建拦截器

import 'package:dio/dio.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import '../settings/globals.dart';


class AuthInterceptor extends Interceptor {
  static bool isRetryCall = false;

  @override
  void onRequest(
      RequestOptions options, RequestInterceptorHandler handler) async {
    bool _token = isTokenExpired(token);
    bool _refresh = isTokenExpired(refresh);
    bool _refreshed = true;

    if (_refresh) {
      appAuth.logout();
      EasyLoading.showInfo(
          'Expired session');
      DioError _err;
      handler.reject(_err);
    } else if (_token) {
      _refreshed = await appAuth.refreshToken();
    }
    if (_refreshed) {
      options.headers["Authorization"] = "Bearer " + token;
      options.headers["Accept"] = "application/json";

      handler.next(options);
    }
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) async {
    handler.next(response);
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) async {
    handler.next(err);
  }
}
Run Code Online (Sandbox Code Playgroud)

安全存储功能来自:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

SecureStorage secStore = new SecureStorage();

class SecureStorage {
  final _storage = FlutterSecureStorage();
  void addNewItem(String key, String value) async {
    await _storage.write(
      key: key,
      value: value,
      iOptions: _getIOSOptions(),
    );
  }

  IOSOptions _getIOSOptions() => IOSOptions(
        accountName: _getAccountName(),
      );

  String _getAccountName() => 'blah_blah_blah';

  Future<String> secureRead(String key) async {
    String value = await _storage.read(key: key);
    return value;
  }

  Future<void> secureDelete(String key) async {
    await _storage.delete(key: key);
  }

  Future<void> secureWrite(String key, String value) async {
    await _storage.write(key: key, value: value);
  }
}
Run Code Online (Sandbox Code Playgroud)

检查过期时间:

bool isTokenExpired(String _token) {
  DateTime expiryDate = Jwt.getExpiryDate(_token);
  bool isExpired = expiryDate.compareTo(DateTime.now()) < 0;
  return isExpired;
}
Run Code Online (Sandbox Code Playgroud)

然后是原始请求

var dio = Dio();

Future<Null> getTasks() async {
EasyLoading.show(status: 'Wait ...');
    
Response response = await dio
    .get(baseURL + 'tasks/?task={"foo":"1","bar":"30"}');
    
if (response.statusCode == 200) {
    print('success');
} else {
    print(response?.statusCode);
}}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,Login 和refreshToken 请求使用http 包(它们不需要拦截器)。getTasks 使用 dio 及其拦截器,以便在一个且唯一的请求中获取其响应

  • 你的答案是正确的。检查401/403并刷新的逻辑是错误的。由于数据泄露,后端可以决定使令牌无效,客户端在看到 401/403 后将需要注销应用程序。 (2认同)
  • 这应该是公认的答案 (2认同)
  • @JohnAnderton 这很好,但我认为您还应该添加代码来处理无效令牌,我的意思是由于某些原因,令牌可以在过期之前从后端失效,我们应该将用户返回到登录页面。也许我们应该在 AuthInterceptor 的 onError 方法中通过检查状态代码(401 / 403)来处理它 (2认同)

Gur*_*urt 7

迪奥4.0.0

dio.interceptors.clear();
dio.interceptors.add(
      InterceptorsWrapper(
        onRequest: (request, handler) {
          if (token != null && token != '')
            request.headers['Authorization'] = 'Bearer $token';
          return handler.next(request);
        },
        onError: (err, handler) async {
          if (err.response?.statusCode == 401) {
            try {
              await dio
                  .post(
                      "https://refresh.api",
                      data: jsonEncode(
                          {"refresh_token": refreshtoken}))
                  .then((value) async {
                if (value?.statusCode == 201) {
                  //get new tokens ...
                  print("acces token" + token);
                  print("refresh token" + refreshtoken);
                  //set bearer
                  err.requestOptions.headers["Authorization"] =
                      "Bearer " + token;
                  //create request with new access token
                  final opts = new Options(
                      method: err.requestOptions.method,
                      headers: err.requestOptions.headers);
                  final cloneReq = await dio.request(err.requestOptions.path,
                      options: opts,
                      data: err.requestOptions.data,
                      queryParameters: err.requestOptions.queryParameters);

                  return handler.resolve(cloneReq);
                }
                return err;
              });
              return dio;
            } catch (err, st) {
              
            }
          }
       },
    ),
);
Run Code Online (Sandbox Code Playgroud)