企业信息平台逆向登录与风控对抗实战:从JS破解到Python实现

1. 项目概述与核心价值

最近在做一个数据采集项目,需要从某眼查这类企业信息平台获取一些公开数据。但凡做过类似爬虫的朋友都知道,这类平台的风控机制是出了名的复杂,尤其是登录环节,简直就是一道“叹息之墙”。直接上Selenium模拟浏览器?效率低不说,还容易被识别为自动化工具,分分钟给你弹滑块验证码或者直接封IP。所以,逆向分析其登录流程和风控逻辑,就成了获取稳定数据源的必经之路。

这个项目标题“某眼查模拟登录+解除风控全流程”,说白了,就是一场攻防演练。我们的目标不是攻击,而是理解其防御机制,从而以合规、高效的方式模拟一个“真人”的登录行为,并绕过或适应其后续的数据访问风控。这涉及到前端JavaScript逆向、网络请求分析、加密参数破解、以及风控策略的识别与应对。整个过程就像在解一个层层嵌套的谜题,每一步都需要耐心和技巧。对于从事数据工程、安全研究或逆向工程的朋友来说,掌握这套流程,不仅能搞定某眼查,其方法论也适用于分析其他具有类似风控体系的Web应用。

2. 逆向分析前的环境与工具准备

工欲善其事,必先利其器。逆向分析Web应用,尤其是现代前端框架构建的复杂应用,没有趁手的工具寸步难行。这里的工具链可以分为三大类:浏览器开发者工具、抓包与调试工具、以及逆向辅助工具。

2.1 核心工具链详解

首先,浏览器开发者工具是基石。Chrome DevTools 是最佳选择,其Network(网络)面板用于记录所有HTTP/HTTPS请求,重点关注XHR/Fetch请求,登录和关键数据接口通常在这里。Sources(源代码)面板用于调试JavaScript,可以设置断点、单步执行、查看调用栈。Console(控制台)用于执行JavaScript代码片段,测试加密函数。

其次,抓包与调试工具用于更深入的分析。Fiddler Classic 或 Charles 这类代理工具可以拦截和修改所有经过系统的网络流量,对于分析HTTPS请求的明文内容(在安装并信任其CA证书后)以及模拟弱网环境非常有用。对于更复杂的、协议可能被混淆的场景,可以考虑使用 mitmproxy,它支持Python脚本进行流量实时修改,灵活性极高。

最后,逆向辅助工具能极大提升效率。当遇到JavaScript代码被压缩、混淆得面目全非时,我们需要借助工具进行格式化、反混淆和静态分析。Chrome DevTools自带的Pretty Print(美化打印)功能是第一步。对于更高级的混淆,可以使用 AST(抽象语法树)解析库,如esprimaescope,自己编写脚本进行反混淆,或者使用现成的工具如de4js在线服务进行初步解混淆。此外,一个强大的文本编辑器(如VSCode)用于搜索和比对代码,以及一个能够执行Node.js环境用于本地测试加密函数,都是必不可少的。

2.2 环境配置与代理设置

为了能稳定地拦截和分析某眼查的请求,配置一个可靠的代理环境是关键。我推荐使用 Fiddler Everywhere 或 mitmproxy,因为它们对现代Web应用的支持更好。

以 Fiddler Everywhere 为例,安装后,首先需要在其设置中开启“解密HTTPS流量”,并按照指引在电脑和浏览器上安装其根证书。这一步至关重要,否则你看到的HTTPS请求内容都是加密的乱码。接着,将浏览器的代理设置为 Fiddler 监听的地址(通常是127.0.0.1:8866)。之后,所有浏览器流量都会经过 Fiddler。

注意:在开始分析敏感目标前,最好关闭其他不必要的网络应用,并创建一个干净的浏览器用户配置文件,避免个人浏览数据干扰分析。同时,准备好多个可用的IP地址资源(例如优质的住宅代理IP池),因为分析过程中可能会触发IP级别的风控。

3. 登录流程的逆向分析与关键参数破解

某眼查的登录页面看似简单,一个手机号输入框,一个密码/验证码区域,但背后的逻辑非常复杂。通常,它会包含动态加载的JavaScript,里面藏着加密算法、Token生成逻辑和提交参数的构造方法。

3.1 登录请求的捕获与初步分析

打开无痕模式的浏览器(配置好代理),访问某眼查登录页。打开开发者工具的网络面板,勾选“Preserve log”(保留日志)。在输入框里随意输入一个手机号(不需要真实注册),点击“获取验证码”或尝试登录。

