news 2026/9/15 14:09:43

第三方H5页面自定义随机安全键盘:WebView注入与原生键盘叠层实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第三方H5页面自定义随机安全键盘:WebView注入与原生键盘叠层实战

1. 第三方H5页面里的密码框,到底藏着什么风险

先还原一个真实场景。你手上有一个银行系或证券系的App,里面嵌了不少合作方的H5页面——比如贷款申请页、开户资料页、积分兑换页。这些页面是第三方团队维护的,你们拿不到源码,改不了里面的input标签,但监管和用户都把“密码安全”这笔账算在App头上。

问题就出在密码框本身。系统输入法上屏时,会经过输入法进程,第三方输入法完全有能力记录键盘事件;就算用户装了系统输入法,Android的输入法框架本身也存在被hook的风险。更重要的是,系统键盘没有随机性,按键位置固定,被偷窥、录屏、屏幕共享时等于把密码直接送出去。这两年金融类App上安全键盘几乎是标配,但第三方H5页面往往来不及改造,或者根本不愿意配合改造,只能由宿主App兜底。

我当时接手这个需求时,业务方给的约束很明确:H5页面不能动,只能从WebView这一层想办法;密码框是标准的<input type="password">,可能有多个,可能是动态渲染出来的;键盘必须每次弹出时随机排列数字。这就是标题里“第三方H5页面”和“自定义随机键盘”两个关键词的由来。

这篇文章把我实际做这套方案时的选型逻辑、注入脚本、原生键盘层、值回填机制和兼容性坑逐一讲清楚。不论你用的是原生Android WebView还是uni-app、Flutter里封装的WebView容器,核心思路都能迁移。有些坑是文档里查不到的,我尽量把排查过程和背后原因说透。

2. 两条技术路线对比:纯JS键盘 vs 原生键盘叠层

需求确定后,第一个问题是键盘做在哪一层。当时我评估了两条路线。

路线A是纯JS方案,在H5页面里注入一套HTML+CSS的随机键盘DOM。这套键盘直接附着在页面内,点击时通过JS往input里填值。优点是实现简单、跨平台一致,iOS和Android共用一套JS;缺点也很致命:第三方页面的脚本随时可能操作DOM,键盘层会被页面样式干扰;H5页面发生路由跳转或框架重新渲染时,键盘DOM可能被清掉;而且键盘本身画在页面上,视频录制、页面快照都能直接拍到键盘内容,防截屏的作用大打折扣。另一个隐形问题是,如果H5页面本身有CSP(内容安全策略)限制,通过evaluateJavascript注入内联脚本和样式时有些WebView内核会执行时处理得比较别扭,排查成本不低。

路线B是原生键盘叠层方案。注入的JS只做一件事——找到密码框、监听focus/blur事件、把焦点状态告诉原生层;原生层在WebView之上盖一个自定义键盘View,按键点击后通过JS接口把值回填到H5的input里。这个方案的好处是键盘完全脱离页面DOM,不受页面脚本干扰;键盘本体是原生View,天然支持FLAG_SECURE防截屏;随机键盘逻辑在Java层维护,每次弹出重新洗牌,第三方H5拿不到任何键盘事件。

我最终选了路线B,但这里必须说清楚一个前置条件:WebView的JS注入能力是整套方案的地基。如果页面禁用了JavaScript,或者WebView的JavaScriptEnabled没有开启,后面所有步骤都无从谈起。所以拿到需求后第一件事不是写键盘,而是确认目标WebView允许注入JS,且第三方H5页面没有显式禁用。

两条路线的核心差异可以归结为一张表:

对比维度纯JS键盘原生键盘叠层
实现成本中高
防页面脚本干扰
防截屏/录屏强(配合FLAG_SECURE)
跨平台一致性各自实现
针对动态DOM的健壮性中等较高
后续维护成本中(JS被页面更新破坏)低(原生层稳定)

生产环境里我强烈建议走路线B。虽然原生侧多写几百行代码,但换来的是“键盘不再受制于人”。

3. 注入脚本与焦点劫持:核心链路与关键代码

