分析一次有意思的需求——HTML代码注入

栏目: Html · 发布时间: 6年前

内容简介:有个朋友问了我一个问题:他们通过其实,我之前接触WKWebView并不多,但是这个问题我觉得很有意思。这篇文章便是我解决这个问题的全部思路,与最终的解决办法。

有个朋友问了我一个问题:

他们通过 WKWebView ,访问了一个其他的页面,然后希望原生获得用户的输入信息。

其实,我之前接触WKWebView并不多,但是这个问题我觉得很有意思。这篇文章便是我解决这个问题的全部思路,与最终的解决办法。

思路分析 与 代码实践

这个问题其实很具象了,就是希望原生获得H5的用户输入内容(这样子感觉有些不地道-_-)

接下来我们就需要分析这个需求了。

首先我们先需要抓住两个点,1个是H5,1个是原生。

所以这个问题现在被我拆分出了1个额外的问题

HTML能否和原生交互?如何进行?

这个问题其实是一个很关键的问题,因为我们只有实现了原生和HTML的交互,才能获得相关信息(这里我们假定网页是我们自己写的,完全受操纵于我们自己)

于是便搜寻资料,发现WKWebView提供了一个很方便的交互渠道 WKScriptMessageHandler ,我们通过对 WKWebView 进行相关的定制操作便可以解决。

我们先创建一个工程,这里我把整个工程命名为 InjectHTML

为了防止循环引用,我们先构建一个中间层 ScriptHandler

import Foundation
import WebKit
// 这里我们使用一个中间层来解除循环WebView和Controller间的循环引用问题
class ScriptHandler: NSObject, WKScriptMessageHandler {

    weak var delegate: WKScriptMessageHandler?

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        delegate?.userContentController(userContentController, didReceive: message)
    }

    init(delegate: WKScriptMessageHandler? = nil) {
        self.delegate = delegate
    }
}
复制代码

在ViewController中的代码

import UIKit
import WebKit
class ViewController: UIViewController {
    var webview: WKWebView!

    static let scriptKey = "InjectHTML"

    override func viewDidLoad() {
        super.viewDidLoad()
        //初始化 Configuration
        let configuration = WKWebViewConfiguration()
        configuration.userContentController = WKUserContentController()
        // 给Configuration 增加一个js script处理器
        // 采用了中间层的因素,避免循环引用导致无法释放问题
        configuration.userContentController.add(ScriptHandler(delegate: self), name: ViewController.scriptKey)

        webview = WKWebView(frame: view.bounds, configuration: configuration)

        view.addSubview(webview)
        // 设置导航处理器
        webview.navigationDelegate = self
        // 我们先从本地读网页方便自我改动测试
        let fileURL = Bundle.main.url(forResource: "index", withExtension: "html")
        // 加载网页
        webview.loadFileURL(fileURL!, allowingReadAccessTo: fileURL!)
    }
}

extension ViewController: WKScriptMessageHandler {
    // 遵守协议
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        // 因html可能传递多种类型名称,我们这里须指定key
        if message.name == ViewController.scriptKey {
            guard let dic = message.body as? [String: String] else {
                return
            }
            // 交给真实处理解析函数
            receiveInputValue(para: dic)
        }
    }
    // 解析函数可以负责更具体的内容,因为demo,故此只是打印
    func receiveInputValue(para: [String: String]) {
        let title = para["title"] ?? "无值"

        let message = para["message"] ?? "无值"

        let `id` = para["id"] ?? "无值"

        print("title: \(title)")
        print("message: \(message)")
        print("id: \(id)")
    }
}
// 遵循导航协议,方便我们知道何时网页加载完成
extension ViewController: WKNavigationDelegate {
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {

    }
}
复制代码

这样子我们的ViewController 就构建完成,访问了一个本地index.html的网页。

我们在项目中新建一个html文件(从外界拖入也可)

html页面只用于测试,因此我们也就不做类似自适应之类的css样式了

html代码如下:

<html>
    <head>
        <title>注入测试网页</title>
    </head>
    <body bgcolor="#FFFFFF">
        <h1>Test for inject js</h1>
        <!--添加一个button按钮用以测试点击事件-->
        <button onclick="onClickTest()">Test Button</button>
    </body>
    <script type="text/javascript">
        function onClickTest() {
            // 注意,这里是需要注意的是我们在ViewController中定义的Script Name需要作为messageHandler的一个属性
            window.webkit.messageHandlers.InjectHTML.postMessage({title: 'test title', message:'test message', id: 'test id'})
            // 若我们未注册此名称,则无法触发对应回调,postMessage中的参数可传为任意的,但我们在原生中定义为字典了,则我们在这里需要传入字典
            window.webkit.messageHandlers.InjectHTMLS.postMessage({title: 'test title', message:'test message', id: 'test id'})
        }
    </script>
