Skip to content

Instantly share code, notes, and snippets.

@agehua
Created August 3, 2016 08:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save agehua/99233b40e05db29ee0ed4f50fb2c7530 to your computer and use it in GitHub Desktop.
Save agehua/99233b40e05db29ee0ed4f50fb2c7530 to your computer and use it in GitHub Desktop.
Android Webview防止js远程代码攻击
package XXX;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.net.Uri;
import android.net.http.SslError;
import android.os.Build;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.webkit.JsPromptResult;
import android.webkit.SslErrorHandler;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import org.json.JSONArray;
import org.json.JSONObject;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* Created by agehua on 2016/5/26.
* 只处理js与native交互安全问题
* <p>或关闭js交互<p/>
* <li>没有使用js的可以不使用该类<li/>
*/
public class BaseWebView extends WebView {
private NativeAndJsCallBackInterface mCallBackInterface = null;
private int webmode=0;
private Context mContext;
private RoundCornerProgressBar mProgressbar;//自定义progressbarview
private boolean isShowProgressBar=false;
private boolean isShowDialog=false;
private static final boolean DEBUG = true;
private static final String VAR_ARG_PREFIX = "arg";
private static final String MSG_PROMPT_HEADER = "MyApp:";
private static final String KEY_INTERFACE_NAME = "obj";
private static final String KEY_FUNCTION_NAME = "func";
private static final String KEY_ARG_ARRAY = "args";
private static final String[] mFilterMethods = {
"getClass",
"hashCode",
"notify",
"notifyAll",
"equals",
"toString",
"wait",
};
private HashMap<String, Object> mJsInterfaceMap = new HashMap<String, Object>();
private String mJsStringCache = null;
public BaseWebView(Context context) {
super(context);
initView(context,null);
mContext =context;
}
public BaseWebView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context,attrs);
mContext =context;
}
public BaseWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context,attrs);
mContext =context;
}
private void initView(Context context,AttributeSet attrs){
//进度条
mProgressbar = new RoundCornerProgressBar(context,attrs);
mProgressbar.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, ImageSize.inPX(context,10), 0, 0));
addView(mProgressbar);
}
/**
* 一定要初始化这个方法
* @param isShowTopProgressbar 是否显示顶部进度条
* @param callBackInterface
* <li>可以传null,则默认不使用js</li>
* <li>传递NativeCallBack类,则默认不使用js</li>
* <li>传递NativeAndJsCallback类,则根据webmode决定是否使用js </li>
* <li>传由fragment或activity实现NativeAndJsCallBackInterface接口,则根据webmode决定是否使用js </li>
* @param webMode 0,不使用js;1,使用js
*/
public void initWebView(NativeAndJsCallBackInterface callBackInterface, int webMode, boolean isShowTopProgressbar) {
this.mCallBackInterface =callBackInterface;
this.webmode =webMode;
WebSettings settings = getSettings();
if (mCallBackInterface instanceof NativeCallBack){
webmode =0;
}
if (webmode == 1){
//不使用缓存
settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
//disable zoom control.
settings.setBuiltInZoomControls(false);
//不使用数据库
settings.setDatabaseEnabled(false);
//滚动条覆盖在网页上
setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
// Enable JavaScript.
settings.setJavaScriptEnabled(true);
if(null!= mCallBackInterface) { //可以添加自己的js接口类
addJavascriptInterface(mCallBackInterface, "app");
}
removeSearchBoxImpl();
/**尝试解决h5游戏画面卡顿*/
//(deprecated from API 18+):
getSettings().setRenderPriority(WebSettings.RenderPriority.HIGH);
if (Build.VERSION.SDK_INT >= 19) {
// chromium, enable hardware acceleration
setLayerType(View.LAYER_TYPE_HARDWARE, null);
} else {
// older android version, disable hardware acceleration
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
}else {
//不使用缓存
settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
//disable zoom control.
settings.setBuiltInZoomControls(false);
//不使用数据库
settings.setDatabaseEnabled(false);
//滚动条覆盖在网页上
setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
}
this.setWebViewClient(new WebViewClientEx());
isShowProgressBar =isShowTopProgressbar;
if (isShowTopProgressbar)
mProgressbar.setVisibility(View.VISIBLE);
else
mProgressbar.setVisibility(View.GONE);
this.setWebChromeClient(new WebChromeClientEx());
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
this.setBackgroundColor(Color.argb(1, 0, 0, 0));
// mWebView.setBackgroundResource(R.drawable.right_angle);
}
}
@Override
public void addJavascriptInterface(Object obj, String interfaceName) {
if (TextUtils.isEmpty(interfaceName)) {
return;
}
// 如果在4.2以上,直接调用基类的方法来注册
if (hasJellyBeanMR1()) {
super.addJavascriptInterface(obj, interfaceName);
} else {
mJsInterfaceMap.put(interfaceName, obj);
}
}
@Override
public void removeJavascriptInterface(String interfaceName) {
if (hasJellyBeanMR1()) {
super.removeJavascriptInterface(interfaceName);
} else {
mJsInterfaceMap.remove(interfaceName);
mJsStringCache = null;
injectJavascriptInterfaces();
}
}
private void loadJavascriptInterfaces() {
this.loadUrl(mJsStringCache);
}
private String genJavascriptInterfacesString() {
if (mJsInterfaceMap.size() == 0) {
mJsStringCache = null;
return null;
}
/*
* 要注入的JS的格式,其中XXX为注入的对象的方法名,例如注入的对象中有一个方法A,那么这个XXX就是A
* 如果这个对象中有多个方法,则会注册多个window.XXX_js_interface_name块,我们是用反射的方法遍历
* 注入对象中的所有带有@JavaScripterInterface标注的方法
*
* javascript:(function JsAddJavascriptInterface_(){
* if(typeof(window.XXX_js_interface_name)!='undefined'){
* console.log('window.XXX_js_interface_name is exist!!');
* }else{
* window.XXX_js_interface_name={
* XXX:function(arg0,arg1){
* return prompt('MyApp:'+JSON.stringify({obj:'XXX_js_interface_name',func:'XXX_',args:[arg0,arg1]}));
* },
* };
* }
* })()
*/
Iterator<Map.Entry<String, Object>> iterator = mJsInterfaceMap.entrySet().iterator();
// Head
StringBuilder script = new StringBuilder();
script.append("javascript:(function JsAddJavascriptInterface_(){");
// Add methods
try {
while (iterator.hasNext()) {
Map.Entry<String, Object> entry = iterator.next();
String interfaceName = entry.getKey();
Object obj = entry.getValue();
createJsMethod(interfaceName, obj, script);
}
} catch (Exception e) {
e.printStackTrace();
}
// End
script.append("})()");
return script.toString();
}
private void createJsMethod(String interfaceName, Object obj, StringBuilder script) {
if (TextUtils.isEmpty(interfaceName) || (null == obj) || (null == script)) {
return;
}
Class<? extends Object> objClass = obj.getClass();
script.append("if(typeof(window.").append(interfaceName).append(")!='undefined'){");
if (DEBUG) {
script.append(" console.log('window." + interfaceName + "_js_interface_name is exist!!');");
}
script.append("}else {");
script.append(" window.").append(interfaceName).append("={");
// Add methods
Method[] methods = objClass.getMethods();
for (Method method : methods) {
String methodName = method.getName();
// 过滤掉Object类的方法,包括getClass()方法,因为在Js中就是通过getClass()方法来得到Runtime实例
if (filterMethods(methodName)) {
continue;
}
script.append(" ").append(methodName).append(":function(");
// 添加方法的参数
int argCount = method.getParameterTypes().length;
if (argCount > 0) {
int maxCount = argCount - 1;
for (int i = 0; i < maxCount; ++i) {
script.append(VAR_ARG_PREFIX).append(i).append(",");
}
script.append(VAR_ARG_PREFIX).append(argCount - 1);
}
script.append(") {");
// Add implementation
if (method.getReturnType() != void.class) {
script.append(" return ").append("prompt('").append(MSG_PROMPT_HEADER).append("'+");
} else {
script.append(" prompt('").append(MSG_PROMPT_HEADER).append("'+");
}
// Begin JSON
script.append("JSON.stringify({");
script.append(KEY_INTERFACE_NAME).append(":'").append(interfaceName).append("',");
script.append(KEY_FUNCTION_NAME).append(":'").append(methodName).append("',");
script.append(KEY_ARG_ARRAY).append(":[");
// 添加参数到JSON串中
if (argCount > 0) {
int max = argCount - 1;
for (int i = 0; i < max; i++) {
script.append(VAR_ARG_PREFIX).append(i).append(",");
}
script.append(VAR_ARG_PREFIX).append(max);
}
// End JSON
script.append("]})");
// End prompt
script.append(");");
// End function
script.append(" }, ");
}
// End of obj
script.append(" };");
// End of if or else
script.append("}");
}
/**
* 过滤掉Object类的方法
* @param methodName
* @return
*/
private boolean filterMethods(String methodName) {
for (String method : mFilterMethods) {
if (method.equals(methodName)) {
return true;
}
}
return false;
}
/**
* 重新注入js接口
* @param webView
*/
private void injectJavascriptInterfaces(WebView webView) {
if (webView instanceof BaseWebView) {
injectJavascriptInterfaces();
}
}
/**
* 注入js接口
*/
private void injectJavascriptInterfaces() {
if (!TextUtils.isEmpty(mJsStringCache)) {
loadJavascriptInterfaces();
return;
}
String jsString = genJavascriptInterfacesString();
mJsStringCache = jsString;
loadJavascriptInterfaces();
}
/**
* 版本号大于3.0
* @return
*/
private boolean hasHoneycomb() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
}
/**
* 版本号大于4.2
* @return
*/
private boolean hasJellyBeanMR1() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1;
}
@Override
public void setWebChromeClient(WebChromeClient client) {
if (client instanceof WebChromeClientEx) {
super.setWebChromeClient(client);
}else {
throw new IllegalArgumentException("Don't override this method!");
}
}
private void removeSearchBoxImpl() {
if (hasHoneycomb() && !hasJellyBeanMR1()) {
//移除系统开放的js接口
removeJavascriptInterface("searchBoxJavaBridge_");
removeJavascriptInterface("accessibility");
removeJavascriptInterface("accessibilityTraversal");
}
}
private boolean handleJsInterface(WebView view, String url, String message, String defaultValue,
JsPromptResult result) {
String prefix = MSG_PROMPT_HEADER;
if (!message.startsWith(prefix)) {
return false;
}
String jsonStr = message.substring(prefix.length());
try {
JSONObject jsonObj = new JSONObject(jsonStr);
String interfaceName = jsonObj.getString(KEY_INTERFACE_NAME);
String methodName = jsonObj.getString(KEY_FUNCTION_NAME);
JSONArray argsArray = jsonObj.getJSONArray(KEY_ARG_ARRAY);
Object[] args = null;
if (null != argsArray) {
int count = argsArray.length();
if (count > 0) {
args = new Object[count];
for (int i = 0; i < count; ++i) {
args[i] = argsArray.get(i);
}
}
}
if (invokeJSInterfaceMethod(result, interfaceName, methodName, args)) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
result.cancel();
return false;
}
private boolean invokeJSInterfaceMethod(JsPromptResult result,
String interfaceName, String methodName, Object[] args) {
boolean succeed = false;
final Object obj = mJsInterfaceMap.get(interfaceName);
if (null == obj) {
result.cancel();
return false;
}
Class<?>[] parameterTypes = null;
int count = 0;
if (args != null) {
count = args.length;
}
if (count > 0) {
parameterTypes = new Class[count];
for (int i = 0; i < count; ++i) {
parameterTypes[i] = getClassFromJsonObject(args[i]);
}
}
try {
Method method = obj.getClass().getMethod(methodName, parameterTypes);
Object returnObj = method.invoke(obj, args); // 执行接口调用
boolean isVoid = returnObj == null || returnObj.getClass() == void.class;
String returnValue = isVoid ? "" : returnObj.toString();
result.confirm(returnValue); // 通过prompt返回调用结果
succeed = true;
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
result.cancel();
return succeed;
}
private Class<?> getClassFromJsonObject(Object obj) {
Class<?> cls = obj.getClass();
// js对象只支持int boolean string三种类型
if (cls == Integer.class) {
cls = Integer.TYPE;
} else if (cls == Boolean.class) {
cls = Boolean.TYPE;
} else {
cls = String.class;
}
return cls;
}
public class WebChromeClientEx extends WebChromeClient {
@Override
public void onProgressChanged(WebView view, int newProgress) {
if (isShowProgressBar) {
if (newProgress == 100) {
mProgressbar.setVisibility(View.GONE);
} else {
if (mProgressbar.getVisibility() == View.GONE)
mProgressbar.setVisibility(View.VISIBLE);
mProgressbar.setProgress(newProgress);
}
}
injectJavascriptInterfaces(view);
super.onProgressChanged(view, newProgress);
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result)
{
if (view instanceof BaseWebView) {
if (handleJsInterface(view, url, message, defaultValue, result)) {
return true;
}
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}
@Override
public final void onReceivedTitle(WebView view, String title) {
injectJavascriptInterfaces(view);
}
}
public class WebViewClientEx extends android.webkit.WebViewClient{
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if(url.startsWith("tel:")) {
Intent intent = new Intent("android.intent.action.DIAL", Uri.parse(url));
mContext.startActivity(intent);
} else {
view.loadUrl(url);
}
return true;
}
@Override
public void onLoadResource(WebView view, String url) {
injectJavascriptInterfaces(view);
super.onLoadResource(view, url);
}
@Override
public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {
injectJavascriptInterfaces(view);
super.doUpdateVisitedHistory(view, url, isReload);
}
@Override
public void onPageFinished(WebView view, String url) {
injectJavascriptInterfaces(view);
if (null!= mCallBackInterface&&null!=mContext)
mCallBackInterface.onPageLoadFinished();
super.onPageFinished(view,url);
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
// 不要使用super,否则有些手机访问不了,因为包含了一条 handler.cancel()
// super.onReceivedSslError(view, handler, error);
// 接受所有网站的证书,忽略SSL错误,执行访问网页
handler.proceed();
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
// TODO Auto-generated method stub
if (null!= mCallBackInterface&&null!=mContext)
mCallBackInterface.onPageLoadStarted();
if (url.endsWith(".apk")) {
// download(url);//下载处理
}
injectJavascriptInterfaces(view);
super.onPageStarted(view, url, favicon);
}
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
if (null!= mCallBackInterface&&null!=mContext)
mCallBackInterface.onPageLoadError(description);
//这里判断是否是网络断开连接引起的错误,可以添加自己的处理
if (NetworkUtils.isAvailable(mContext)) {
return;
}
super.onReceivedError(view, errorCode, description, failingUrl);
}
}
}
package XXX;
/**
* Created by agehua on 2016/5/26.
*/
public interface NativeAndJsCallBackInterface {
void onPageLoadFinished();
void onPageLoadStarted();
void onActivityViewDestory();
void onPageLoadError(String description);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment