HTTP 请求包含用户代理或内容类型等标头。除了浏览器附加的标头之外,Android 应用还可以通过 EXTRA_HEADERS Intent extra 添加额外的标头,例如 Cookie 或 Referrer。出于安全考虑,Chrome 会根据 intent 的启动方式和位置过滤一些额外的标头。
由于客户端和服务器不属于同一方,因此跨源请求需要额外的安全层。本指南将讨论如何通过 Chrome 自定义标签页启动此类请求,即从应用启动的在浏览器标签页中打开网址的 intent。在 Chrome 83 之前,开发者可以在启动自定义标签页时添加任何标头。从版本 83 开始,Chrome 开始过滤除已列入许可名单的跨源标头之外的所有标头,因为未列入许可名单的标头存在安全风险。从 Chrome 86 开始,当服务器和客户端通过数字资产链接相关联时,可以将非许可列表中的标头附加到跨源请求。下表总结了此行为:
| Chrome 版本 | 允许的 CORS 标头 |
|---|---|
| Chrome 83 之前的版本 | 已获批列出、未获批列出 |
| Chrome 83 至 Chrome 85 | 已列入许可名单 |
| 自 Chrome 86 起 | 设置数字资产链接时,approvelisted、non-approvelisted |
表 1:过滤未列入许可名单的 CORS 标头。
本文介绍了如何设置服务器与客户端之间的已验证连接,并使用该连接发送已列入许可名单以及未列入许可名单的 HTTP 标头。您可以跳到向自定义标签页 intent 添加额外标头部分查看代码。
背景
已批准列入名单的 CORS 请求标头与未批准列入名单的 CORS 请求标头
跨域资源共享 (CORS) 可让一个来源的 Web 应用请求另一个来源的资源。CORS 批准名单中的标头列表维护在 HTML 标准中。下表显示了 approvelisted 标头的示例:
| 标题 | 说明 |
|---|---|
| accept-language | 宣传客户能理解的自然语言 |
| content-language | 描述了面向当前受众群体的语言 |
| content-type | 表示资源的媒体类型 |
表 2:已列入许可名单的 CORS 标头示例。
已获批准的标头被认为是安全的,因为它们不包含敏感的用户信息,并且不太可能导致服务器执行可能造成损害的操作。
下表显示了非许可名单标头的示例:
| 标题 | 说明 |
|---|---|
| 不记名令牌 | 在服务器上对客户端进行身份验证 |
| 源 | 指明请求来源 |
| 曲奇 | 包含服务器设置的 Cookie |
表 3:非许可名单中的 CORS 标头示例。
HTML 标准不建议将非许可名单中的标头附加到 CORS 请求,并且服务器会假定跨源请求仅包含许可名单中的标头。从跨源网域发送非许可名单中的标头,将允许恶意第三方应用精心设计标头,滥用 Chrome(或其他浏览器)存储并附加到请求中的用户 Cookie。这些 Cookie 可以验证恶意服务器交易,而这些交易在其他情况下是不可能发生的。
将 CORS 许可名单中的标头附加到自定义标签页请求
自定义标签页是一种在自定义浏览器标签页中启动网页的特殊方式。可以使用 CustomTabsIntent.Builder() 创建自定义标签页 intent。您还可以使用带有 Browser.EXTRA_HEADERS 标志的 Bundle 将标头附加到这些 intent:
CustomTabsIntent intent = new CustomTabsIntent.Builder(session).build();
Bundle headers = new Bundle();
headers.putString("bearer-token", "Some token");
headers.putString("redirect-url", "Some redirect url");
intent.intent.putExtra(Browser.EXTRA_HEADERS, headers);
intent.launchUrl(Activity.this, Uri.parse("http://www.google.com"));
我们始终可以将许可名单中的标头附加到自定义标签页 CORS 请求。不过,Chrome 默认会过滤未列入许可名单的标头。虽然其他浏览器可能具有不同的行为,但开发者应预计非许可名单中的标头通常会被屏蔽。
在自定义标签页中包含非许可名单中的标头的支持方式是,先使用数字访问链接验证跨源连接。下一部分将介绍如何设置这些标头并使用所需的标头启动 Custom Tabs intent。
向自定义标签页 intent 添加额外标头
设置 Digital Asset Links
如需允许非许可名单中的标头通过自定义标签页 intent 传递,必须在 Android 应用和 Web 应用之间设置数字资产链接,以验证作者是否同时拥有这两个应用。
按照官方指南设置数字资产链接。对于链接关系,请使用“delegate_permission/common.use_as_origin”,这表示在验证链接后,两个应用属于同一来源。
创建带有额外标头的自定义标签页 intent
您可以通过多种方式创建 Custom Tabs intent。您可以通过将库添加到 build 依赖项中来使用 androidX 中提供的构建器:
implementation 'androidx.browser:browser:1.2.0'
构建 intent 并添加额外的标头:
CustomTabsIntent constructExtraHeadersIntent(CustomTabsSession session) {
CustomTabsIntent intent = new CustomTabsIntent.Builder(session).build();
// Example non-cors-approvelisted headers.
Bundle headers = new Bundle();
headers.putString("bearer-token", "Some token");
headers.putString("redirect-url", "Some redirect url");
intent.intent.putExtra(Browser.EXTRA_HEADERS, headers);
return intent;
}
设置自定义标签页连接以验证 Asset Link
自定义标签页连接用于在应用与 Chrome 标签页之间设置 CustomTabsSession。我们需要会话来验证应用和 Web 应用是否属于同一来源。
只有在正确设置了 Digital Asset Links 的情况下,验证才会通过。
建议调用 CustomTabsClient.warmup()。它允许浏览器应用在后台预先初始化,并加快网址打开过程。
// Set up a connection that warms up and validates a session.
CustomTabsServiceConnection connection = new CustomTabsServiceConnection() {
@Override
public void onCustomTabsServiceConnected(@NonNull ComponentName name,
@NonNull CustomTabsClient client) {
// Create session after service connected.
mSession = client.newSession(callback);
client.warmup(0);
// Validate the session as the same origin to allow cross origin headers.
mSession.validateRelationship(CustomTabsService.RELATION_USE_AS_ORIGIN,
Uri.parse(url), null);
}
@Override
public void onServiceDisconnected(ComponentName componentName) { }
};
设置在验证后启动 intent 的回调
CustomTabsCallback 已传递到会话中。我们设置了其 onRelationshipValidationResult(),以便在来源验证成功后启动之前创建的 CustomTabsIntent。
// Set up a callback that launches the intent after session validated.
CustomTabsCallback callback = new CustomTabsCallback() {
@Override
public void onRelationshipValidationResult(int relation, @NonNull Uri requestedOrigin,
boolean result, @Nullable Bundle extras) {
// Launch custom tabs intent after session was validated as the same origin.
CustomTabsIntent intent = constructExtraHeadersIntent(mSession);
intent.launchUrl(MainActivity.this, Uri.parse(url));
}
};
绑定自定义标签页服务连接
绑定服务会启动该服务,并且最终会调用连接的 onCustomTabsServiceConnected()。别忘了适当地取消绑定服务。绑定和取消绑定通常在 onStart() 和 onStop() activity 生命周期方法中完成。
// Bind the custom tabs service connection.
// Call this in onStart()
CustomTabsClient.bindCustomTabsService(this,
CustomTabsClient.getPackageName(MainActivity.this, null), connection);
// …
// Unbind the custom tabs service.
// Call this in onStop().
unbindService(connection);
演示应用代码
如需详细了解自定义标签页服务,请点击此处。如需查看可运行的示例应用,请参阅 android-browser-helper GitHub 代码库。
摘要
本指南演示了如何向自定义标签页 CORS 请求添加任意标头。审批名单中的标头可以附加到每个自定义标签页 CORS 请求。在 CORS 请求中,未列入许可名单的标头通常被认为是不安全的,Chrome 默认会过滤掉这些标头。仅允许附加相同来源的客户端和服务器(通过数字资产链接验证)。