3.1 注入时机与去重

注入JS的时机是个很容易踩坑的点。onPageFinished是最多人用的回调,但它有个问题:页面里如果有持续加载的异步资源,或者H5本身就是一个单页应用(SPA),onPageFinished可能在DOM完全就绪前就回调了,也可能在一次完整的页面生命周期里被触发多次(比如页面内部发生了hash路由变化)。

我采用的策略是:在onPageFinished里先执行幂等注入。所谓幂等,就是脚本通过一个全局标记保证只生效一次,比如在window对象上挂一个window.__secureKeyboardInjected__属性,注入脚本开头先检查这个标记,已经存在就直接跳过,避免同一个页面上重复绑定focus监听导致原生键盘弹出两次。

这是注入脚本的骨架:

(function() { if (window.__secureKeyboardInjected__) { return; } window.__secureKeyboardInjected__ = true; // ...绑定逻辑 })();

3.2 密码框的识别与focus/blur事件绑定

识别密码框最直接的方式是input[type="password"]。但现实项目中会遇到几种变体:有的H5为了自定义样式,会把type设成text,再通过CSS样式把字符遮住;有的会用input事件自己拼掩码。如果只认type="password",这些变体密码框就漏了。

考虑到这次需求明确限定是“文本密码框”,即标准type="password",我先把标准场景做扎实。但代码里可以预留扩展点,把选择器统一收口到一个数组里:

var PASSWORD_SELECTORS = 'input[type="password"]';

以后遇到变体,只需要往这个数组里加规则。

绑定focus/blur的代码:

function hookPasswordInput(input) { if (input.__secureKeyboardHooked__) { return; } input.__secureKeyboardHooked__ = true; input.addEventListener('focus', function() { // 通知原生层:当前密码框获得了焦点 if (window.SecureKeyboardBridge && window.SecureKeyboardBridge.onInputFocus) { window.SecureKeyboardBridge.onInputFocus(input.__secureKeyboardId__, getInputPosition(input)); } }); input.addEventListener('blur', function() { if (window.SecureKeyboardBridge && window.SecureKeyboardBridge.onInputBlur) { window.SecureKeyboardBridge.onInputBlur(); } }); }

这里有个细节:每个input需要有一个唯一ID。第三方页面的input大概率没有id属性,或者id是重复的,所以注入脚本要在绑定时自己分配一个自增ID,同时把input引用缓存到一个Map里。原生层后续回填值时,只需要传这个ID,JS侧通过ID找到对应的DOM元素。这样比用document.activeElement更可靠,因为键盘弹出后焦点状态可能被干扰。

3.3 原生层收到focus事件后的三件事

JS通知原生会通过WebView.addJavascriptInterface注入的桥接对象。原生onInputFocus回调里必须同步做三件事:

第一,隐藏系统输入法。这是整个方案成败的关键步骤。Android的WebView内部有一个WebViewClassicWebViewChromium持有的输入框(EditText子类),当我们不做任何处理时,点击密码框系统输入法会自动弹出来。要禁用它,最有效的手段是把这个内部输入框标记为showSoftInputOnFocus(false)

通过反射拿WebView内部输入框的代码大致是这样:

private void disableSoftInput(WebView webView) { try { Class<?> clazz = Class.forName("android.webkit.WebViewClassic"); Field field = clazz.getDeclaredField("mInputConnection"); field.setAccessible(true); // 真实项目中这一层反射在不同ROM上差异较大,建议加try-catch降级 } catch (Throwable t) { Log.w("SecureKeyboard", "disableSoftInput reflect failed", t); } }

这段反射代码在现代Android版本上已经不太可靠,因为WebView内核发生了变化。更稳妥的做法是双管齐下:

  • onFocus回调里直接调用InputMethodManager.hideSoftInputFromWindow()隐藏系统键盘;
  • 在注入JS时,对密码框设置readonly属性。只读输入框不会唤起系统输入法,这是浏览器内核层面的默认行为。

设置readonly有一个副作用:用户看起来光标还是有的,但H5页面自身的脚本如果读取input.readOnly属性,可能会做一些额外判断。因此我在注入脚本里用了一个更隐蔽的方式——设置readonly的同时,在focus事件里把焦点重新拉回这个input,让H5感知不到异常:

input.setAttribute('readonly', 'true'); input.addEventListener('mousedown', function(e) { e.preventDefault(); });

mousedownpreventDefault能挡住鼠标点击事件默认给input带来的焦点转移,确保焦点始终停留在密码框上,这样selectionStartselectionEnd才能正常读取。

第二,记录当前密码框的身份和位置。JS桥接回调里会传过来input.__secureKeyboardId__和位置信息,原生层把它们存成成员变量,键盘弹出后按这个ID做回填。位置信息主要用于确定键盘在屏幕上的弹出位置,但由于密码框通常在页面中间高度,键盘可以直接用底部弹出的方式,位置计算其实用不太多。位置信息更多的价值在于调试时定位“哪个input触发了focus”。

第三,弹出原生随机键盘。这一步放在焦点记录之后,避免键盘比键盘层先出现导致闪烁。

3.4 动态DOM怎么办:MutationObserver兜底

现代H5页面几乎没有纯静态的。React、Vue这类框架会根据用户操作动态渲染表单;有些页面在用户切换tab后才生成密码框;甚至有的页面在setTimeout里延迟创建input。如果只在注入时遍历一次DOM,动态出现的密码框就漏了。

因此注入脚本里必须挂一个MutationObserver,监听页面DOM结构变化,对新出现的密码框补绑事件:

var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { var addedNodes = mutation.addedNodes; for (var i = 0; i < addedNodes.length; i++) { var node = addedNodes[i]; if (node.nodeType !== 1) continue; if (node.matches(PASSWORD_SELECTORS)) { hookPasswordInput(node); } var inputs = node.querySelectorAll ? node.querySelectorAll(PASSWORD_SELECTORS) : []; for (var j = 0; j < inputs.length; j++) { hookPasswordInput(inputs[j]); } } }); }); observer.observe(document.documentElement, { childList: true, subtree: true });

注意:监听的是document.documentElement而不是document.body,因为有些页面在body还没生成时就开始了脚本执行。subtree: true必须加,否则子节点变化可能观察不到。

MutationObserver本身也有性能开销,我在实际项目中给observer加了一个简单的“积压-批量处理”策略:当一次mutations数组长度超过50时,主动合并去重再遍历,避免短时间内大量DOM变更导致绑卡顿。不过对密码框场景来说,大多数页面不会频繁增删密码框,基本没有性能压力。

4. 随机键盘本体:UI设计、洗牌算法与防泄露加固

4.1 键盘布局与自适应宽度

原生侧的键盘View我用一个自定义LinearLayout实现,最外层是垂直排列的两行:上面是数字区,下面是功能键区。数字区用GridLayout,列数设为5,每行放5个按键,这样10个数字刚好两行,功能键(删除、清空、收起)可以放在第三行。

屏幕宽度通过getResources().getDisplayMetrics().widthPixels获取,每个按键的宽度按屏幕宽度除以5计算,高度统一设为屏幕宽度的六分之一左右,大拇指按压时不会觉得局促。这里有个小细节:按键之间一定要留1~2dp的间距,否则快速连续点击时极容易误触到相邻键。

按键的字体大小推荐sp单位且不小于20sp,因为金融类App用户很多是中老年用户,字太小会被吐槽。

4.2 Fisher-Yates洗牌:每次弹出都是新布局

随机键盘的核心是“随机”。每次键盘弹出时,十个数字的排列顺序都不同。如果用一个固定的排列顺序,键盘就失去了抗偷窥的意义。

洗牌用经典的Fisher-Yates算法,从数组末尾往前遍历,每次随机选一个位置交换。这个算法O(n)时间复杂度,均匀性好,不会出现某些排列组合概率偏高的问题。实现如下:

private static final int[] DIGITS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; private int[] shuffledDigits() { int[] arr = Arrays.copyOf(DIGITS, DIGITS.length); Random random = new Random(); for (int i = arr.length - 1; i > 0; i--) { int j = random.nextInt(i + 1); int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } return arr; }

Random类的实例可以复用,不要每次洗牌都new一个,否则在高频点击场景下可能产生相同的随机序列。每个按键构建时,根据洗牌结果重新设置TextView的文本。

4.3 FLAG_SECURE防截屏与防录屏

随机键盘防的是“眼睛”,FLAG_SECURE防的是“摄像头”。

在键盘View显示时,把整个Activity的Window加上FLAG_SECURE

getActivity().getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);

加上这个flag后,系统截屏返回的内容是黑色或空白,主流的录屏软件也录不到有效内容。Android 10及以上部分系统录屏会弹出“当前应用禁止截屏”的提示,对金融App来说这个提示反而是一种合规信号,能震慑别有用心的人。

需要特别注意的是,FLAG_SECURE是一种全窗口级别的保护,一旦设置,整个Activity的截屏都会被禁止,而不只是键盘区域。如果你不想让整个页面都禁止截屏——比如页面其他地方有分享需求——那就要考虑用SurfaceView实现键盘,或者把键盘放到一个单独的Dialog窗口里。这里我踩过一个坑:Dialog设置FLAG_SECURE只能保护Dialog自己的窗口,主窗口不受影响,但Dialog的显示层级和WebView叠加时的焦点控制比较麻烦,所以最终我还是选择了全窗口FLAG_SECURE。

键盘像素层的防泄露还有两个小点:按键字体不要用太花哨的字体,统一用系统默认字体,毕竟防的是泄露不是美观;按键不要有涟漪之外的高光反馈,涟漪动画默认是半透明的,录屏时不会把数字信息带出来。

4.4 生命周期与键盘回收

键盘View的生命周期必须和Activity绑定。我在onPause()里强制隐藏键盘,onDestroy()里把键盘View从父容器中移除并置空。否则Activity切到后台再回来时,键盘可能悬挂在屏幕上,而且焦点状态已经乱了。

有一种极端场景:用户在密码框获得焦点后,点击了H5页面里的某个链接,触发了页面跳转。这时blur事件通常会被触发,但页面跳转过程中WebView正在销毁旧的document,注入脚本里的blur回调可能来不及执行。因此不能只依赖JS的blur通知,原生层还要监听WebView的onPageStarted回调,在页面开始跳转时立即隐藏键盘并清空焦点记录。

5. 值回填的完整链路:从按键到H5表单数据同步

5.1 原生按键到JS插值的调用链

用户点击随机键盘上的数字“7”,事件经过Java层的OnClickListener,最终要落到H5页面里当前焦点密码框的value上。这一条链路看起来就一行evaluateJavascript,但里面有三层细节必须处理干净。

首先是取当前焦点密码框的句柄。我在前面提到,JS侧维护了一个__secureKeyboardMap__,键是自增ID,值是DOM元素。原生侧存了当前焦点ID,所以调用链是:

Java: 拿到当前焦点ID(比如 "3") -> evaluateJavascript("window.__secureKeyboard__.insertChar('7', '3')") -> JS侧找到id为3的input -> 读取selectionStart/selectionEnd -> 在光标处插入字符 -> 触发input事件 -> 更新光标位置

JS侧的核心实现:

window.__secureKeyboard__ = { currentId: null, insertChar: function(ch, id) { var input = this.getInputById(id); if (!input) return; var start = input.selectionStart; var end = input.selectionEnd; var val = input.value; var newValue = val.substring(0, start) + ch + val.substring(end); // 关键:使用原型上的原生setter,触发框架的value变更检测 var protoSetter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, 'value').set; protoSetter.call(input, newValue); // 同步光标位置:插入后光标移动到插入字符的后面 input.selectionStart = input.selectionEnd = start + 1; // 触发事件,让H5框架感知到值变化 input.dispatchEvent(new Event('input', { bubbles: true })); }, removeChar: function(id) { var input = this.getInputById(id); if (!input) return; var start = input.selectionStart; var end = input.selectionEnd; if (start === 0 && end === 0) return; var newValue; if (start === end) { // 没有选中文本时,删除光标前一个字符 newValue = input.value.substring(0, start - 1) + input.value.substring(end); start = start - 1; } else { // 有选中文本时,删除整个选区 newValue = input.value.substring(0, start) + input.value.substring(end); } var protoSetter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, 'value').set; protoSetter.call(input, newValue); input.selectionStart = input.selectionEnd = start; input.dispatchEvent(new Event('input', { bubbles: true })); }, clear: function(id) { var input = this.getInputById(id); if (!input) return; var protoSetter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, 'value').set; protoSetter.call(input, ''); input.dispatchEvent(new Event('input', { bubbles: true })); input.focus(); } };

5.2 为什么必须用原型上的原生setter

直接写input.value = newValue行不行?对于纯HTML页面来说行,但对于React或Vue这类框架页面来说不行。框架在初始化时会劫持input元素value属性的setter,如果直接赋值,框架内部的虚拟DOM状态没有同步,用户点提交按钮时框架读取到的是自己维护的state,拿到的还是空字符串,导致“密码明明输入了,提交却提示密码为空”。

解决办法就是上面代码里的“偷鸡”方式:从HTMLInputElement.prototype上拿到框架劫持之前的原始setter,用protoSetter.call(input, newValue)赋值。这一步绕过了框架层的属性劫持,让原生setter直接写到DOM底层值上,同时框架内部的value tracker也能感知到值变化(因为最终还是走了一遍原生setter,触发了对应属性变更)。

这个技巧不是我发明的,React社区早就有人总结过,但在WebView注入键盘回填这个场景里,它直接决定方案能不能在SPA页面上跑通。我第一次接测试反馈“回头页面不正常”就是这个原因。

5.3 dispatchEvent触发input事件

只改value还不够。现在很多H5页面在密码框上绑定了inputchange事件,用于实时校验密码强度、控制提交按钮置灰状态。如果不触发事件,页面UI会出现两种症状:提交按钮永远置灰、密码强度提示永远不更新。

我在insertCharremoveChar方法末尾都dispatchEvent(new Event('input', {bubbles: true}))bubbles: true必须写,因为有些框架是事件委托,事件冒泡到document才被处理。change事件不用手动触发,因为change是在失焦时才触发的,密码框失焦时blur事件会自然带出change。不过为了保险,我在removeChar里也会触发一下input,删除键如果没触发事件,页面的剩余字符数提示可能错乱。

5.4 焦点与光标维护的几个坑

readonly属性在阻止系统键盘的同时,也带来了一个副作用:某些Android WebView版本中,设置了readonly的input在点击时不会自动获得焦点,导致selectionStart获取不到。这个问题的表现是:键盘弹出来了,但输入的字符都跑到输入框最末尾,而不是光标位置。

解决办法是在绑定focus事件时,在setTimeout里把焦点强制拉回来:

input.focus(); setTimeout(function() { input.focus(); try { input.setSelectionRange(input.value.length, input.value.length); } catch (e) {} }, 0);

另外,原生键盘上的删除键逻辑需要同时处理“有选中文本”和“无选中文本”两种情况。上面代码里已经区分了。这里的细节是很多半成品安全键盘会忽略的——用户长按数字想选中一段文本时,如果键盘不支持删除选中区,体验会非常割裂。

6. 动态页面与极端ROM下的兼容性实战

6.1 键盘弹出导致WebView重排的连锁反应

键盘的弹入弹出会让WebView高度发生变化,H5页面内部通常会对window.resize事件做响应,比如把底部按钮顶上来或者调整滚动条位置。在测试中我们发现,部分页面的密码输入框在键盘弹出后会向上滚动一段距离,但原生的键盘View是贴在Window底部的,两者叠加后,密码框反而被键盘挡住了。

这个问题的根源是:键盘View使用WindowManager直接添加时,不影响WebView的高度;但如果用FrameLayout在Activity布局里addView,WebView的高度可能会自适应变化。当时我采用的方案是:无论系统键盘是否弹出,都固定WebView的布局高度不动,键盘View叠加在WebView之上,两者互不干预。H5页面感知不到键盘的存在,视觉上不会出现页面跳动。

具体做法是:用addContentView把键盘View添加到Window的根布局,挂载时记录WebView原有高度,键盘弹出时不让根布局重算高度。

6.2 首次点击二次隐藏的尴尬:系统键盘的延迟显示

隐藏系统输入法时有一个常见问题:hideSoftInputFromWindowonFocus回调里调用,有时隐藏不彻底,系统键盘闪一下又弹回来了。原因是WebView内核自己的焦点请求是异步的,隐藏动作被后来的焦点请求覆盖。

我的处理办法是在onInputFocus里延迟两次执行隐藏,间隔分别是50ms和150ms:

public void onInputFocus(String inputId, int x, int y) { currentInputId = inputId; showSecureKeyboard(); for (int delay : new int[]{50, 150}) { webView.postDelayed(() -> hideSoftInput(), delay); } }

第一次隐藏能抢在系统键盘完全展开前按下去,第二次兜底防止WebView重新唤起。这个“postDelayed双次隐藏”方案在小米、华为、三星等主流机型都验证过,覆盖面够广。

如果遇上某些定制ROM(比如部分OPPO/vivo),两次隐藏仍然效果不佳,还有一个杀手锏:在注入脚本里,对密码框的touchend事件调用preventDefault。这样WebView不会收到点击事件,自然不会触发自身的焦点请求,系统键盘就没有机会弹出来。但这种方法会同时阻止页面的点击聚焦逻辑,需要额外在touchend结束后手动调用input.focus(),否则光标无法闪烁。

6.3 SPA路由切换导致的焦点残留

单页应用路由切换时,document可能整体不刷新,但原密码框所在DOM节点被移除了。此时原生层记录的currentInputId还指向一个不存在的节点,键盘弹出后,用户点击数字键,JS侧getInputById找不到元素,所有输入静默丢失。

这个问题的表象比真实情况要隐蔽——键盘正常弹出、按键有声音反馈、密码框看起来也有焦点,但输入就是不进表单。排查了一整天才确认是路由切换后旧input被卸载,新input虽然出现了但focus事件还没来得及触发。

解决方案是双管齐下:一是在JS侧监听popstate和pushState包装,路由发生变化时主动通知原生层清除焦点记录:

['pushState', 'replaceState'].forEach(function(method) { var original = history[method]; history[method] = function() { original.apply(this, arguments); if (window.SecureKeyboardBridge && window.SecureKeyboardBridge.onPageRouteChange) { window.SecureKeyboardBridge.onPageRouteChange(); } }; }); window.addEventListener('popstate', function() { if (window.SecureKeyboardBridge && window.SecureKeyboardBridge.onPageRouteChange) { window.SecureKeyboardBridge.onPageRouteChange(); } });

二是在getInputById找不到节点时,原生层自动隐藏键盘并清空currentInputId,避免后续点击空转。

6.4 多个密码框切换时的脏状态

一个H5页面上可能出现两个密码框,“输入密码”和“确认密码”。简单实现下,如果用户在第二个密码框上点击时,键盘还停留在第一个密码框的回填ID上,会导致两个框的内容串掉。原因在于原生层只存了一个currentInputId

第二个密码框的focus事件触发后,JS桥接会更新currentInputId为新的ID。这个逻辑本身没问题,但有一个时序陷阱:如果用户从一个密码框直接切换到另一个密码框,旧input的blur事件和新input的focus事件几乎是同时触发的,原生层可能先收到onInputBlur隐藏键盘,再收到onInputFocus重新显示键盘,造成键盘闪烁。

我在onInputBlur里加了一个判断:如果500ms内收到了新的onInputFocus,就不隐藏键盘,直接更新currentInputId。这个“防抖”策略让键盘在密码框之间切换时保持稳定,用户体验顺畅很多。

6.5 内存与性能:注入脚本别铺太大

最后提一下性能。注入到H5页面的JS代码要克制,不要塞进一个完整的jQuery,三五十行搞定核心逻辑就够了。整套方案跑下来,注入脚本执行耗时应该控制在10ms以内。如果页面本身有几千个节点,querySelectorAll遍历会稍微慢一点,但要主动加input.__secureKeyboardHooked__标记避免重复遍历。

原生键盘View的创建也要懒加载。不要每次弹出都重新inflater布局,而是在键盘第一次弹出时创建一次,后续通过setVisibility(VISIBLE/GONE)控制显隐。这样键盘弹出速度能控制在100ms左右,用户几乎感知不到延迟。

结尾:一点实际操作中的个人体会

这套方案落地到现在跑了快半年,线上反馈的键盘相关bug一只手数得过来。回头再看,整个项目的核心难点其实不在键盘UI,而在“JS和原生两个世界之间的状态同步”上。无论焦点、光标、DOM节点还是框架状态,任何一个环节脱节,用户体验都会崩掉。

我自己的习惯是:每次交互都在原生侧打一条点日志——焦点ID是几按键是几插入后光标在哪——配合WebView的evaluateJavascript回调,能快速定位问题出在JS层还是原生层。这类侵入式的监控代码,排查多密码框切换问题时帮了大忙。

如果你想在这个方案上继续扩展,有几个方向可以参考:把键盘的随机范围从数字扩展到字母和特殊符号;在键盘底层接入更多生物识别方案(比如指纹确认后自动填充);把整套注入逻辑封装成一个WebView的扩展库,供多个业务方复用。安全键盘这件事永远没有终点,攻防双方都在进步,但只要思路对,后续迭代的路子就很顺。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 14:09:42

Loop 窗口管理工具使用指南:用径向菜单快速排版 macOS 窗口

Loop 窗口管理工具使用指南&#xff1a;用径向菜单快速排版 macOS 窗口 【免费下载链接】Loop Window management made elegant. 项目地址: https://gitcode.com/GitHub_Trending/lo/Loop Loop 是一款免费、开源的 macOS 窗口管理工具&#xff0c;适配 macOS 13 及以上系…

作者头像 李华
网站建设 2026/9/15 14:09:33

2026最新icp备案网站服务内容避坑指南

2026最新icp备案网站服务内容避坑指南 刚拿到服务器,域名也解析好了,结果在备案页面卡了三天?别急,这种“备案流程一头雾水”的焦虑,我见过太多。很多做市场推广的朋友,技术底子薄,一看到工信部那个复杂的表单就头大。其实,2026年最新的ICP备案逻辑虽然更严,但核心没变:…

作者头像 李华
网站建设 2026/9/15 14:08:59

PLC编程标准到底是什么?ISA88与IEC 61131-3详解

逛工控论坛的时候&#xff0c;经常能看到一类帖子&#xff0c;讨论PLC编程有没有标准&#xff0c;底下一定会有人搬出ISA88&#xff0c;再带上一句“全球公认”。最近有条帖子的标题很有代表性&#xff1a;《0913【万泉河】PLC编程标准&#xff0c;全球公认的标准 ISA88&#x…

作者头像 李华
网站建设 2026/9/15 14:08:57

Java对接海康SDK实现浏览器实时预览与录像回放实战

最近接手了一个园区安防平台的项目&#xff0c;需求本身听起来不复杂&#xff1a;在网页上实时预览海康摄像头画面&#xff0c;同时支持按时间段拉录像回放。但真正落地的过程中&#xff0c;涉及到的选型、取流、转码、前后端配合&#xff0c;问题远比想象中多。如果你也在做 j…

作者头像 李华
网站建设 2026/9/15 14:08:28

MATLAB实现EMD包络谱的滚动轴承故障诊断指南

简介&#xff1a;基于EMD的包络谱故障诊断MATLAB程序实例&#xff0c;是一套面向机械设备状态监测与故障诊断学习者的完整示例工程&#xff0c;用于对非线性、非平稳振动信号进行经验模态分解和包络解调&#xff0c;进而提取滚动轴承等设备的故障特征频率。压缩包共包含15个文件…

作者头像 李华