Native/Webview bridge for Hybrid分分快三计划

作者:分分快三计划
android

安卓通过addJavaScriptInterface方法注入Java对象到js上下文对象window中,由于4.2以下版本中,该方法有漏洞, 解决该漏洞的方法有两种,第一种通过URL scheme解决,第二种通过如下方案解决:

webview.loadUrl("javascript:if(window.WebViewBridge === undefined) { window.WebViewBridge = { call: function(jsonString) { window.prompt(jsonString); }}};");

在webview中通过loadUrl定义一个window.WebViewBridge及call通用方法,方法体内执行了window.prompt,然后在WebChromeClient类中处理onJsPrompt,设置拦截规则,onJsPrompt返回true,将不处理dialog;

推荐文章:安卓Webview

Native调用Web

Native主动调用WebView,可以通过WebView注入JS方式实现Web接口的调用。涉及到两个接口:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { webView.evaluateJavascript(trigger, null);} else { webView.loadUrl;}

WebView可以load一个url,进而打开相关的页面,用法如下:

webView.loadUrl("http://app.dev.dajiazhongyi.com");

应用更加灵活广泛的则是Native可以向WebView注入js代码,用法如下:

String trigger = "javascript:dj.callback({"content":"测试一下","callbackname":"djapi_callback_1492767300785_4195"})";if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { webView.evaluateJavascript(trigger, null);} else { webView.loadUrl;}说明:假设js代码中我们提供了一个dj.callback 方法,那就可以通过以上方式来注入这段js并执行。其他的扩展也是如此

安装

npm i --save webview-bridge

Web-Native之间的通信是双向的,即Native端和Web端互为调用者和被调用者。在Android上,Native端使用WebView来展现Web页,WebView自身提供了与Web交互的接口。

Native/Webview bridge for Hybrid

Hybrid是目前App开发的主流模式,它兼具Native良好的用户交互性能,以及Web良好的页面扩展和跨平台特性。如FaceBook的React-Native,微信的小程序开发等都是Hybrid模式。本文要探讨的问题就是Hybrid模式中Native和Web的交互问题,并介绍一下自我摸索实现的Native-Web交互框架。

使用

import Bridge from 'hybride-webview-bridge';

// 如果客户端没有使用URL scheme,则不需要传递参数
const WebViewBridge = new Bridge('mqq://');
WebViewBridge.call(); // 将会唤起手机版qq软件

/**
 * 调用原生方法
 * @param  {String} method 方法名
 * @param  {Object} params 参数
 * @return {Promise}       当收到原生方法执行的返回结果时resolve
 */
// WebViewBridge.call(method, params);

// for instance
WebViewBridge.call('getUserInfo').then(res => {
    // handle response info
});

// for instance
WebViewBridge.call('getLocation', { CacheMode: 0 }).then(res => {
    // handle response info
});

Web调用Native

JavascriptInterface方式

Native端作为被调用者,它是通过WebView的addJavascriptInterface接口实现Web端对它的回调。WebView内核层会为页面的window对象添加了一个属性,并将这个属性绑定到Native端的一个Java对象,页面使用这个属性访问到Java对象的方法,实现Web端对Native端的调用。

举个例子:在Native端,定义一个JsInterface类,并定义了了一个方法 post 供Web进行调用

