【问题标题】:Using Interceptor in Dio for Flutter to Refresh Token使用 Dio 中的 Interceptor for Flutter 刷新 Token
【发布时间】:2019-11-06 12:14:12
【问题描述】:

我正在尝试在颤振中使用带有 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;
  }

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

【问题讨论】:

    标签: http flutter dart firebase-authentication


    【解决方案1】:

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

    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;
            }));
    
    

    基本上,它会检查错误是401 还是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'];
        }
      }
    
    

    我使用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);
      }
    
    

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

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

    【讨论】:

    • 这个答案应该放在首位,谢谢兄弟
    • @iamnabink 谢谢,很高兴你发现它有帮助!
    • 如何更新刷新令牌,例如用户关闭应用程序并在 4 小时后返回?
    • @reza47 accessToken 应该大约 15 分钟到期。您的 refreshToken 不应过期,如果过期,则应每隔几周左右过期一次。 refreshToken 方法获取一个新的 accessToken,但是如果你的 refreshToken 过期了,你应该重新登录。
    【解决方案2】:

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

      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;
      }
    

    Dio 4.0.0 支持

        dio.interceptors.add(
              InterceptorsWrapper(
                onRequest: (request, handler) {
                  if (token != null && token != '')
                    request.headers['Authorization'] = 'Bearer $token';
                  return handler.next(request);
                },
                onError: (e, handler) async {
                  if (e.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("access token" + token);
                          print("refresh token" + refreshtoken);
                          //set bearer
                          e.requestOptions.headers["Authorization"] =
                              "Bearer " + token;
                          //create request with new access token
                          final opts = new Options(
                              method: e.requestOptions.method,
                              headers: e.requestOptions.headers);
                          final cloneReq = await dio.request(e.requestOptions.path,
                              options: opts,
                              data: e.requestOptions.data,
                              queryParameters: e.requestOptions.queryParameters);
        
                          return handler.resolve(cloneReq);
                        }
                        return e;
                      });
                      return dio;
                    } catch (e, st) {
                      
                    }
                  }
               },
            ),
        );
    

    【讨论】:

    • 这对你有用吗?我拦截 401 然后发送请求以获取新令牌,如果我锁定 requestLock 和 responseLock 刷新请求未完成,我不得不删除这两行
    • @demsey 看到我正在使用 firebase auth 生成令牌,您的用例可能与我的不同。
    • @demsey 我认为在您的情况下,您可以使用新实例来获取令牌,并继续使用旧实例进行正常请求/响应
    • 这里是不是也需要errorLock,否则错误响应会不断返回?
    • 如果总是再次返回请求有无限循环的风险
    【解决方案3】:

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

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

    在我的示例中,我使用:

    http: ^0.13.3
    dio: ^4.0.0
    flutter_secure_storage: ^4.2.0
    jwt_decode: ^0.3.1
    flutter_easyloading: ^3.0.0 
    

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

    我这样做了:

    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;
        }
      }
    }
    

    然后创建拦截器

    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);
      }
    }
    

    安全存储功能来自:

    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);
      }
    }
    

    检查到期日期:

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

    然后是原始请求

    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);
    }}
    

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

    【讨论】:

    • 这只会在token是JWT格式的情况下有效,但是token的格式并不是每个人都一样。
    • @r4jiv007 你是对的。校长保持不变;如果您可以在客户端检查令牌的到期情况,这是一个可行的解决方案
    • 你的答案是正确的。检查401/403并刷新的逻辑是错误的。由于数据泄露,后端可以决定使令牌无效,客户端一旦看到 401/403 就需要退出应用程序。
    • 这应该是公认的答案
    • @JohnAnderton 这很好,但我认为您还应该添加代码来处理无效令牌,我的意思是由于某些原因,令牌可以在过期之前从后端失效,我们应该将用户重新设置为登录页面。也许我们应该在 AuthInterceptor 的 onError 方法中通过检查状态码 (401 / 403) 来处理它
    【解决方案4】:

    我还没有完成自定义 Http 客户端的实现,但它可以帮助你。

    Custom Http

    Interceptor

    Main

    【讨论】:

    • 没有 Dio 4+ 的人的好例子。谢谢。
    • 知道如何注销()吗?从拦截器导航回登录页面的最佳做法是什么?
    • 回答我自己的问题。这里我使用了 OneContext pub.dev/packages/one_context 。我不确定这是否是最好的解决方案,但它确实有效,而且我还没有注意到任何性能变化。
    【解决方案5】:

    迪奥 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) {
                  
                }
              }
           },
        ),
    );
    

    【讨论】:

      【解决方案6】:

      我修改John Anderton's answer。我同意在您实际提出请求之前检查令牌是更好的方法。我们必须检查令牌是否过期,而不是发出请求并检查错误 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 https://stackoverflow.com/a/53397266/9101876
          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;
          }
        }
      }
      

      用法

        final dio = Dio();
      
        dio.options.baseUrl = "https://yourDomain.com/api";
        dio.interceptors.addAll([
            AuthInterceptor(dio), // add this line before LogInterceptor
            LogInterceptor(),
        ]);
      

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

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

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

      await dio.get("/user", data: myData);
      

      【讨论】:

      • 这个方案是否可以处理多个需要token的并发请求?
      • @akifarhan 是的,因为如果令牌过期,我会锁定请求
      【解决方案7】:

      您将收到一个响应状态代码为 401 以表示令牌到期。为了请求新的访问令牌,您需要使用 post 方法以及 form data 和所需的 Dio 的 options(内容类型和标题)。下面是如何请求新令牌的代码。

      请求成功后,如果您得到响应状态码为 200,那么您将获得新的访问令牌值以及刷新令牌值,并将它们保存在您喜欢使用的任何存储中。例如,共享偏好。

      一旦您保存了新的访问令牌,您就可以使用下面相同代码中显示的 get 方法使用它来获取数据。

      onError(DioError error) async {
          if (error.response?.statusCode == 401) {
            Response response;
            var authToken = base64
                .encode(utf8.encode("username_value" + ":" + "password_value"));
            FormData formData = new FormData.from(
                {"grant_type": "refresh_token", "refresh_token": refresh_token_value});
            response = await dio.post(
              url,
              data: formData,
              options: new Options(
                  contentType: ContentType.parse("application/x-www-form-urlencoded"),
                  headers: {HttpHeaders.authorizationHeader: 'Basic $authToken'}),
            );
            if (response.statusCode == 200) {
              response = await dio.get(
                url,
                options: new Options(headers: {
                  HttpHeaders.authorizationHeader: 'Bearer access_token_value'
                }),
              );
              return response;
            } else {
              print(response.data);
              return null;
            }
          }
          return error;
        }
      

      【讨论】:

        【解决方案8】:

        下面是我的拦截器的一个 sn-p

         dio.interceptors
                .add(InterceptorsWrapper(onRequest: (RequestOptions options) async {
        
        /* Write your request logic setting your Authorization header from prefs*/
        
              String token = await prefs.accessToken;
              if (token != null) {
                options.headers["Authorization"] = "Bearer " + token;
              return options; //continue
            }, onResponse: (Response response) async {
        // Write your response logic
        
              return response; // continue
            }, onError: (DioError dioError) async {
        
              // Refresh Token
              if (dioError.response?.statusCode == 401) {
                Response response;
                var data = <String, dynamic>{
                  "grant_type": "refresh_token",
                  "refresh_token": await prefs.refreshToken,
                  'email': await prefs.userEmail
                };
                response = await dio
                    .post("api/url/for/refresh/token", data: data);
                if (response.statusCode == 200) {
                  var newRefreshToken = response.data["data"]["refresh_token"]; // get new refresh token from response
                  var newAccessToken = response.data["data"]["access_token"]; // get new access token from response
                  prefs.refreshToken = newRefreshToken;
                  prefs.accessToken = newAccessToken; // to be used in the request section of the interceptor
                  return dio.request(dioError.request.baseUrl + dioError.request.path,
                      options: dioError.request);
                }
              }
              return dioError;
            }));
            return dio;
          }
        }
        

        【讨论】:

          【解决方案9】:

          它正在 100% 工作

           RestClient client;
                  
                    static BaseOptions options = new BaseOptions(
                      connectTimeout: 5000,
                      receiveTimeout: 3000,
                    );
                    RemoteService() {
                      // or new Dio with a BaseOptions instance.
                      final dio = Dio(options);
                  
                      dio.interceptors
                          .add(InterceptorsWrapper(onRequest: (RequestOptions options) async {
                        // Do something before request is sent
                        return options; //continue
                      }, onResponse: (Response response) async {
                        // Do something with response data
                        return response; // continue
                      }, onError: (DioError error) async {
                        // Do something with response error
                        if (error.response.statusCode == 401) {
                          Response response =
                              await dio.post("http://addrees-server/oauth/token",
                                  options: Options(
                                    headers: {
                                      'Authorization': ApiUtils.BASIC_TOKEN,
                                      'Content-Type': ApiUtils.CONTENT_TYPE,
                                    },
                                  ),
                                  queryParameters: {
                                "grant_type": ApiUtils.GRANT_TYPE,
                                "username": AppConstants.LOGIN,
                                "password": AppConstants.PASSWORD
                              });
                  
                          Sessions.access_token = response.data['access_token'];
                  
                          error.response.request.queryParameters
                              .update('access_token', (value) => Sessions.access_token);
                          RequestOptions options = error.response.request;
                  
                          return dio.request(options.path, options: options); //continue
                        } else {
                          return error;
                        }
                      }));
                      client = RestClient(dio);
                    }
          

          【讨论】:

          • 如何处理并发调用?
          • 你好,你是什么意思,请举个例子。
          猜你喜欢
          • 2022-11-16
          • 1970-01-01
          • 2020-11-02
          • 2021-09-29
          • 2021-08-03
          • 2019-05-02
          • 2021-01-24
          • 2019-03-17
          • 2020-04-19
          相关资源
          最近更新 更多