此时,网络面板会刷出一系列请求。我们需要找到那个最终提交登录信息的POST请求。通过查看请求的URL(通常包含loginsubmit等关键字)、请求体(Payload)的类型(通常是application/jsonapplication/x-www-form-urlencoded)以及响应结果,可以定位到核心登录接口,比如可能类似于/api/auth/login/v2这样的端点。

点击这个请求,查看其详细信息:

  • Headers: 关注CookieUser-AgentReferer,以及一些自定义的Header,如X-TokenX-Sign等。这些往往是风控的一部分。
  • Payload: 这是重点。你会看到提交的数据,除了明文的手机号,其他字段如passwordcaptchatokensignature等很可能被加密或编码过。密码字段几乎不可能是明文传输。

3.2 JavaScript逆向定位加密函数

发现加密参数后,下一步就是找到生成这些参数的JavaScript代码。在Network面板中,该登录请求的“Initiator”(发起者)列会显示是哪个JS文件发起了这个请求。点击这个JS文件名,会跳转到Sources面板对应的代码位置。

由于代码通常被压缩成一行,首先点击左下角的{}(Pretty Print)按钮进行格式化。格式化后,代码有了结构,但仍然变量名混淆(如a, b, c, _0x1234)。我们的目标是搜索加密参数名。

在格式化后的JS文件中,使用Ctrl+F搜索关键词。例如,搜索passwordencryptencodesign,或者搜索你在Payload里看到的那个加密后字符串的前几个字符。这通常能定位到关键的加密函数附近。

找到疑似函数后,最有效的方法是下断点。在加密函数被调用的行号左侧点击,设置一个断点。然后回到网页,再次触发登录动作(比如再次点击登录按钮)。执行流会暂停在断点处。

此时,在Console面板中,你可以查看当前作用域的变量值。重点关注传入函数的参数是什么(通常是明文密码或一个对象),以及函数的返回值是什么。通过单步执行(F10逐过程,F11逐语句),你可以一步步跟踪加密逻辑。你需要理清:

  1. 加密的输入是什么?(明文密码+盐值?时间戳?)
  2. 使用了什么算法?(可能是自定义的Base64变种、AES、RSA、或简单的异或操作)
  3. 密钥从哪里来?(可能硬编码在JS里,也可能由另一个接口动态返回)

3.3 关键参数sign的生成逻辑还原

在许多风控体系中,sign(签名)参数是重中之重,用于防止请求被篡改。它的生成逻辑通常是将所有请求参数(有时还包括固定盐值、时间戳)按特定规则排序后拼接成一个字符串,然后进行MD5、SHA256等哈希计算,或者再进行一次自定义的加密。

通过断点调试,定位到生成sign的函数。你需要记录下:

  • 参数集合: 哪些字段参与了签名?除了phonepassword,可能还有timestampnonce(随机数)、appVersion等。
  • 排序规则: 通常是按照参数名的ASCII码升序排列。
  • 拼接规则: 参数名和值如何拼接?常见格式如key1=value1&key2=value2
  • 哈希/加密算法: 拼接后的字符串经过什么处理?可能是MD5(sign_str + secret),其中secret是一个隐藏的密钥。

将这套逻辑用Python(或其他语言)完整复现出来,是模拟登录的核心。你可以将调试时捕获的真实参数代入你自己的复现函数,计算出的sign值应该与原始请求中的完全一致。

4. 风控机制的识别与动态对抗策略

成功模拟登录请求只是第一步。某眼查的风控是立体的、动态的,会在你后续的数据访问请求中持续生效。识别并适应这些风控,才是项目稳定的保证。

4.1 常见风控手段剖析

  1. 行为指纹(Browser Fingerprinting): 网站会通过JavaScript收集你浏览器的大量特征,如User-Agent、屏幕分辨率、时区、语言、Canvas指纹、WebGL指纹、字体列表等,生成一个唯一标识。即使你更换IP,如果指纹不变,仍可能被关联识别。在逆向时,你会发现一些接口会返回一段JS代码或一个Token,这段代码执行后会上报指纹信息。
  2. 请求参数验签与时效性: 如前所述,重要的数据查询接口往往也有签名机制,而且签名可能有时效性(如timestamp参与签名,服务器会校验时间差)。此外,一些Token(如登录后的sessionIdaccess_token)也有生命周期,需要定期刷新。
  3. 请求频率与模式识别: 这是最直观的风控。短时间内高频访问、访问模式过于规律(如固定间隔秒杀式请求)、访问非人类浏览的热点路径,都会触发限制。限制方式从弹出验证码到直接封禁IP或账号不等。
  4. 验证码挑战: 当行为被判定为可疑时,会触发验证码,包括图形验证码、滑块验证码、点选验证码、甚至智能推理验证码。其中,滑块验证码(如某验)的逆向难度较高,涉及轨迹模拟、缺口识别等。