</html>
复制代码

其中对应部分均已加上注释,接下来我们来跑一遍测试结果:

title: test title
message: test message
id: test id

当我们点击WebView上的按钮时候,我们可以打印出来对应的js端返回结果,说明成功了。

所以,这个需求我们可以说解析了3分之1了。

接下来我们需要继续分析了,这个需求需要我们能监控所有的输入控件。

所以我们需要从网页端剖析了,问题回归HTML端。

我们继续拆解问题,我们既然要监控所有的输入控件,那么我们首先就得知道,我们能否获得所有的输入控件(甚至说,我们能否获得页面的所有控件,然后进行遍历过滤也可以)

HTML如何获取指定元素

这个问题,通过搜索与询问前端朋友得知:

HTML提供了对应的Api,直接获取指定标签的内容,因为我们是要获得输入框,输入框在HTML中的标签是 input 。所以我们获得页面中所有输入框元素的方法就已经出来了

// 获得所有input数组
var inputs = document.getElementsByTagName("input")
复制代码

那么问题就迎刃而解了,然后我们还是需要测试一下,毕竟万一这个方法不好用怎么办

这里就不在手机端进行测试,因为如果模拟器还是比较费事的,并且我们如果想打印console日志的话,看起来也不那么容易,因此我们将接下来的测试网页步骤,直接挪到Google Chrome上。

这里还是给出对应的html测试代码

<html>
    <head>
        <title>注入测试网页</title>
    </head>
    <body bgcolor="#FFFFFF">
        <h1>Test for inject js</h1>
        <!--添加一个button按钮用以测试点击事件-->
        <button onclick="onClickTest()">Test Button</button>

        <input type="text" placeholder="Test Input1">
        <input type="text" placeholder="Test Input2">
    </body>
    <script type="text/javascript">
        // 获得所有input数组
        var inputs = document.getElementsByTagName("input");
        // 打印input数组
        console.log(inputs)
    </script>
</html>
复制代码

然后使用浏览器(下文浏览器均为Google Chrome)打开。

我们按Command+Shift+c即可打开页面审查和网页控制台,在这里我们看见了我们执行网页的log,打印结果如下

分析一次有意思的需求——HTML代码注入

说明我们获取input成功了

接下来我们只需要进行过滤即可获得我们需要的页面元素了(如过滤checkbox之类的)

然后接下来新的问题来了

JS代码如何动态添加事件

我们已经通过js代码获得input了,所以问题就变到了如何添加点击事件。毕竟原生的输入框还有至少监听输入事件,或者监听输入完成类的,那么JS端理应也存在。

这里感谢菜鸟教程,查询到了一个函数 addEventListener

这个函数的功能就是给HTTP的DOM元素增加对应的事件,也就是说我们可以通过这个方法额外的增加点击事件。

同时,新增加的事件并不会覆盖原有事件,一个元素可以拥有多个同样的事件(如一个按钮可以同时出发两个onClick事件)

这个函数给了我们新的天地啊。因此我们可以给input增加事件,这里我选择了两个事件,一个是 input 事件(输入框文字发生变化),一个是 change 事件(输入框失去焦点)

我们修改上文的html代码,修改结果如下:

<html>
    <head>
        <title>注入测试网页</title>
    </head>
    <body bgcolor="#FFFFFF">
        <h1>Test for inject js</h1>
        <!--添加一个button按钮用以测试点击事件-->
        <button onclick="onClickTest()">Test Button</button>
        <!--这里给input增加一些回调-->
        <input type="text" placeholder="Test Input1" onchange="onChange()" oninput="onInput()">
        <input type="text" placeholder="Test Input2" onchange="onChange()" oninput="onInput()">
    </body>
    <script type="text/javascript">
        // 这里是为了测试原有对应事件是否会被覆盖
        function onChange(input) {
            console.log("原有 失去焦点");
            console.log(input);
        }

        // 这里是为了测试原有对应事件是否会被覆盖
        function onInput(input) {
            console.log("原有 键盘输入");
            console.log(input);
        }
    </script>
    <script type="text/javascript">
        function demoOnchange(input) {
            console.log("失去焦点");
            console.log(input);
        }
        function demoOnInput(input) {
            console.log("键盘输入");
            console.log(input);
        }
        function demoSet() {
            var inputs=document.getElementsByTagName("input");
            for(var i=0;i < inputs.length;i++) {
                var input = inputs[i];
                // 这里我们增加一些过滤条件,因为我们有时并不需要所有的input,这里我只是允许了text(文字输入框)
                if(input.type=="text") {
                    input.addEventListener('change', demoOnchange);
                    input.addEventListener('input', demoOnInput)
                }
            }
        }
        demoSet();
    </script>
