Webview与js交互漏洞及解决方法(有注释代码)
来源:互联网 发布:基金公司招聘条件 知乎 编辑:程序博客网 时间:2024/04/30 15:36
前两天去Tencent面试,被面试官问到这个问题,答得不是很好,于是回来总结一下,并提供了解决方法与大家分享。
在Android中,netive与js交互已经不是什么新鲜事。大多数人都知道WebView存在一个漏洞,大致是因为js可以通过webview的window对象获得Class然后通过遍历所有的方法,找到runtime方法,边,虽然该漏洞已经在Android 4.2上修复了,即使用@JavascriptInterface代替addJavascriptInterface,但是由于兼容性和安全性问题,基本上我们不会再利用Android系统为我们提供的addJavascriptInterface方法或者@JavascriptInterface注解来实现,所以我们只能另辟蹊径,去寻找既安全,又能实现兼容Android各个版本的方案。
我们发现当setWebChromeClient之后,前端调用的一些js方法如:alert、Prompt、confirm等方法都会走你自定义的WebChromeClient类中相对应的重写的方法,由于alert、confirm使用频率较高,我们不建议处理,而prompt使用频率就少多了,甚至基本没使用,于是思路来了:我们可以让前端调用prompt这个方法,传过来一些信息,然后原生拦截prompt方法,获取这些信息,然后确定需要调用的jsbridge。
代码实现:
首先看一下目录结构:
前端部分
1、在页面加载时先定义好一些方法,包括生成一段前端与原生自己定义好的内容格式(URI)、调用prompt的方法;
//JSBridge.js//定义的URI内容格式如下://JSBridge://WindowJSBridge:821021544/toast?{"msg":"Hello JSBridge"}//JSBridge:固定标识,两端统一即可;//WindowJSBridge:要调用原生的bridge类名;//821021544:存储在前端callback数组中的index,代表是哪个jsbridge请求的callback;//toast:要调用的原生方法名;//{"msg":"Hello JSBridge"}:携带给原生的参数;(function (win) { var hasOwnProperty = Object.prototype.hasOwnProperty; var JSBridge = win.JSBridge || (win.JSBridge = {}); var JSBRIDGE_PROTOCOL = 'JSBridge'; var Inner = { callbacks: {}, call: function (obj, method, params, callback) { console.log(obj+" "+method+" "+params+" "+callback); var port = Util.getPort(); console.log(port); this.callbacks[port] = callback; var uri=Util.getUri(obj,method,params,port); console.log(uri); window.prompt(uri, ""); }, onFinish: function (port, jsonObj){ var callback = this.callbacks[port]; callback && callback(jsonObj); delete this.callbacks[port]; }, }; var Util = { getPort: function () { return Math.floor(Math.random() * (1 << 30)); }, getUri:function(obj, method, params, port){ params = this.getParam(params); var uri = JSBRIDGE_PROTOCOL + '://' + obj + ':' + port + '/' + method + '?' + params; return uri; }, getParam:function(obj){ if (obj && typeof obj === 'object') { return JSON.stringify(obj); } else { return obj || ''; } } }; for (var key in Inner) { if (!hasOwnProperty.call(JSBridge, key)) { JSBridge[key] = Inner[key]; } }})(window);
前端页面主要是通过点击button使用在上面js文件中定义好的方法调用原生方法。
<!DOCTYPE HTML , 前端页面><html><head> <meta charset="utf-8"> <title>JSBridge</title> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1, user-scalable=no"/> <script src="file:///android_asset/JSBridge.js" type="text/javascript"></script> <script type="text/javascript"> </script> <style> </style></head><body><div> <h3>JSBridge 测试</h3></div><ul class="list"> <li> <div> <button onclick="JSBridge.call('WindowJSBridge','toast',{'msg':'Hello JSBridge'},function(res){alert(JSON.stringify(res))})"> 测试showToast </button> </div> </li> <br/></ul><ul class="list"> <li> <div> <button onclick="JSBridge.call('AppInfoJSBridge','getPackageName',{},function(res){alert(JSON.stringify(res))})"> 测试子线程回调 </button> </div> </li> <br/></ul></body></html>
原生代码:
1、自定义WebChromeClient,处理相对应方法
/** * @author caoyujie * WebChromeClient主要辅助WebView处理JavaScript的对话框、网站图标、网站title、加载进度等 */public class JSBridgeWebChromeClient extends WebChromeClient { /** * @param view * @param url * @param message 前端设置的内容 * @param defaultValue * @param result 按确定时回调的结果, result.cancel()关闭回调,否则将卡住界面 * @return true:交给客户端自定义处理 false:游览器默认处理 */ @Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { JSBridgeManager.getInstance().excuteJS(view,message); result.cancel(); return true; }}
2、之前我们定义好的uri格式的信息就是message,我们生成一个JSBridge管理类,解析这个uri,得到需要调用的类和方法,处理相对应的前端请求:
/** * jsBridge处理类,采用单例 */public class JSBridgeManager { private static JSBridgeManager INSTANCE; private static String BRIDGE_PREFIX = "JSBridge"; //约定前缀 private static HashMap<String,HashMap<String,Method>> exposedMethods = new HashMap<>(); public static JSBridgeManager getInstance() { if (INSTANCE == null) { synchronized (JSBridgeManager.class) { if (INSTANCE == null) { INSTANCE = new JSBridgeManager(); } } } return INSTANCE; } /** * 执行js所调用的原生代码 * @param webview * @param uriString 前端传来的信息,包括了js类名、方法名、参数、callback等 */ public void excuteJS(WebView webview , String uriString) { JSBridgeRequest jsBridgeRequest = parseJSBridge(uriString); if (jsBridgeRequest == null) return; Class registJSBridge = null; switch (jsBridgeRequest.getMethodName()) { case "toast": registJSBridge = WindowJSBridge.class; break; case "getPackageName": registJSBridge = AppInfoJSBridge.class; break; } if(registJSBridge != null){ regitstJSBridge(registJSBridge); callJava(webview , registJSBridge , jsBridgeRequest); } } /** * 将前端传过来的信息转换成 js请求model */ private JSBridgeRequest parseJSBridge(String uriString) { JSBridgeRequest request = null; try { if (!TextUtils.isEmpty(uriString) && uriString.startsWith(BRIDGE_PREFIX)) { request = new JSBridgeRequest(); Uri jsUri = Uri.parse(uriString); request.setClassName(jsUri.getHost()); request.setPort(String.valueOf(jsUri.getPort())); request.setParm(new JSONObject(jsUri.getQuery())); String path = jsUri.getPath(); if (!TextUtils.isEmpty(path)) { request.setMethodName(path.replace("/", "")); } } } catch (Exception e) { e.printStackTrace(); } return request; } /** * 根据前端指定的jsbridge类名找到该类,并获得其提供的方法 * @param clazz */ private void regitstJSBridge(Class<IBridge> clazz){ String exposedName = clazz.getSimpleName(); if(!exposedMethods.containsKey(exposedName)){ try { exposedMethods.put(exposedName, getAllMethod(clazz)); } catch (Exception e){ e.printStackTrace(); } } } /** * 获得某个jsbridge类中所有公开方法 * @param clazz 具体的jsbridge类 */ private HashMap<String,Method> getAllMethod(Class<IBridge> clazz){ HashMap<String,Method> methods = new HashMap<>(); Method[] declaredMethods = clazz.getDeclaredMethods(); for (Method method : declaredMethods) { String name; if(method.getModifiers() != Modifier.PUBLIC || (name = method.getName()) == null){ continue; } Class[] parameters = method.getParameterTypes(); if(parameters != null && parameters.length == 3){ if(parameters[0] == WebView.class && parameters[1] == JSONObject.class && parameters[2] == JsCallback.class){ methods.put(name , method); } } } return methods; } /** * 通过反射得到该类的实例,并执行前端需要的js方法 * @param webview * @param classImpl jsbridge类的class对象 * @param request 封装的js请求model */ private void callJava(WebView webview , Class<IBridge> classImpl , JSBridgeRequest request){ if(request == null) return; String className = request.getClassName(); if (exposedMethods.containsKey(request.getClassName())) { HashMap<String, Method> methods = exposedMethods.get(className); String methodName = request.getMethodName(); if (methods != null && methods.size() > 0 && methods.containsKey(methodName)) { Method method = methods.get(methodName); try { method.invoke(classImpl.newInstance(), webview, request.getParm(), new JsCallback(webview, request.getPort())); } catch (Exception e) { e.printStackTrace(); } } } } /** * 释放资源 * 游览器页面关闭时调用 */ public void release(){ exposedMethods.clear(); }}
另外一些相关类
/** * Created by caoyujie on 17/3/13. * 根据前端请求信息转换成的请求实体类 */public class JSBridgeRequest { /** * native方法名 */ private String methodName; /** * jsbridge的类名 */ private String className; /** * 请求参数 */ private JSONObject parm; /** * 区分回调的标志 */ private String port; public String getMethodName() { return methodName; } public void setMethodName(String methodName) { this.methodName = methodName; } public String getClassName() { return className; } public void setClassName(String className) { this.className = className; } public JSONObject getParm() { return parm; } public void setParm(JSONObject parm) { this.parm = parm; } public String getPort() { return port; } public void setPort(String port) { this.port = port; }}
/** * Created by caoyujie on 17/3/13. * 返回结果给前端的回调类 */public class JsCallback { private Handler mHandler = new Handler(Looper.getMainLooper()); private static final String CALLBACK_FORMAT = "javascript:JSBridge.onFinish(%s,%s);"; private String mPort; private WeakReference<WebView> webview; public JsCallback(WebView view, String port) { mPort = port; webview = new WeakReference<WebView>(view); } /** * 添加回调 * @param callbackEntity 回调给前端结果实体类 */ public void apply(JsCallback.Entity callbackEntity) { JSONObject jsonObject = callbackEntity.toJSON(); final String execJs = String.format(CALLBACK_FORMAT, mPort, String.valueOf(jsonObject)); if (webview != null && webview.get() != null) { mHandler.post(new Runnable() { @Override public void run() { webview.get().loadUrl(execJs); } }); } } /** * 回调给前端结果实体类 */ public static class Entity { /** * 成功码 */ private int code; /** * 响应信息 */ private String message; /** * 回调数据 */ private String data; public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public String getData() { return data; } public void setData(String data) { this.data = data; } public JSONObject toJSON() { JSONObject jsonObject = null; try { jsonObject = new JSONObject(); jsonObject.put("code", code); jsonObject.put("message", message); jsonObject.put("data", data); } catch (JSONException e) { e.printStackTrace(); } return jsonObject; } }}
/** * Created by caoyujie * jsbridge基类,写一些公用方法 */public interface IBridge {}
/** * Created by caoyujie on 17/3/13. * 窗口类js接口 */public class WindowJSBridge implements IBridge { public void toast(WebView webView , JSONObject parmObject , JsCallback callback){ Toast.makeText( webView.getContext() , parmObject.optString("msg"), Toast.LENGTH_SHORT).show(); }}/** * Created by caoyujie on 17/3/13. * 获取应用信息类js接口 */public class AppInfoJSBridge implements IBridge { public void getPackageName(WebView webView , JSONObject parmObject , JsCallback callback){ JsCallback.Entity entity = new JsCallback.Entity(); entity.setCode(200); entity.setMessage("成功"); entity.setData(webView.getContext().getPackageName()); callback.apply(entity); }}
查看源码: github源码地址
最后上一张效果演示:
- Webview与js交互漏洞及解决方法(有注释代码)
- webview与js交互的漏洞
- android与js交互,以及webview漏洞修复问题
- ios中webview与js交互代码
- Android Webview优化及与Js交互
- webview与js交互
- webview与js交互
- webview与js交互
- webview与js交互
- webview与js交互
- webview与js交互
- WebView与JS交互
- webView与js交互
- webView与js交互
- webview与js交互
- webview与js交互
- webview与js交互
- webview与js交互
- 【J2EE】servlet
- 只读表空间的备份与恢复
- 并查集 路径压缩
- CTR预估中GBDT与LR融合方案
- hibernate框架初次搭建并使用
- Webview与js交互漏洞及解决方法(有注释代码)
- LeetCode 278. First Bad Version
- MySql常用命令整理
- JAVA实现DES加密实现详解
- CSS3之box-sizing属性分析
- CAD关闭文件时提示对话框信息注意事项
- [蓝桥杯]递归求二项式系数
- Apple Swift3.0 Guides and Sample Code
- hdu5726 GCD ST表+离线