android fetch add custom cookie

[TOC]

0x00 为React Native的网络请求添加公共Cookie

本文分析基于React Native 0.44版本分析。

由于我们在使用React Native编写应用时,内部的网络请求均使用了fetch函数,所以下面我们主要分析一下fetch函数的整个调用流程。

0x01 Fetch追根溯源

从React Native的源代码我们可以知道fetch函数最终也是由native端的NetworkingModule.java(Android)或RCTNetworking.mm(iOS)实现。

1. 其调用流程如下

2. 代码分析如下

1. 将fetch函数添加到全局变量

react-native/Libraries/Core/InitializeCore.js的全部变量global中定义了fetch函数。

// Set up XHR
// The native XMLHttpRequest in Chrome dev tools is CORS aware and won't
// let you fetch anything from the internet
defineProperty(global, 'XMLHttpRequest', () => require('XMLHttpRequest'));
defineProperty(global, 'FormData', () => require('FormData'));

defineProperty(global, 'fetch', () => require('fetch').fetch);
defineProperty(global, 'Headers', () => require('fetch').Headers);
defineProperty(global, 'Request', () => require('fetch').Request);
defineProperty(global, 'Response', () => require('fetch').Response);
defineProperty(global, 'WebSocket', () => require('WebSocket'));

2. fetch函数的具体实现

扒取fetch的源代码,我们可以发现fetch是由XMLHttpRequest实现,并调用XMLHttpRequest.send()函数发起请求。

同时react native将fetchcredentials:'include'属性转变成了XMLHttpRequestwithCredentials属性,所以下一步,我们只需要关注XMLHttpRequestwithCredentials如何处理即可。

react-native/Libraries/Network/fetch.js

'use strict';

import 'whatwg-fetch';

module.exports = {fetch, Headers, Request, Response};

react-native/node_modules/whatwg-fetch/fetch.js

self.fetch = function(input, init) {
    return new Promise(function(resolve, reject) {
      var request = new Request(input, init)
      var xhr = new XMLHttpRequest()

      xhr.onload = function() {
        var options = {
          status: xhr.status,
          statusText: xhr.statusText,
          headers: parseHeaders(xhr.getAllResponseHeaders() || '')
        }
        options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL')
        var body = 'response' in xhr ? xhr.response : xhr.responseText
        resolve(new Response(body, options))
      }

      xhr.onerror = function() {
        reject(new TypeError('Network request failed'))
      }

      xhr.ontimeout = function() {
        reject(new TypeError('Network request failed'))
      }

      xhr.open(request.method, request.url, true)

      if (request.credentials === 'include') {
        xhr.withCredentials = true
      }

      if ('responseType' in xhr && support.blob) {
        xhr.responseType = 'blob'
      }

      request.headers.forEach(function(value, name) {
        xhr.setRequestHeader(name, value)
      })

      xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit)
    })
  }

3. XMLHttpRequest具体实现

XMLHttpRequest中将withCredentials作为参数传递给RCTNetworking.sendRequest()发起网络请求。

react-native/Libraries/Network/XMLHttpRequest.js

send(data: any): void {
    if (this.readyState !== this.OPENED) {
      throw new Error('Request has not been opened');
    }
    if (this._sent) {
      throw new Error('Request has already been sent');
    }
    this._sent = true;
    const incrementalEvents = this._incrementalEvents ||
      !!this.onreadystatechange ||
      !!this.onprogress;

    this._subscriptions.push(RCTNetworking.addListener(
      'didSendNetworkData',
      (args) => this.__didUploadProgress(...args)
    ));
    this._subscriptions.push(RCTNetworking.addListener(
      'didReceiveNetworkResponse',
      (args) => this.__didReceiveResponse(...args)
    ));
    this._subscriptions.push(RCTNetworking.addListener(
      'didReceiveNetworkData',
      (args) => this.__didReceiveData(...args)
    ));
    this._subscriptions.push(RCTNetworking.addListener(
      'didReceiveNetworkIncrementalData',
      (args) => this.__didReceiveIncrementalData(...args)
    ));
    this._subscriptions.push(RCTNetworking.addListener(
      'didReceiveNetworkDataProgress',
      (args) => this.__didReceiveDataProgress(...args)
    ));
    this._subscriptions.push(RCTNetworking.addListener(
      'didCompleteNetworkResponse',
      (args) => this.__didCompleteResponse(...args)
    ));

    let nativeResponseType = 'text';
    if (this._responseType === 'arraybuffer' || this._responseType === 'blob') {
      nativeResponseType = 'base64';
    }

    invariant(this._method, 'Request method needs to be defined.');
    invariant(this._url, 'Request URL needs to be defined.');
    RCTNetworking.sendRequest(
      this._method,
      this._trackingName,
      this._url,
      this._headers,
      data,
      nativeResponseType,
      incrementalEvents,
      this.timeout,
      this.__didCreateRequest.bind(this),
      this.withCredentials
    );
  }