</html>
复制代码

继续进入浏览器测试网址,我们可以看到,当我们输入的时候,同时触发了两个回调,说明后期注入有效

接下来我们需要继续考虑,我们需要能让原生接收到对应事件啊

原生如何收到对应事件

我们在上文中已经知道了js如何调用原生,那么这一步也就我们也就可以直接通过结合便可以实现了。

这一步还是更改html代码

<html>
    <head>
        <!--HTML页面内容需告知为utf8,否则会出现乱码问题-->
        <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
        <title>注入测试网页</title>
    </head>
    <body bgcolor="#FFFFFF">
        <h1>Test for inject js</h1>
        <!--添加一个button按钮用以测试点击事件-->
        <button onclick="onClickTest()">Test Button</button>
        <!--这里给input增加一些回调-->
        <input type="text" placeholder="Test Input1" onchange="onChange()" oninput="onInput()">
        <input type="text" placeholder="Test Input2" onchange="onChange()" oninput="onInput()">
    </body>
    <script type="text/javascript">
        // 这里是为了测试原有对应事件是否会被覆盖
        function onChange(input) {
            console.log("原有 失去焦点");
            console.log(input);
        }

        // 这里是为了测试原有对应事件是否会被覆盖
        function onInput(input) {
            console.log("原有 键盘输入");
            console.log(input);
        }
    </script>
    <script type="text/javascript">
        function demoOnchange(input) {
            // 这里,input是一个点击事件,target才是真正的input元素,我们可以通过此任意获取input的相关信息,如class,id以及其他的一些信息等等
            window.webkit.messageHandlers.InjectHTML.postMessage({title: '输入框失去焦点', message:input.target.value, id: input.target.id});
        }
        function demoOnInput(input) {
            window.webkit.messageHandlers.InjectHTML.postMessage({title: "输入框正在输入", message:input.target.value, id: input.target.id});
        }
        function demoSet() {
            var inputs=document.getElementsByTagName("input");
            for(var i=0;i < inputs.length;i++) {
                var input = inputs[i];
                // 这里我们增加一些过滤条件,因为我们有时并不需要所有的input,这里我只是允许了text(文字输入框)
                if(input.type=="text") {
                    input.addEventListener('change', demoOnchange);
                    input.addEventListener('input', demoOnInput)
                }
            }
        }
        demoSet();
    </script>
</html>
复制代码

重新启动刚才的项目App,在输入框中输入,可以看到控制台中返回了数据

title: 输入框正在输入
message: 1
id:
title: 输入框失去焦点
message: 1
id:

我们的打印出来了。到这里我们自有网页的测试全部通过,那么就剩下最后一步了,如何在三方网页上执行?

JS代码注入

WKWebView既然能让js调用OC,那么OC能否调用js代码呢?

答案是可以的,WKWebView提供给我们原生的方法可以动态的执行js代码

webview.evaluateJavaScript(someTest, completionHandler: closure)
复制代码

这个方法可以让我们动态的执行由原生生成的js代码。

那么我们需要执行什么方法呢?

自然就是我们上述研究出来的获取HTML的元素并增加事件回调的事情啊。

这里我们在项目中新建一个js文件,方便我们以后修改js代码,名称就叫 Inject.js

从项目中的html文件中把如下方法复制进来

function demoOnchange(input) {
    window.webkit.messageHandlers.InjectHTML.postMessage({title: "输入框失去焦点", message:input.target.value, id: input.target.id});
}
function demoOnInput(input) {
    window.webkit.messageHandlers.InjectHTML.postMessage({title: "输入框正在输入", message:input.target.value, id: input.target.id});
}
function demoSet() {
    var inputs=document.getElementsByTagName("input");
    for(var i=0;i < inputs.length;i++) {
        var input = inputs[i];
                
        if(input.type=="text") {
            input.addEventListener('change', demoOnchange);
            input.addEventListener('input', demoOnInput)
        }
    }
}
demoSet();
复制代码

