一。自定義 Scheme
Android 應用/組件間通信有一種方式是 intent,應用可以註冊intent filter 聲明自己對什麼樣的 intent 感興趣,其它應用發送 intent 時通過系統級廣播傳遞過來,如果與預先註冊的 intent filter 匹配,應用將收到該 intent(無論應用是否正在運行,都會被「喚醒」,也就是隱式啟動 Activity),取出 intent 攜帶的數據,做進一步處理
就是這樣,通過系統廣播拿到一次起來的機會,例如在 manifest 裡靜態註冊intent filter 聲明自定義 scheme:
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!--註冊 scheme-->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<!--BROWSABLE 指定該 Activity 能被瀏覽器安全調用-->
<category android:name="android.intent.category.BROWSABLE"/>
<!--聲明自定義 scheme,類似於 http, https-->
<data android:scheme="hoho"/>
</intent-filter>
</activity>
action、category、data 都必須完全匹配才能獲得 intent,這裡聲明了 2 個 category,只有在 intent 同時含有這 2 個 category 時才算匹配,而 android.intent.category.DEFAULT 是預設的,有實際意義的是 android.intent.category.BROWSABLE,表示允許通過瀏覽器啟動該 activity(呼起 App)。後續的 data 限定了觸發條件,當 scheme 為 hoho 時才匹配,例如瀏覽器訪問 hoho://abc,能夠匹配成功,App 就起來了
二。取出數據
在 onCreate 裡拿到 intent,取出 uri:
@Override
protected void onCreate(Bundle savedInstanceState) {
//...
// 獲取 uri 參數
Intent intent = getIntent();
String scheme = intent.getScheme();
Uri uri = intent.getData();
String str = "";
if (uri != null) {
String host = uri.getHost();
String dataString = intent.getDataString();
String from = uri.getQueryParameter("from");
String path = uri.getPath();
String encodedPath = uri.getEncodedPath();
String queryString = uri.getQuery();
//...根據 uri 判斷打開哪個頁,或者打開哪個功能
}
}
這裡的 URI 就是標準的 URI,有協議、主機名、端口號、路徑、查詢字符串等等,但一般自定義 scheme 不需要這麼麻煩,只用 path/query 做簡單區分就行,比如:
// 通過 path 區分
hoho://toFeature/login
// 通過 query 區分
hoho://open?feature=login
當然,也可以通過端口號等區分,沒什麼區別
三。在線頁面呼起 App
瀏覽器先發出自定義 scheme 請求,系統廣播收到後再分發給各應用,那麼頁面發送請求的方式就多了:
location.href
iframe.src
a.href
img.src
...其它能發出請求的方式
這些方式在強弱上有區別,比如 location.href 是強的,而 img.src 很弱,至少要強到瀏覽器決定把這個請求交給系統廣播才行,比如 img 請求自定義 scheme,瀏覽器認為沒有必要交給系統廣播。一般只用前 2 種最強的方式:location.href 和 iframe.src,隱藏 iframe 偷偷請求自定義 scheme 相對用得更多,因為不會有未知的副作用(location 方式或許可能被記入歷史棧或者 unload 當前頁,但 iframe 絕對沒有太嚴重的副作用)
但無論哪種方式,都無法得知 App 被呼起了沒,可能沒安裝 App,也可能 intent 沒匹配成功,但頁面肯定沒有辦法得知。所以一般呼起 App 的頁面都會延遲自動跳轉下載頁,無論有沒有成功呼起 App,這也是迫不得已
除了頁面發出請求,還有一種更強的方式:通過應用發出請求,例如:
// 通過 webview 發出請求
webview.loadUrl(mySchemeUri);
這個起點就是應用級,比 WebView 中頁面請求要強一些。所以一般 Hybrid App 中,用戶端會提供這樣的接口,用來跳轉第三方,比頁面請求更強
四。Intent Scheme URL 攻擊
自定義 Scheme 存在安全風險,比如:
-
註冊優先順序更高的相同
intent filter,竊取scheme uri -
如果知道跳轉的自定義
scheme格式,可以跳向釣魚頁面(確實是在 App 裡打開的頁面,但它是第三方做的假的) -
...其它風險
一般自定義 scheme 都是不公開的,但難免會洩漏出去(反編譯 App 等方式),scheme 接口本身要做好防範,接收 intent 時可以這樣做:
// forbid launching activities without BROWSABLE category
intent.addCategory("android.intent.category.BROWSABLE");
// forbid explicit call
intent.setComponent(null);
// forbid intent with selector intent
intent.setSelector(null);
不信任所有來自自定義 scheme 的輸入,對於跳轉接口,還要有白名單限制
五。WebView Scheme 白名單
WebView 作為頁面容器,可以過濾/攔截頁面請求:
class MyWebClient extends WebViewClient {
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
if (url.startsWith("hoho://")) {
return null;
}
return super.shouldInterceptRequest(view, url);
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String scheme = request.getUrl().getScheme();
if (scheme.equals("hoho")) {
return null;
}
return super.shouldInterceptRequest(view, request);
}
}
上面的用於 API[11-20],21 棄用String url,新增了WebResourceRequest request,在 API21+ 只觸發WebResourceRequest request 形式的,所以兼容考慮,兩個都要重寫一遍
對於滿足過濾條件的,攔截掉,所以在微信裡無法呼起 App,因為不在白名單裡,被攔截下來,沒有交給系統廣播
在被攔截的情況下,iframe 方式的優勢就體現出來了,a.href 和 location.href 都會導致頁面跳轉,顯示「網頁無法打開...因為 net::ERR_UNKNOWN_URL_SCHEME」,而 iframe 方式不影響當前頁
六。Demo
apk 下載地址:http://ayqy.net/apk/android-scheme.apk
測試頁面:http://ayqy.net/temp/android-scheme.html
寫在最後
Android Studio 實在太慢了,懷念 eclipse,另外,感謝 @旭
暫無評論,快來發表你的看法吧