Flutter network request framework Dio source code analysis and packaging (2)--Cookie management analysis

Flutter network request framework Dio source code analysis and encapsulation--Cookie management analysis

foreword

In the previous article, we briefly analyzed the general workflow of Dio when it made a request. This is only the most basic function of Dio, and we have not analyzed the content after httpClientAdapter. But don’t worry, this time we will continue with the previous content and look at the problem of cookie management in Dio, because we encountered this problem in the project before, and we will analyze it from the source code point of view, which is a review.

question

The problem encountered before is this: the login interface will return a cookie to the client after successful login, which is included in the "
set-cookie" Headers attribute of Response. Later, when some interfaces are called on the client side, the background needs to verify the cookie, so the cookie needs to be stored in the memory or even the hard disk, and uploaded to the background in the request header, otherwise the background will report a request failure.
Since the previous projects basically used tokens for interface authentication, this method of cookie persistence is a bit strange to me, but in the end I can still think of using interceptors to do this. Dio allows us to customize the interceptor and adjust the request and returned parameters. In the onResponse callback method, we get the Cookie from the headers in the Response, and then add it to the RequestOptions in the onRequest. The principle is very simple, but many details were not handled well when I implemented it myself before, resulting in code redundancy and not robust enough.
In fact, Dio provides a supporting Plugin: dio_cookie_manager to help us manage cookies, and it is very convenient to use. So this time we will take a look at how it handles these problems.

how to use

First of all, we need to import the cookie_manager library, based on the version 2.0.0 used in the project,
which indirectly depends on the version cookie_jar3.0.0

dependencies:
  dio_cookie_manager: ^2.0.0
  cookie_jar: ^3.0.0
  final dio = Dio();
  final cookieJar = CookieJar();
  dio.interceptors.add(CookieManager(cookieJar));

As shown in the example, we need to construct an instance of CookieJar, then pass it into the constructor of CookieManager, and finally add it to Dio's interceptor list.

CookieJar

Let's take a look at the CookieJar class first:

/// CookieJar is a cookie manager for http requests。
abstract class CookieJar {
    
    
  factory CookieJar({
    
    bool ignoreExpires = false}) {
    
    
    return DefaultCookieJar(ignoreExpires: ignoreExpires);
  }
  /// Save the cookies for specified uri.
  Future<void> saveFromResponse(Uri uri, List<Cookie> cookies);
  /// Load the cookies for specified uri.
  Future<List<Cookie>> loadForRequest(Uri uri);
  Future<void> deleteAll();
  Future<void> delete(Uri uri, [bool withDomainSharedCookie = false]);
  final bool ignoreExpires = false;
}

CookieJar is an abstract class, and finally the constructor of the DefaultCookieJar class is called

/// [DefaultCookieJar] is a default cookie manager which implements the standard
/// cookie policy declared in RFC. [DefaultCookieJar] saves the cookies in RAM, so if the application
/// exit, all cookies will be cleared.
class DefaultCookieJar implements CookieJar {
    
    
  /// [ignoreExpires]: save/load even cookies that have expired.
  DefaultCookieJar({
    
    this.ignoreExpires = false});
  ...
 }

DefaultCookieJar is a default implementation of CookieJar, which parses cookies from Request and Response in http format and saves them in Ram.

CookieManager

Next, let's take a look at the CookieManager class:

  /// Don't use this class in Browser environment
class CookieManager extends Interceptor {
    
    
  final CookieJar cookieJar;
  CookieManager(this.cookieJar);
```、
```dart
  
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    
    
    cookieJar.loadForRequest(options.uri).then((cookies) {
    
    
      var cookie = getCookies(cookies);
      if (cookie.isNotEmpty) {
    
    
        options.headers[HttpHeaders.cookieHeader] = cookie;
      }
      handler.next(options);
    }).catchError((e, stackTrace) {
    
    
      var err = DioError(requestOptions: options, error: e);
      err.stackTrace = stackTrace;
      handler.reject(err, true);
    });
  }
  static String getCookies(List<Cookie> cookies) {
    
    
    return cookies.map((cookie) => '${
      
      cookie.name}=${
      
      cookie.value}').join('; ');
  }

After the constructed cookieJar is passed in, call the loadForRequest method in the onRequest method to obtain the formatted cookies, convert them into the format required by the request and set them in the request header, and then continue to execute the logic of the interceptor below.

  
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    
    
    _saveCookies(response)
        .then((_) => handler.next(response))
        .catchError((e, stackTrace) {
    
    
      var err = DioError(requestOptions: response.requestOptions, error: e);
      err.stackTrace = stackTrace;
      handler.reject(err, true);
    });
  }
  Future<void> _saveCookies(Response response) async {
    
    
    var cookies = response.headers[HttpHeaders.setCookieHeader];
    if (cookies != null) {
    
    
      await cookieJar.saveFromResponse(
        response.requestOptions.uri,
        cookies.map((str) => Cookie.fromSetCookieValue(str)).toList(),
      );
    }
  }