4.2 动态Token与会话维持

登录成功后,服务器会返回一个Token(可能在响应体里,也可能在Set-Cookie头里)。这个Token是后续所有请求的“通行证”。你需要分析:

  • Token的使用位置: 是放在请求头Authorization: Bearer xxx里,还是放在Cookie里,抑或是作为一个普通的请求参数?
  • Token的刷新机制: Token有过期时间吗?是否有专门的刷新接口?刷新逻辑是怎样的?通常,在Token临近过期时调用刷新接口获取新Token,并更新本地存储。
  • 会话一致性: 保持整个会话过程中,Cookie、LocalStorage、以及一些内存中的全局变量(可能在JS中初始化)的一致性。有时,一个请求的响应会包含下一个请求必需的上下文信息。

在Python模拟中,你需要使用requests.Session()对象来自动管理Cookie,并手动维护需要放在请求头中的Token。

4.3 模拟真人行为模式

这是对抗频率和模式风控的核心。你的爬虫行为不能像机器一样精准和快速。

  • 随机化延迟: 在请求间加入随机等待时间,例如time.sleep(random.uniform(1, 3)),模拟人类阅读和点击的间隔。
  • 模拟浏览路径: 不要直接访问目标数据接口。可以先访问首页,再模拟点击进入某个分类,然后再进行搜索或详情查看。这会在你的会话中留下“自然”的浏览记录。
  • 维护合理的请求头User-Agent最好使用常见的浏览器字符串池随机选择。Referer头要设置得合理,让它看起来是从上一个页面跳转过来的。
  • 代理IP池的质量与使用策略: 使用高质量的住宅代理IP,并设计合理的IP切换策略。例如,一个IP用于登录和维持会话,另一个IP池用于数据抓取,且每个IP的请求频率和总量都要控制。

5. 完整模拟登录与数据采集代码实现

基于以上的分析,我们可以构建一个相对健壮的模拟登录和数据采集类。这里以Python的requests库为例,展示核心框架。

5.1 核心类结构与初始化

首先,我们设计一个类来封装所有功能,包括登录、会话维持、请求签名和风控处理。