然后我们决定采用直接从项目中读取文件的形式将js文件转成字符串,我们在ViewController中的

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)

函数中加入js代码注入,使我们每次加载成功网页都注入对应的js代码

完整函数如下:

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    guard let jsString = try? String(contentsOfFile: Bundle.main.path(forResource: "Inject", ofType: "js") ?? "") else {
            // 没有读取出来则不执行注入
        return
    }
    // 注入语句
    webView.evaluateJavaScript(jsString, completionHandler: { _, _ in
        print("代码注入成功")
    })
}
复制代码

我们将应用程序中的 index.html 网页中对应的js代码删除。使这个网页更像是一个三方网页(不会主动支持该功能)。这里,就不再详述HTML代码了。

我们执行测试后发现,测试网页回调成功

三方网页集成测试

当我们以三方网页测试的时候,便会发现这样或者那样的问题。

我们以 掘金 为例,试试我们的自有脚本。我们的js代码在掘金网上的登录竟然失效了。

但是作为程序,函数是具有幂等性的(在相同情况下进行无限次的操作,结果一定相同)。那么只能是我们的环境出现了问题,而不是我们的js代码失败了。

所以我们需要观察环境究竟哪里出现问题了。

首先在我们的自有测试网站上,输入框是直接就存在的。但是在掘金上却不是,它需要我们点击登录按钮后作为弹框出现。而我们的js代码注入是在网页渲染完成,因此我们接下来尝试给我们的页面增加一个按钮。点击的时候再进行js代码注入

为了简化代码,我们给ViewController加入了一个导航栏,并在导航栏右上角增加一个 注入 按钮。(导航控制器通过StoryBoard增加)

我们在ViewController的 viewDidLoad 中增加如下代码:

self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "注入", style: .plain, target: self, action: #selector(injectJS))
复制代码

同时在类中增加如下方法

@objc func injectJS() {
    guard let jsString = try? String(contentsOfFile: Bundle.main.path(forResource: "Inject", ofType: "js") ?? "") else {
        // 没有读取出来则不执行注入
        return
    }
    // 注入语句
    webview.evaluateJavaScript(jsString, completionHandler: { _, _ in
        print("代码注入成功")
    })
}
复制代码

重新运行代码

发现头部被挡在导航栏下面了-_-

继续改尺寸(代码就不列出来了)

这阵我们点击网页上的 登录 按钮,随后点击导航栏上 注入

然后再进行输入,我们就可以看见控制台打印出我们正在输入的账户名和密码了-_-

如果你问我怎么自动注入?我给的思路就是获取到登录的元素,然后再多做一步回调,我们可以先注入一次js再注入我们的目标js。

但是具体怎么实现么,不好意思,不说了。

总结

其实这篇文章的主要目的介绍如何分析具体的需求,并将其转换为代码。根本在于 从一个相对于具象的需求中,我们一步一步抽离问题,组成更细小的问题的组合,然后逐步的去实验小问题的可行性,最终完成复杂问题

所有的需求均可以如此实现,只是有的我们可以实现,有的问题分析到最后发现,细节无法实现或实现起来成本过高(比如平安银行的自动识别手机壳颜色)。

当我写完这篇文章的demo的时候发现,我们的用户信息实在是太容易泄漏了,比如我的掘金账户和密码,如果在别人app上的内嵌网页登录的话,不是直接就泄漏了吗(手动滑稽)。

ps:这篇文章可以有个别名: 震惊!!!你还敢在你的手机上登录账户么

本文所述demo链接:

github.com/chouheiwa/I…

参考:

菜鸟教程

结尾

本文纯属娱乐写出,三方网站也只是以有脚本为例注入测试。并无任何破解成分。

另外大家如果有什么更好的建议或意见,也可以评论指出。

推广一波个人的公众号与博客

分析一次有意思的需求——HTML代码注入

以上所述就是小编给大家介绍的《分析一次有意思的需求——HTML代码注入》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Linux Command Line

The Linux Command Line

William E. Shotts Jr. / No Starch Press, Incorporated / 2012-1-17 / USD 39.95

You've experienced the shiny, point-and-click surface of your Linux computer-now dive below and explore its depths with the power of the command line. The Linux Command Line takes you from your very ......一起来看看 《The Linux Command Line》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

MD5 加密
MD5 加密

MD5 加密工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具