public final class JsInterface { ... private final Handler mHandler = new Handler(); @JavascriptInterfacepublic void post(String cmd, String param) { mHandler.post -> { Toast.makeText(sContext, cmd param, Toast.LENGTH_LONG).show; } ...}

初始化WebView的时候注册JavascriptInterface对象:

protected JsInterface jsInterface;@SuppressLint({"SetJavaScriptEnabled", "JavascriptInterface", "AddJavascriptInterface"})private void initWebView() { webView.addJavascriptInterface(jsInterface, "webview");}

Web中Js调用方式:

window.webview.post(cmd, JSON.stringify;

JavascriptInterface的方式是有限制的,在4.2版本之后回调方法加上@JavascriptInterface注解即可解决漏洞问题。在Android4.2版本之前漏洞问题

该交互框架主要包含以下几个方面:

  • 数据结构
  • Native层
  • Web层

数据结构

Native定义的JavascriptInterface方式是post(String command, String para), command是事件类型,以字符串区分,形如“showToast”、“showDialog”; para是JSON格式的数据

Native层

Native层提供了Web层调用的方法,主要提供了3个能力:

  1. WebView初始化
  2. Js事件定义
  3. Js事件分发

WebView的初始化指的是添加JavascriptInterface到WebView; Js事件定义则是在框架结构内添加Native支持的事件接口;Js分发表示Native通过post方式接收到WebView的事件,接收到事件后同统一进行分发。

具体代码如下:

public final class JsInterface { private final Context mContext; private final Handler mHandler = new Handler(); private final Map<String, Command> mCommands = Maps.newHashMap(); public JsInterface(Context context) { mContext = context; } @JavascriptInterface public void post(String cmd, String param) { mHandler.post -> { final Command command = mCommands.get; if (command != null) { if (TextUtils.isEmpty || param.equals("undefined")) { command.exec(mContext, null); } else { command.exec(mContext, new Gson().fromJson(param, Map.class)); } } }); } public void registerCommand(Command command) { mCommands.put(command.name(), command); } public void unregisterCommand(Command command) { mCommands.remove(command.name; } public void unregisterAllCommands() { mCommands.clear(); } public interface Command { String name(); void exec(Context context, Map params); }}

public abstract class BaseWebViewFragment extends BaseFragment { @BindView(R.id.web_view) protected DWebView webView; @Inject protected JsInterface jsInterface; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); component().inject; } @Override public void onDestroy() { super.onDestroy(); } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(getLayoutRes(), container, false); ButterKnife.bind(this, view); return view; } @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); initWebView(); registBaseCommands(); } @Override public void onDestroyView() { super.onDestroyView(); unregistBaseCommands(); } @SuppressLint({"SetJavaScriptEnabled", "JavascriptInterface", "AddJavascriptInterface"}) private void initWebView() { final WebSettings settings = webView.getSettings(); settings.setUserAgentString(HttpHeaderUtils.getUserAgent; webView.addJavascriptInterface(jsInterface, "webview"); } protected void loadJS(String trigger) { if (!TextUtils.isEmpty { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { webView.evaluateJavascript(trigger, null); } else { webView.loadUrl; } } } @LayoutRes protected abstract int getLayoutRes(); protected abstract void registerCommands(); /** * 注册基本的command,使之具备基本的native交互能力 */ private void registBaseCommands() { registerCmd4JsInterface(pageLoadCompletedCommand); registerCmd4JsInterface(showToastCommand); registerCmd4JsInterface(showDialogCommand); registerCommands(); } protected final void registerCmd4JsInterface(JsInterface.Command cmd) { jsInterface.registerCommand; } private void unregistBaseCommands() { jsInterface.unregisterAllCommands(); } public void loadJS(String cmd, Object param) { if (webView != null) { String trigger = "javascript:"   cmd   "("   new Gson().toJson   ")"; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { webView.evaluateJavascript(trigger, null); } else { webView.loadUrl; } } } public void dispatchEvent(String name) { Map<String, String> param = Maps.newHashMapWithExpectedSize; param.put("name", name); loadJS("dj.dispatchEvent", param); } /**************** Native callback interface define *******************/ /** * web页面加载完成回调 */ private JsInterface.Command pageLoadCompletedCommand = new JsInterface.Command() { @Override public String name() { return "pageLoadComplete"; } @Override public void exec(Context context, Map params) { if (null != params.get("callback")) { String functionName = params.get("callback").toString(); onFrameworkLoadCompleted(functionName); } else { onFrameworkLoadCompleted; } } }; protected void onFrameworkLoadCompleted(String functionName) { if (StringUtils.isNotNullOrEmpty(functionName)) { final Map<String, Object> param = new HashMap<>(); loadJS("dj.callback", param); } } /** * Native回调web处理 */ public void handleCallback(String functionName, HashMap hashMap) { if (StringUtils.isNotNullOrEmpty(functionName)) { hashMap.put("callbackname", functionName); loadJS("dj.callback", hashMap); } } /** * 显示Toast信息 */ private final JsInterface.Command showToastCommand = new JsInterface.Command() { @Override public String name() { return "showToast"; } @Override public void exec(Context context, Map params) { Toast.makeText(context, String.valueOf(params.get("message")), Toast.LENGTH_SHORT).show(); } }; private final JsInterface.Command showDialogCommand = new JsInterface.Command() { @Override public String name() { return "showDialog"; } @Override public void exec(Context context, Map params) { if (CollectionUtils.isNotNull { String title =  params.get; String content =  params.get("content"); int canceledOutside = 1; if (params.get("canceledOutside") != null) { canceledOutside =   params.get("canceledOutside"); } List<Map<String, String>> buttons = (List<Map<String, String>>) params.get("buttons"); String callbackName =  params.get("callback"); if (!TextUtils.isEmpty { AlertDialog dialog = new AlertDialog.Builder(getContext .setTitle .setMessage .create(); dialog.setCanceledOnTouchOutside(canceledOutside == 1 ? true : false); if (CollectionUtils.isNotNull { for (int i = 0; i < buttons.size { Map<String, String> button = buttons.get; int buttonWhich = getDialogButtonWhich; if (buttonWhich == 0) return; dialog.setButton(buttonWhich, button.get, (dialog1, which) -> { button.put("callbackname", callbackName); loadJS("dj.callback", button); }); } } dialog.show(); } } } private int getDialogButtonWhich(int index) { switch  { case 0: return DialogInterface.BUTTON_POSITIVE; case 1: return DialogInterface.BUTTON_NEGATIVE; case 2: return DialogInterface.BUTTON_NEUTRAL; } return 0; } };}

此处用到了一个自定义的DWebView,其继承自WebView,主要是为了设定WebView的相关特性,比如WebSettings配置、WebViewClient设置、ActionMode.CallBack的处理。考虑到篇幅问题,此处不再展开。

Web层

Web层的核心提供了一下能力:

  1. 为页面模块封装快捷方法,使之可以通过window.webview.post(String command, String para); 快捷的调用native提供的接口;
  2. 接收native的回调事件,并进行分发处理,细心的你应该可以发现,在上述代码中有这么一段:
 /** * Native回调web处理 */ public void handleCallback(String functionName, HashMap hashMap) { if (StringUtils.isNotNullOrEmpty(functionName)) { hashMap.put("callbackname", functionName); loadJS("dj.callback", hashMap); } }

所以web核心层需要提供“dj.callback”的处理。

  1. Webview自定义事件,及事件触发,这个主要用于Hybrid开发中,WebView自定义Menu,用户点击menu可以直接触发相关事件

具体的js代码如下,使用时每个页面模块需要单独引入:

var dj = {};dj.os = {};dj.os.isIOS = /iOS|iPhone|iPad|iPod/i.test(navigator.userAgent);dj.os.isAndroid = !dj.os.isIOS;dj.callbackname = function(){ return "djapi_callback_"   (new Date.getTime()   "_"   Math.floor(Math.random() * 10000);};dj.callbacks = {};dj.addCallback = function(name,func,userdata){ delete dj.callbacks[name]; dj.callbacks[name] = {callback:func,userdata:userdata};};dj.callback = function{ var callbackobject = dj.callbacks[para.callbackname]; if (callbackobject !== undefined){ if (callbackobject.userdata !== undefined){ callbackobject.userdata.callbackData = para; } if(callbackobject.callback != undefined){ var ret = callbackobject.callback(para,callbackobject.userdata); if(ret === false){ return } delete dj.callbacks[para.callbackname]; } }};dj.post = function{ if(dj.os.isIOS){ var message = {}; message.meta = { cmd:cmd }; message.para = para || {}; window.webview.post; }else if(window.dj.os.isAndroid){ window.webview.post(cmd,JSON.stringify; }};dj.postWithCallback = function(cmd,para,callback,ud){ var callbackname = dj.callbackname(); dj.addCallback(callbackname,callback,ud); if(dj.os.isIOS){ var message = {}; message.meta = { cmd:cmd, callback:callbackname }; message.para = para; window.webview.post; }else if(window.dj.os.isAndroid){ para.callback = callbackname; window.webview.post(cmd,JSON.stringify; }};dj.dispatchEvent = function{ if  { para = {"name":"webviewLoadComplete"}; } var evt = {}; try { evt = new Event(para.name); evt.para = para.para; } catch { evt = document.createEvent("HTMLEvents"); evt.initEvent(para.name, false, false); } window.dispatchEvent;};dj.addEventListener = window.addEventListener;dj.stringify = function{ var type = typeof obj; if (type == "object"){ return JSON.stringify; }else { return obj; }};window.dj = dj;

关于callback的机制,简单说明一下:

  1. WebView调用Native需要callback时,会生成callbackname,并以callbackname为key将callback函数记录起来,WebView将callbackname一并传给native;
  2. native通过loadJS("dj.callback", hashMap);回调,回调时将callbackname和回调的内容一并封装到hashmap传给WebView,WebView根据callbackname获取记录中的callback函数,进而实现回调

以上是Android中Web-Native交互框架的主要内容,该框架Native层可以方便的进行native接口的扩展,Web层提供了接口调用和事件回调的方法,也可以进一步扩展一些通用的接口以方便上层业务模块进行调用。

补充:

漏洞说明addJavascriptInterface的本质是向webview注入一个Java对象,如上所示,注入了JsInterface对象。根据Java对象的反射机制,就可以通过该对象获取到java.lang.Runtime 的实例,并通反射执行getRuntime(String command) 方法,从而窃取了信息

Js获取Runtime方法如下:

function execute { for (var obj in window) { if ("getClass" in window[obj]) { alert; return window[obj].getClass().forName("java.lang.Runtime") .getMethod("getRuntime",null).invoke(null,null).exec; } } } 

感兴趣的用户可以尝试在js中调用:execute("ls /mnt/sdcard/")

漏洞解决Web端调用Native端使用WebChromeClient的onJsPrompt回调, onJsPrompt回调接口是页面弹出提示用的,页面JS调用prompt方法时,WebView内核会将内容回传到onJsPrompt接口,这样Native可以弹出本地提示框。我们可以利用这个接口来实现Web端对Native端的调用,只需要将prompt的内容约定为特定的格式,Web端按照这个格式生成内容,Native端在onJsPrompt接收到内容后按照这个格式进行解析,如果内容符合约定的格式,则作为Web-Native交互逻辑处理,否则作为增加的提示逻辑处理。

具体的处理如下:

1、 指定Android4.2以下版本的处理方式

@SuppressLint({"SetJavaScriptEnabled", "JavascriptInterface", "AddJavascriptInterface"}) private void initWebView() { final WebSettings settings = webView.getSettings(); settings.setUserAgentString(HttpHeaderUtils.getUserAgent; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { webView.addJavascriptInterface(jsInterface, "webview"); } else { webView.removeJavascriptInterface("searchBoxJavaBridge_"); webView.setWebChromeClient(new DWebChromeClient; } }

2、在onPageStarted的时候,注入js代码,定义window.webview

public void onPageStarted(WebView view, String url, Bitmap favicon) { super.onPageStarted(view, url, favicon); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { view.loadUrl("JavaScript:if(window.webview == undefined){window.webview={call:function(command,para){prompt('{"command":'   command   ',"param":'   param   '}')}}};"); }}

3、分发处理Web端的调用事件

public class DWebChromeClient extends WebChromeClient { @Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { new Handler -> { // TODO: 2017/4/23 }); } return true; } }

参考文章Android Developer WebView

JS 与 Native 安全交互浅析,两种方式实现

知识点扩充

callback 回调

当调用 WebViewBridge.call('getUserInfo')成功,要求客户端调用前端 WebViewBridgeCallback 方法进行响应,源码如下:

/**
 * 调用原生客户端方法后执行的回调函数
 * @param  {String} method 方法名
 * @param  {Object|String} res 回调响应信息
 */
window.WebViewBridgeCallback = (method, res) => {
    if (typeof res === 'String') {
        res = JSON.parse(res);
    }
    window.WebViewBridge.receiveResponse(method, res);
};

要求(原理)

Native/Webview bridge for Hybrid分分快三计划。1、如果ios开发在ios8及以上系统使用postMessage,请支持js变量window.webkit.messageHandlers.WebViewBridge,内部实现如下:

window.webkit.messageHandlers.WebViewBridge.postMessage(JSON.stringify({
    method: 'getLocation',
    params: {
        CacheMode: 0,
    },
}));

2、客户端注入全局对象 WebViewBridge,并实现call方法,js用法如下:

window.WebViewBridge.call('getLocation', JSON.stringify({
    CacheMode: 0,
}));

如果没有实现call方法,则js内部会调用被注入WebViewBridge对象方法,如:

window.WebViewBridge.getLocation(JSON.stringify({
    CacheMode: 0,
}));

3、如果不支持postMessage发送消息,也没有注入全局js对象,最一种就是使用URL scheme了,客户端url拦截处理,这种方式需要使用setTimeout延时处理,避免后者覆盖前者(同时调用多次)协议地址类似如下:

const msg = decodeURIComponent(JSON.stringify({
    method: 'getLocation',
    params: {
        CacheMode: 0,
    },
}));
const URLScheme = `qq://${msg}`;
ios

ios8系统及以上版本可以通过注入 window.webkit.messageHandlers.XXX.postMessage方法,我们可以使用这个方法直接向 Native 层传值,非常方便。 推荐文章:postMessage技术 ios官方webkit网站

ios7开始,还可以使用javascriptcore注入Java对象到js上下文对象window中 最后一种 ios也支持URL scheme

推荐文章:WKWebview相关

 

Native/Webview bridge for Hybrid分分快三计划。个人微信公众号:

分分快三计划 1

 

特点

  • Native/Webview bridge for Hybrid分分快三计划。支持自定义app URL scheme
  • 支持多种处理方式(全部涵盖)
  • 支持Promise处理回调

本文由分分快三计划发布,转载请注明来源

关键词: 分分快三计划 Android 框架 Native