[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将fetch
的credentials:'include'
属性转变成了XMLHttpRequest
的withCredentials
属性,所以下一步,我们只需要关注XMLHttpRequest
中withCredentials
如何处理即可。
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。