The onResponse is similar. After getting the cookie, call saveFromResponse to format and save it, and then continue to execute the logic of the interceptor below.
Of course, the normal order should be to get the Cookie in onResponse first and then use it in onResponse.

PersistCookieJar

If you want to persist cookies, you can consider using PersistCookieJar:

    final Directory appDocDir = await getApplicationDocumentsDirectory();
    final String appDocPath = appDocDir.path;
    final jar = PersistCookieJar(
      ignoreExpires: true,
      storage: FileStorage(appDocPath + "/.cookies/"),
    );
    dio!.interceptors.add(CookieManager(jar));
/// [PersistCookieJar] is a cookie manager which implements the standard
/// cookie policy declared in RFC. [PersistCookieJar]  persists the cookies in files,
/// so if the application exit, the cookies always exist unless call [delete] explicitly.
class PersistCookieJar extends DefaultCookieJar {
    
    
  ///
  /// [persistSession]: Whether persisting the cookies that without
  /// "expires" or "max-age" attribute;
  /// If false, the session cookies will be discarded;
  /// otherwise, the session cookies will be persisted.
  ///
  /// [ignoreExpires]: save/load even cookies that have expired.
  ///
  /// [storage]: Defaults to FileStorage

  PersistCookieJar(
      {
    
    this.persistSession = true,
      bool ignoreExpires = false,
      Storage? storage})
      : super(ignoreExpires: ignoreExpires) {
    
    
    this.storage = storage ?? FileStorage();
  }

In order to achieve persistence, a Storage class is introduced. The default implementation is FileStorage, file storage, and other methods can also be implemented by yourself.

  
  Future<List<Cookie>> loadForRequest(Uri uri) async {
    
    
    await _checkInitialized();
    await _load(uri);
    return super.loadForRequest(uri);
  }

PersistCookieJar finally calls the loadForRequest method of DefaultCookieJar, but two methods are executed before that, let's look at them one by one:

  Future<void> _checkInitialized({
    
    bool force = false}) async {
    
    
    if (force || !_initialized) {
    
    
      await storage.init(persistSession, ignoreExpires);
      // Load domain cookies
      var str = await storage.read(DomainsKey);
      ...
      }

Executed the init and read methods of storage

  
  Future<void> init(bool persistSession, bool ignoreExpires) async {
    
    
    _curDir = dir ?? './.cookies/';
    if (!_curDir!.endsWith('/')) {
    
    
      _curDir = _curDir! + '/';
    }
    _curDir = _curDir! + 'ie${
      
      ignoreExpires ? 1 : 0}_ps${
      
      persistSession ? 1 : 0}/';
    await _makeCookieDir();
  }
  Future<void> _makeCookieDir() async {
    
    
    final directory = Directory(_curDir!);
    if (!directory.existsSync()) {
    
    
      await directory.create(recursive: true);
    }
  }

The init method is mainly to check whether the incoming save directory exists, and create it if it does not exist.
The read method and the load method respectively read the data in the file and store it in the memory, domainCookies and hostCookies

  Future<void> _load(Uri uri) async {
    
    
    final host = uri.host;
    if (_hostSet.contains(host) && hostCookies[host] == null) {
    
    
      var str = await storage.read(host);
  
  Future<void> saveFromResponse(Uri uri, List<Cookie> cookies) async {
    
    
    await _checkInitialized();
    if (cookies.isNotEmpty) {
    
    
      await super.saveFromResponse(uri, cookies);
      if (cookies.every((Cookie e) => e.domain == null)) {
    
    
        await _save(uri);
      } else {
    
    
        await _save(uri, true);
      }
    }
  }

First check whether there is a save directory, and then see if cookies are passed in. If so, directly call the saveFromResponse of DefaultCookieJar, and then call the _save method:

  Future<void> _save(Uri uri, [bool withDomainSharedCookie = false]) async {
    
    
    final host = uri.host;
    if (!_hostSet.contains(host)) {
    
    
      _hostSet.add(host);
      await storage.write(IndexKey, json.encode(_hostSet.toList()));
    }
    final cookies = hostCookies[host];
    if (cookies != null) {
    
    
      await storage.write(host, json.encode(_filter(cookies)));
    }
    if (withDomainSharedCookie) {
    
    
      var filterDomainCookies =
          domainCookies.map((key, value) => MapEntry(key, _filter(value)));
      await storage.write(DomainsKey, json.encode(filterDomainCookies));
    }
  }

According to the cookie level, it is written to the file in sequence.

Summarize

The entire workflow of CookieManager and CookieJar is basically analyzed. It can be seen that the entire process is very clear, the code is also very concise, and the robustness is also very good. This source code learning has given me a deeper understanding of code writing. So, next time, let's take a look at how to package Dio, so that we can use it more conveniently.

Guess you like

Origin blog.csdn.net/Yaoobs/article/details/131174589