接下来重头戏来了,RCTNetworking在android和iOS两端的实现并不一致,在iOS端,发送网络请求时是包含withCredentials这个参数的,但是在android端withCredentials这个参数会被忽略(因为在RCTNetworking.android.js中定义sendRequest函数时并没有withCredentials这个参数,所以即使调用放传递了这个参数,也会被忽略)。

也就是说在iOS端使用fetch发送请求时,可以通过配置属性,灵活决定是否发送cookie;而在android端则会一直发送cookie issue: withCredentials flag in XHRs should default to “true”

react-native/Libraries/Network/RCTNetworking.ios.js

  sendRequest(
    method: string,
    trackingName: string,
    url: string,
    headers: Object,
    data: RequestBody,
    responseType: 'text' | 'base64',
    incrementalUpdates: boolean,
    timeout: number,
    callback: (requestId: number) => any,
    withCredentials: boolean
  ) {
    const body = convertRequestBody(data);
    RCTNetworkingNative.sendRequest({
      method,
      url,
      data: {...body, trackingName},
      headers,
      responseType,
      incrementalUpdates,
      timeout,
      withCredentials
    }, callback);
  }

react-native/Libraries/Network/RCTNetworking.android.js

    sendRequest(
    method: string,
    trackingName: string,
    url: string,
    headers: Object,
    data: RequestBody,
    responseType: 'text' | 'base64',
    incrementalUpdates: boolean,
    timeout: number,
    callback: (requestId: number) => any
  ) {
    const body = convertRequestBody(data);
    if (body && body.formData) {
      body.formData = body.formData.map((part) => ({
        ...part,
        headers: convertHeadersMapToArray(part.headers),
      }));
    }
    const requestId = generateRequestId();
    RCTNetworkingNative.sendRequest(
      method,
      url,
      requestId,
      convertHeadersMapToArray(headers),
      {...body, trackingName},
      responseType,
      incrementalUpdates,
      timeout
    );
    callback(requestId);
  }

4. Android端网络具体实现

同样,我们可以看到在com.facebook.react.modules.network.NetworkingModule.java中也没有withCredentials相关处理,故而如果要保持android和iOS两端行为一致,则必须同时修改android+javascript两端代码才可以。


@Override
public void initialize() {
	mCookieJarContainer.setCookieJar(new JavaNetCookieJar(mCookieHandler));
}

public void sendRequest(
  final ExecutorToken executorToken,
  String method,
  String url,
  final int requestId,
  ReadableArray headers,
  ReadableMap data,
  final String responseType,
  final boolean useIncrementalUpdates,
  int timeout) {
  
	Request.Builder requestBuilder = new Request.Builder().url(url);
	
	if (requestId != 0) {
	  requestBuilder.tag(requestId);
	}
	
	// 略...
}

5. iOS端网络具体实现

可以看到在react-native/Libraries/Network/RCTNetworking.mm文件中对withCredentials进行了单独处理。

- (RCTURLRequestCancellationBlock)buildRequest:(NSDictionary<NSString *, id> *)query
                                 completionBlock:(void (^)(NSURLRequest *request))block
{
  RCTAssertThread(_methodQueue, @"buildRequest: must be called on method queue");

  NSURL *URL = [RCTConvert NSURL:query[@"url"]]; // this is marked as nullable in JS, but should not be null
  NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
  request.HTTPMethod = [RCTConvert NSString:RCTNilIfNull(query[@"method"])].uppercaseString ?: @"GET";
  request.allHTTPHeaderFields = [self stripNullsInRequestHeaders:[RCTConvert NSDictionary:query[@"headers"]]];
  request.timeoutInterval = [RCTConvert NSTimeInterval:query[@"timeout"]];
  request.HTTPShouldHandleCookies = [RCTConvert BOOL:query[@"withCredentials"]];

  // 略...
  
  }];
}

