-
-
Save agehua/99233b40e05db29ee0ed4f50fb2c7530 to your computer and use it in GitHub Desktop.
Android Webview防止js远程代码攻击
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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