import requests import time import random import hashlib import json from urllib.parse import urlencode class TianYanChaSpider: def __init__(self, username, password, proxy_pool=None): self.username = username self.password = password self.session = requests.Session() self.base_headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Accept': 'application/json, text/plain, */*', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Origin': 'https://www.tianyancha.com', 'Referer': 'https://www.tianyancha.com/', } self.proxy_pool = proxy_pool # 代理IP池,格式 [{'http': 'http://ip:port'}, ...] self.current_proxy = None self.access_token = None self.token_expire_time = 0 # 从逆向分析中得到的固定参数或密钥 self.app_secret = '你逆向分析得到的SECRET_KEY' self.app_version = '9.0.0' # 示例版本 def _get_proxy(self): """从代理池中随机获取一个代理""" if self.proxy_pool: return random.choice(self.proxy_pool) return None def _make_request(self, method, url, **kwargs): """封装请求,加入代理、重试和风控延迟逻辑""" proxy = self._get_proxy() kwargs['proxies'] = proxy # 合并headers headers = self.base_headers.copy() if 'headers' in kwargs: headers.update(kwargs['headers']) kwargs['headers'] = headers # 加入随机延迟,模拟人类操作 time.sleep(random.uniform(0.5, 2.0)) try: response = self.session.request(method, url, **kwargs) # 这里可以加入状态码判断,例如 429(请求过多)、403(禁止访问)时触发风控处理 if response.status_code == 429: print(f"触发频率限制,等待10秒后重试...") time.sleep(10) return self._make_request(method, url, **kwargs) # 简单重试一次 return response except requests.exceptions.ProxyError: print("代理失败,更换代理重试...") if self.proxy_pool: self.proxy_pool.remove(proxy) # 简单移除失效代理 return self._make_request(method, url, **kwargs)

5.2 登录与签名函数实现

接下来是实现最关键的登录和签名函数。这里的_generate_sign_encrypt_password函数需要你用逆向分析得到的逻辑来填充。

def _generate_sign(self, params): """ 生成请求签名。 params: 参与签名的参数字典 返回签名字符串。 """ # 1. 参数排序 (按key字母序) sorted_params = sorted(params.items(), key=lambda x: x[0]) # 2. 拼接成 key1=value1&key2=value2 格式 sign_str = '&'.join([f'{k}={v}' for k, v in sorted_params]) # 3. 拼接密钥(假设是app_secret) sign_str += self.app_secret # 4. 计算MD5(具体算法需根据逆向结果调整,可能是SHA256等) sign_md5 = hashlib.md5(sign_str.encode('utf-8')).hexdigest() # 5. 可能还有二次处理,如转大写 return sign_md5.upper() def _encrypt_password(self, plain_password): """ 加密密码。 这里是一个示例,真实逻辑可能涉及RSA加密公钥或AES加密。 你需要通过逆向分析,用Python复现完全相同的加密过程。 """ # 示例:可能是Base64编码 + 盐值混淆 # 真实情况请替换为你的逆向结果 import base64 # 假设是简单的 base64 编码(实际远比这复杂) encrypted = base64.b64encode(plain_password.encode()).decode() return encrypted def login(self): """执行登录流程""" login_url = "https://www.tianyancha.com/api/auth/login/v2" # 示例URL # 1. 获取必要的初始Token或盐值(有时需要先请求一个接口获取加密公钥或随机数) init_params = { 'phone': self.username, 'timestamp': int(time.time() * 1000), 'appVersion': self.app_version, } init_params['sign'] = self._generate_sign(init_params) # 可能先发一个GET请求获取public_key等 # init_resp = self._make_request('GET', 'https://.../init', params=init_params) # public_key = init_resp.json()['data']['publicKey'] # 2. 构造登录参数 timestamp = int(time.time() * 1000) login_params = { 'phone': self.username, 'password': self._encrypt_password(self.password), # 使用加密后的密码 'timestamp': timestamp, 'appVersion': self.app_version, 'loginType': 'password', # 或 'sms' 验证码登录 # 'captcha': '...', # 如果需要图形验证码 } # 3. 生成登录请求的签名 login_params['sign'] = self._generate_sign(login_params) # 4. 发送登录请求 headers = { 'Content-Type': 'application/json;charset=UTF-8', } resp = self._make_request('POST', login_url, json=login_params, headers=headers) if resp.status_code == 200: result = resp.json() if result.get('code') == 0 or result.get('success'): # 根据实际响应结构调整 data = result.get('data', {}) self.access_token = data.get('accessToken') # 假设返回的token有效期为2小时 self.token_expire_time = time.time() + 2 * 3600 print(f"登录成功,Token: {self.access_token[:20]}...") # 更新session的headers,后续请求携带Token self.session.headers.update({'Authorization': f'Bearer {self.access_token}'}) return True else: print(f"登录失败: {result.get('message')}") # 可能需要处理验证码 if '验证码' in result.get('message', ''): self._handle_captcha() return False else: print(f"登录请求异常,状态码: {resp.status_code}") return False def _handle_captcha(self): """处理验证码(此处为示意,滑块验证码需要复杂逆向)""" print("触发验证码,需要人工处理或接入打码平台。") # 对于图形验证码,可以下载图片,调用打码平台API识别。 # 对于滑块验证码,需要识别缺口位置,生成模拟滑动轨迹。 # 这是一个独立且复杂的话题,此处不展开。

5.3 数据采集与风控应对示例

登录成功后,就可以进行数据采集了。以搜索公司为例:

def search_company(self, keyword, page=1): """搜索公司""" if time.time() > self.token_expire_time: print("Token已过期,尝试刷新...") if not self._refresh_token(): print("刷新Token失败,重新登录...") self.login() search_url = "https://www.tianyancha.com/api/search/v2" params = { 'word': keyword, 'pageNum': page, 'pageSize': 20, 'timestamp': int(time.time() * 1000), } # 搜索接口可能也需要签名 params['sign'] = self._generate_sign(params) resp = self._make_request('GET', search_url, params=params) if resp.status_code == 200: return resp.json() else: print(f"搜索请求失败: {resp.status_code}") return None def _refresh_token(self): """刷新Access Token""" refresh_url = "https://www.tianyancha.com/api/auth/refresh" # 通常用refresh_token来刷新,这里简化处理 # 实际情况可能需要调用特定接口 print("Token刷新逻辑需根据实际API实现") return False

6. 常见问题排查与实战经验分享

在实际操作中,你会遇到各种各样的问题。下面是我踩过的一些坑和对应的解决方案。

6.1 问题排查清单

问题现象可能原因排查思路与解决方案
登录请求返回“参数错误”或“签名无效”1. 签名算法还原错误。
2. 参与签名的参数不全或顺序不对。
3. 加密函数(如密码加密)有误。
4. 缺少必要的请求头(如自定义的X-Client)。
1.核对签名:用抓包到的原始参数,代入自己写的签名函数,计算结果与抓包到的sign值逐字符比对。
2.参数对比:仔细对比自己构造的Payload和抓包到的Payload,查看每个键值对是否完全一致,包括看似无关的字段。
3.断点调试:在浏览器中仔细跟踪签名和加密函数的每一步,记录中间变量的值,与Python代码的中间结果对比。
登录成功但后续接口返回“未登录”或“Token过期”1. Token未正确设置到后续请求中。
2. Token过期时间判断有误。
3. 会话(Session)未保持,Cookie丢失。
4. 某些接口需要额外的认证参数。
1.检查请求头:确认Authorization头或相应的Cookie已正确附加。使用requests.Session()可自动管理Cookie。
2.检查Token生命周期:登录响应中可能包含expires_in字段,据此计算过期时间。
3.全局会话:确保所有请求都使用同一个Session对象。
4.分析后续请求:抓取一个成功的手动操作请求,看除了Token外是否还有X-Request-ID等字段。
请求频率稍高即返回429或弹出验证码触发了基于IP或账号的行为频率风控。1.降低请求频率:增加随机延迟,time.sleep(random.uniform(2, 5))
2.使用高质量代理:切换不同的住宅代理IP,模拟不同地理位置的用户。
3.模拟人类行为:在关键请求(如翻页、查看详情)之间插入访问首页、随机等待等操作。
4.识别验证码类型:准备好应对方案,如接入打码平台。
代码在本地运行正常,部署到服务器失败1. 服务器环境缺少依赖(如Node.js环境用于执行某些JS加密)。
2. 服务器IP被目标网站封禁。
3. 时区、TLS版本等环境差异导致指纹不同。
1.环境一致性:使用Docker容器固化运行环境。
2.检查服务器IP:用服务器curl一下目标网站,看是否可访问。
3.完善请求头:确保服务器上运行的脚本其User-AgentAccept-Language等头与浏览器一致。

6.2 核心实战经验与技巧

  1. 逆向的核心是“对比”和“还原”:不要试图完全理解所有混淆后的代码逻辑。你的目标是把输入变成输出的那个“黑盒”函数用Python还原出来。不断用真实数据(从浏览器调试中获取)输入你自己的函数,对比输出是否与浏览器一致,是最高效的方法。
  2. 善用“Hook”技术:对于难以定位的加密函数,可以在浏览器控制台使用“Hook”技巧。例如,重写window.crypto.subtle.encryptJSON.stringify等方法,在它们被调用时打印出参数和结果,能快速定位加密发生的位置。
  3. 风控是持续对抗:没有一劳永逸的方案。网站的风控策略会升级。你的代码需要有良好的日志系统,记录每次请求的响应状态、触发风控的情况。当发现失败率升高时,要及时分析日志,调整策略(如更换签名参数、更新加密算法、增加新的请求头)。
  4. 尊重robots.txt与法律边界:本文技术讨论仅用于学习与交流。在实际应用中,务必遵守目标网站的robots.txt协议,尊重数据版权和个人隐私,控制访问频率,避免对目标网站服务器造成负担。用于商业用途或大规模抓取前,请务必寻求法律意见。
  5. 滑块验证码的应对:这是一个深水区。简单方案是接入商业打码平台(如超级鹰、图鉴),将截图发送给他们,返回滑动轨迹。如果想自己研究,需要学习图像识别(找缺口位置)和轨迹生成算法(模拟人类加速度曲线)。这本身就是一个庞大的逆向工程课题。

整个逆向分析的过程,是对耐心、细心和逻辑思维能力的极大考验。每一个加密参数背后,都可能藏着开发者精心设计的防御逻辑。破解它,不仅需要技术,更需要像侦探一样,从纷繁的网络请求和混乱的代码中,找到那条通往真相的路径。当你最终看到自己的程序稳定地获取到所需数据时,那种成就感,或许就是技术人最大的乐趣之一。