0x02 Android端添加公共Cookie

从上面的分析,我们知道android端使用NetworkingModule实现网络请求,同时在NetworkingModule初始化的时候添加了CookieJar来处理Cookie,所以如果我们要添加Cookie,就必须从CookieJar入手。由于我们并不想修改React Native的源代码,所以可以通过在打包时修改字节码的方式来达到同样的效果,这里我们使用aspect,用自定义CookieJar替换NetworkingModule原始的CookieJar达到添加公共cookie的目的.

首先,NetworkingModule是在initialize()的时候初始化的CookieJar,所以我们将该方法作为我们的切入点,大致代码如下:

1. 添加aspect依赖:

dependencies {
    classpath fileTree(dir:'plugins', include:['*.jar'])
    //don't lost dependency
    classpath 'org.aspectj:aspectjtools:1.8.+'
}

2. 添加切面:


@Aspect
public class NetworkingModuleAspect {

    @Around("execution (* com.facebook.react.modules.network.NetworkingModule.initialize())")
    public Object injectCustomCookie(ProceedingJoinPoint joinPoint) throws Throwable {
        Object networkingModule = joinPoint.getTarget();

        CookieJarContainer cookieJarContainer = (CookieJarContainer) getFieldValue(networkingModule, "mCookieJarContainer");
        CookieHandler cookieHandler = (CookieHandler) getFieldValue(networkingModule, "mCookieHandler");

        cookieJarContainer.setCookieJar(new JavaNetCookieJarWithCommonHeader(cookieHandler));

        return null;
    }

}

自定义CookieJar,在loadForRequest()方法中添加公共Cookie,大致代码如下:


public static final class JavaNetCookieJarWithCommonHeader implements CookieJar {
    // 略...

    @Override
    public List<Cookie> loadForRequest(HttpUrl url) {
        // The RI passes all headers. We don't have 'em, so we don't pass 'em!
        List<Cookie> cookies = new ArrayList<Cookie>();

		  // add common cookie.
        Map<String, String> commonHeaders = CommonHeaderUtils.getInstance(null).get(url.toString());
        if (commonHeaders != null && !commonHeaders.isEmpty()) {
            for (Map.Entry<String, String> entry : commonHeaders.entrySet()) {
                if (entry.getKey() == null) {
                    continue;
                }

                cookies.add(new Cookie.Builder()
                        .name(entry.getKey())
                        .value(entry.getValue() == null ? "" : entry.getValue())
                        .domain(url.host())
                        .build());
            }
        }

        Map<String, List<String>> headers = Collections.emptyMap();
        Map<String, List<String>> cookieHeaders;
        try {
            cookieHeaders = cookieHandler.get(url.uri(), headers);
        } catch (IOException e) {
            Platform.get().log(WARN, "Loading cookies failed for " + url.resolve("/..."), e);
            return cookies;
        }

        for (Map.Entry<String, List<String>> entry : cookieHeaders.entrySet()) {
            String key = entry.getKey();
            if (("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key))
                    && !entry.getValue().isEmpty()) {
                for (String header : entry.getValue()) {
                    if (cookies == null) cookies = new ArrayList<>();
                    cookies.addAll(decodeHeaderAsJavaNetCookies(url, header));
                }
            }
        }

        return cookies != null
                ? Collections.unmodifiableList(cookies)
                : Collections.<Cookie>emptyList();
    }
}

3. 配置aspect插件:

buildscript {
    repositories {
        mavenLocal()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.3'
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.10'

    }
}

在Application项目中配置aspect插件:

apply plugin: 'android-aspectjx'
aspectjx {
	//includes the libs that you want to weave
    includeJarFilter '自己切面所在的库', 'react-native'
    
    //excludes the libs that you don't want to weave
	excludeJarFilter 'universal-image-loader'
}

关于aspectj插件的具体配置,可以参考android aspectjx plugin