首页 / 资讯中心 / 文章详情

JWT模拟登录爬虫实战:从Token原理到代码落地与问题排查

JWT模拟登录爬虫实战:从Token原理到代码落地与问题排查 ★ FEATURED ARTICLE
接手过不少需要登录才能拿数据的爬虫项目其中很大一部分在登录环节用的是JWTJSON Web Token。很多人一开始觉得模拟登录很神秘其实搞明白JWT的来龙去脉整个爬虫链路就清晰了。这篇内容就围绕“基于JWT的模拟登录爬取实战”展开从原理拆解到代码落地再到问题排查把我在实际项目中踩过的坑和总结的经验一次性说清楚。1. 项目思路拆解为什么模拟登录爬虫绕不开JWT1.1 核心需求解析登录墙后面的数据才是目标刚开始做爬虫的人往往喜欢先从公开接口练手不需要登录、直接拼接URL就能拿到数据看起来简单直接。但真正有业务价值的项目绝大多数数据都藏在登录墙后面。比如某企业内部的管理系统、某个行业数据平台、某个社区的付费内容这些数据接口无一例外都要求在请求头里带上身份凭证。凡是需要带凭证的接口本质上就是一个“模拟登录爬取”的需求。我之前接过一个模拟项目X目标是从某数据平台抓取一批业务报表数据。这个平台前端是典型的单页应用后端API全部通过JWT做身份认证。不带Token请求数据接口返回的是401未授权带上Token才能正常拿到JSON数据。整个项目的核心就是两件事第一分析登录接口的请求格式模拟登录拿到Token第二在后续所有数据请求中正确携带Token并且处理好过期刷新逻辑。这个场景非常典型。JWT之所以在现在的Web应用中大规模普及是因为它解决了传统Session登录的不少痛点服务端不需要存Session、天然适合分布式部署、移动端和Web端共用一套认证逻辑。但对于爬虫工程师来说这也意味着要额外处理Token的解析、携带、刷新这些环节。看懂JWT等于拿到了这类项目的通行证。1.2 Session登录与JWT登录的差异传统基于Session的登录流程是用户提交用户名密码服务端验证通过后创建一个Session把Session ID写入Cookie返回给浏览器。之后每次请求浏览器自动带上Cookie服务端根据Session ID去内存或Redis里查找对应的用户信息。这种方式在单体应用时代问题不大但到了分布式架构下Session同步就成了麻烦事。JWT的思路完全不同。用户登录成功后服务端不保存任何状态只是把用户信息加密签名后生成一串Token返回给客户端。客户端在后续请求中把Token放在请求头里通常是Authorization字段服务端拿到Token后验证签名确认无误就从Token里直接解析出用户身份。整个过程服务端无状态水平扩展非常方便。对爬虫的影响也是很直接的。Session登录爬虫需要维护Cookie很多情况下还需要处理Cookie的过期和更新JWT登录则只需要管理好Token字符串搞清楚它的格式和有效期限。从代码实现角度看JWT反而更简单因为只需要关注请求头的一个字段。1.3 技术方案选型与整体流程设计做这个项目之前我最先考虑的是技术路线。方案其实有好几种第一种纯requests模拟登录。分析登录接口用requests.post提交表单或JSON数据拿到返回的Token再带着Token去请求数据接口。这种方案轻量、可控性最高也是我做这个项目最终采用的方案。第二种selenium自动化登录。用浏览器自动填充用户名密码等登录成功后直接读取浏览器的LocalStorage或Cookie把Token提取出来再交给requests使用。这种方案适合登录环节存在大量JS加密、验证码极其复杂的场景缺点是性能差、容易被检测。第三种Playwright自动化登录。比selenium更现代可以更方便地拦截网络请求从请求头里直接提取Token。如果前端采集Token的逻辑比较复杂这个方案效率很高。我最终选择纯requests方案原因有两个一是目标平台登录逻辑不复杂就是POST用户名密码换取Token二是纯requests方案跑起来最快几秒钟就能完成整个流程对于数据量大的爬取任务来说效率优势明显。整体流程设计大概是这样的先用浏览器开发者工具分析登录接口和数据接口的请求格式写一个登录函数获取Token再写一个带Token的数据请求函数最后加上Token过期自动重登的逻辑封装成一个完整的爬虫脚本。2. JWT核心细节解析拿到Token只是第一步2.1 JWT结构拆解Header、Payload、SignatureJWT看起来就是一长串由三个部分组成的字符串每部分之间用点号分隔。第一部分是Header第二部分是Payload第三部分是Signature。整体格式长这样eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImFkbWluIiwiZXhwIjoxNzAwMDAwMDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cHeader部分通常包含两个字段alg签名算法和typToken类型。大部分情况下alg是HS256即HMAC-SHA256对称加密算法如果用RS256则是基于公钥私钥的非对称算法这种场景在爬虫中遇到得少一些。Payload部分是Token的核心载荷里面包含了签发者、过期时间、用户ID、权限等自定义字段。对爬虫来说最值得关注的就是两个字段一个是exp表示Token的过期时间Unix时间戳格式另一个是sub或者自定义的userId之类的字段代表用户身份。Signature部分是对Header和Payload的签名结果。服务端用密钥对前两部分做HMAC-SHA256计算得到签名之后每次收到请求都会重新计算一遍如果计算结果和Token里的签名一致就认为Token是合法的。这里有个非常重要的爬虫知识点Payload部分是Base64编码的直接解码就能看到明文内容所以Token里不要放敏感数据。但对爬虫来说能解码Payload就意味着我们可以直接读出Token的过期时间提前做好刷新策略。2.2 关键点Token在哪里、存储方式是什么做JWT模拟登录爬虫第一步不是写代码而是在浏览器开发者工具里搞清楚Token的前世今生Token在登录接口的返回数据里、在后续请求的哪个请求头里出现在哪些地方。通常登录接口的响应体是一个JSON里面包含token或者access_token字段。后续的数据请求在Headers里会带一个Authorization: Bearer token字段或者在某些项目里会直接用自定义的请求头比如X-Token: token。这些细节必须通过抓包确认不能想当然。有些平台会把Token放在Cookie里名为jwt_token之类的。这种情况下请求时不需要手动设置Authorization头直接保持Cookie即可。但这里要特别留意Cookie的Domain和Path作用域跨域请求Cookie能不能带上跟这两个属性有很大关系。再一个场景是Token在LocalStorage里。纯requests脚本是拿不到浏览器LocalStorage里的数据的这时要么用selenium读取要么走接口登录逻辑重新获取。我遇到过一个平台登录成功后前端把Token存进了LocalStorage然后前端JS读取LocalStorage里的Token再塞进请求头。遇到这种设计直接用requests模拟登录就行不需要管LocalStorage只要拿到接口返回的原始Token手动塞进请求头即可。2.3 常见坑有效期、过期与刷新机制JWT的过期时间在Payload的exp字段里是一个标准的Unix时间戳。针对某个爬虫项目在写代码之前我建议先把这个Token解码出来看一眼确认有效期是多久。有些平台Token有效期只有30分钟有些平台有7天差异非常大。对于有效期只有半小时甚至更短的场景单次登录拿Token已经不够用了。这时候需要实现自动重登逻辑在每次请求数据前检查当前时间是否接近Token过期时间如果快过期了就重新走一遍登录流程换新Token。还有一种情况是平台同时返回access_token和refresh_token两个Token前者有效期短后者有效期长过期后用refresh_token去刷新接口换新的access_token。这种机制在爬虫里处理起来也不复杂无非是加一个判断逻辑。还有一个容易踩的坑是时区问题。Token里的exp是Unix时间戳本来就带时区无关性但有些平台返回的有效期时长是相对值比如“7200”意思是2小时后过期。这时候需要在代码里手动计算绝对过期时间否则时间一长就会莫名其妙出现401。3. 登录接口分析与模拟登录实现3.1 用开发者工具定位登录接口与参数结构正式写代码前先用浏览器开发者工具摸清登录接口的全部细节。以我的实际操作经验流程基本是固定的打开目标网站进入登录页面按F12打开开发者工具切到Network标签页。勾选Preserve log保留日志防止页面跳转导致请求记录消失。输入用户名密码登录在Network面板里找到登录请求通常是名为login、auth或signin的POST请求。点开这个请求重点看三个部分第一部分是Request Headers看Content-Type是什么。有的是application/json登录参数是JSON格式有的是application/x-www-form-urlencoded参数是表单格式。这个直接决定了requests.post里的data参数怎么传。第二部分是Request Payload看提交了什么字段。一般是username和password但也有的平台会额外要求captcha验证码、client_id等字段。我遇到过一个平台登录参数里有grant_type字段固定值为password这就是OAuth2的密码模式。第三部分是Response登录成功后返回的JSON是什么结构。重点看Token字段名有时候是token有时候是accessToken有时候是data.token。这些细节直接决定代码里的解析方式。数据接口的分析也类似。登录后在页面上打开一个需要权限的功能Network面板里找返回JSON数据的XHR请求看它的请求头里Authorization字段的格式是Bearer eyJ...还是只有一串Token。还要留意请求参数里有没有分页字段、时间范围字段这些都会影响爬虫的遍历逻辑。3.2 模拟登录获取Token的代码实现登录逻辑本身并不复杂用requests就可以完成。核心代码如下import requests import json import time class JwtLoginClient: def __init__(self, username, password): self.session requests.Session() self.username username self.password password self.token None self.token_expire_time 0 self.login_url https://api.example.com/api/login self.data_url https://api.example.com/api/report/list self.headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Content-Type: application/json, Origin: https://app.example.com, Referer: https://app.example.com/login } def login(self): login_data { username: self.username, password: self.password } response self.session.post(self.login_url, jsonlogin_data, headersself.headers) if response.status_code ! 200: raise Exception(f登录失败状态码: {response.status_code}, 响应: {response.text[:500]}) result response.json() if not result.get(success) or token not in result.get(data, {}): raise Exception(f登录响应异常: {result}) self.token result[data][token] # 解析JWT的payload部分获取过期时间 payload_part self.token.split(.)[1] # Base64补全并解码 padding * (4 - len(payload_part) % 4) payload_json base64.urlsafe_b64decode(payload_part padding).decode(utf-8) payload json.loads(payload_json) self.token_expire_time payload.get(exp, 0) print(f登录成功Token有效期至: {time.strftime(%Y-%m-%d %H:%M:%S, time.localtime(self.token_expire_time))}) jwt_client JwtLoginClient(test_user, test_password) jwt_client.login()这段代码里有一个细节值得说明JWT的Payload部分使用的Base64是URL安全的变体字符集里和/被替换成了-和_而且末尾没有等号填充。直接用Python标准库的base64.b64decode会报错必须先做字符替换或者补全等号。我强烈建议在解析Token时用base64.urlsafe_b64decode这是最稳妥的做法。3.3 携带Token请求数据接口拿到Token后请求数据接口就简单了。在每次请求时设置Authorization请求头即可def get_report_list(self, page1, page_size50): if not self.token: raise Exception(请先登录) # 检查Token是否即将过期提前60秒重新登录 if self.token_expire_time and time.time() self.token_expire_time - 60: print(Token即将过期重新登录) self.login() params { page: page, pageSize: page_size } headers dict(self.headers) headers[Authorization] fBearer {self.token} response self.session.get(self.data_url, paramsparams, headersheaders) if response.status_code 401: # Token失效强制重新登录并重试一次 print(收到401重新登录后重试) self.login() headers[Authorization] fBearer {self.token} response self.session.get(self.data_url, paramsparams, headersheaders) if response.status_code ! 200: raise Exception(f数据请求失败状态码: {response.status_code}) return response.json()这里实现了两层防护逻辑第一层是主动检查Token有效期在过期前60秒就重新登录第二层是收到401状态码后强制重登并重试一次。两层逻辑加在一起正常情况下不会因为Token过期中断爬取任务。关于Requests Session的使用我再多说一句。用requests.Session()而不是直接的requests.get()是因为Session会自动保持TCP连接复用降低多次请求的握手开销。在爬虫场景下两者的性能差距非常明显。4. 数据爬取与异常处理4.1 会话保持与请求头伪装虽然JWT认证的核心是请求头的Authorization字段但并不意味着可以忽略其他请求头。不少后端对请求头做校验时会检查User-Agent、Referer、Origin等字段的一致性。我处理这个问题的思路是伪装成完全正常的浏览器请求。User-Agent要设置成当前主流浏览器的版本Referer要设置为登录后页面的URLOrigin要设置为前端应用域名。尤其是有CORS跨域校验的后端请求头里缺少Origin会直接导致400错误。Cookie也需要谨慎处理。在requests.Session中登录时服务端可能返回一些辅助CookieSession会自动保存。后续请求会自动带上这些Cookie和Authorization头一起完成认证。这里有个容易忽略的地方如果手动创建了新的Session对象这些Cookie就丢了即使Token是对的也可能因为缺少某些Cookie而认证失败。我个人的习惯是登录和爬取全程使用同一个requests.Session实例不轻易创建新Session。如果确实需要新Session就把登录流程重新走一遍不要复用旧的Token凭证。4.2 接口分页遍历与数据清洗大部分数据接口都是分页返回的。处理分页的逻辑通常有两种一种是接口返回总条目数通过总条目数计算总页数另一种是接口返回“是否有下一页”的标记。两种方式代码逻辑大同小异def crawl_all_data(self, max_pages100): all_data [] page 1 while page max_pages: result self.get_report_list(pagepage) if not result.get(data) or not result.get(data).get(list): break page_data result[data][list] all_data.extend(page_data) total result[data].get(total, 0) page_size result[data].get(pageSize, 50) total_pages (total page_size - 1) // page_size print(f第{page}页完成本页{len(page_data)}条总条数{total}) if page total_pages: break page 1 return all_data这个分页逻辑里有个细节循环条件里设置了max_pages上限这是为了防止接口返回异常数据导致死循环。我在实际项目中遇到过接口返回的total字段突然变成极大值的情况如果没有上限控制程序会一直请求到天荒地老。数据拿到手之后还有一层清洗工作。有些平台的JSON字段名含义不明确比如a1、b2这种缩写有些字段值有误比如把字符串类型的数字返回成了null。清洗阶段需要根据实际的业务字段做映射和校验。这一步最花时间但也是爬虫项目真正产生价值的地方——零散的数据变成规范的结构化数据才算爬取完成。4.3 限速与反爬应对JWT认证本身不等于反爬但大多数有价值的数据平台都做了反爬限制。绕开反爬不是常规手段但有一些基本的礼貌做法可以减少被限制的概率。第一是请求频率控制。每页请求之间增加一个随机延时通常在0.5秒到2秒之间。用time.sleep(random.uniform(0.5, 2))比固定的延时更难被识别。第二是请求时间分布。大规模爬取任务不要集中在一段时间内完成最好拉长到几小时内完成。比如一万条数据分页只有200页请求完可能只需要几分钟但这样集中请求很容易触发阈值。可以考虑把任务分批执行每批之间间隔数分钟。第三是异常状态码监控。如果发现响应状态码突然变成403或429说明已经被限流或者封禁此时应该立即停止请求等待一段时间再试。盲目继续请求只会让封禁时间更长。5. 常见问题与排查技巧实录5.1 登录成功但Token无效这是我遇到最多的问题。明明登录接口返回了200Token也能拿到手但带Token请求数据接口时依然提示认证失败。排查思路分三步走第一步检查Token在请求头里的格式。有些平台要求Authorization: Bearer token有些平台要求Authorization: tokentoken还有的平台用自定义头。格式错误是最常见的原因。第二步对比浏览器里实际发出的请求和脚本发出的请求。在开发者工具里找到浏览器发出的数据请求看它的请求头里Authorization字段的完整值。有些平台在Token前面加了额外的前缀比如JWT token直接照抄最保险。第三步检查是否需要额外的自定义头。某些平台会在前端JS里动态生成一个签名值放在请求头中这个值可能跟Token绑定。如果遇到这种设计单纯携带Token是不够的。解决办法是用selenium或Playwright截获前端实际发出的请求把额外的请求头都记录下来再在脚本里模拟。5.2 Token过期导致401Token过期是最正常的现象处理方案在代码里已经实现了两层逻辑主动检查过期时间和401后自动重登。但还有一种情况要注意检查了Token的exp字段发现时间还很充裕但请求依然返回401。这种情况往往是服务端将Token加入了黑名单场景包括用户修改了密码、管理员强制下线、服务端密钥更换。遇到这种“被动失效”的情况主动检查过期时间是没有意义的必须依赖401后重登的逻辑。我建议重登逻辑写成循环重试的形式最多重试3次。如果3次重登后依然401说明账号本身可能出了问题这时候停止任务并记录日志是最好的选择而不是无限循环。5.3 登录接口出现验证码干扰JWT登录爬虫最头疼的问题之一就是验证码。用户名密码都好模拟验证码是登录链路里最接近“人机验证”的环节。处理验证码通常有几种思路第一种OCR识别。简单数字、字母类的验证码用开源OCR库或者第三方打码平台都可以解决。识别率在常规场景下能做到80%以上配合重试机制基本够用。第二种人工介入。验证码出现时把验证码图片下载下来发给人工识别识别完成后再回填到登录流程里。这种方式适合低频登录的场景比如每天只需要登录一次人工介入一次完全可接受。第三种绕过登录。有些平台有“记住登录状态”的功能Token的有效期可能长达7天甚至30天。如果能把获取到的Token文件化存储之后每次爬取前读取本地缓存的Token只要Token没过期就完全不用重新登录也就绕过了验证码环节。5.4 参数被加密导致登录失败有些平台的登录参数做了前端加密处理用户名密码不是明文传递。常见的有AES加密、RSA加密、MD5后加盐等。遇到这种情况需要从前端JS代码里提取加密逻辑。我的经验是先用开发者工具在Sources面板里搜索加密函数名比如encrypt、RSA等关键词。找到加密代码后必须弄清楚加密算法的模式、填充方式、公钥来源。Java环境下用JS2Py或者调用Node.js子进程来执行前端加密代码是相对省力的做法。不过这种方案也有代价执行JS代码的性能会比纯Python计算慢很多。如果有办法直接从服务端API文档里找到配套的签名算法说明通常会有官方文档直接说明参数加密方式。有些平台为了降低开发者的接入成本会提供公开的SDK文档里面有完整的加密算法描述。5.5 实操心得三个让我少走弯路的习惯第一开发阶段用日志记录每次请求的URL、请求头和响应状态码。不要嫌日志冗余调试的时候日志就是命根子。我之前排查一个401问题就是因为日志里记录了Authorization头的完整值对比浏览器里的请求才发现Token前面少了个空格。第二数据库表设计时给每条数据加一个唯一索引。爬虫不是跑一遍就结束大概率会跑第二次。没有唯一索引重复数据会把自己的分析结果污染掉。加一个UNIQUE KEY用业务ID做去重后续增量爬取会省很多事。第三写一个健壮的配置文件。登录的用户名、密码、接口地址、分页大小、请求延时等参数全部抽到配置文件里不要硬编码在代码里。一方面是方便调整参数另一方面是为后续其他平台复用代码做好准备。6. 扩展思考从JWT模拟登录到通用登录方案做完了基于JWT的模拟登录爬虫能把经验抽出来沉淀成可复用的东西价值会更大。我后来把这次项目的逻辑整理成了一套通用登录适配框架大致包含三层抽象第一层是认证方式抽象层。无论底层是JWT、Session Cookie还是OAuth2上层调用方只暴露一个get_token()方法屏蔽认证细节。第二层是Token管理层。负责Token的获取、缓存、刷新和失效重试。一旦发现401自动触发重新认证流程调用方完全感知不到Token失效的过程。第三层是请求执行层。封装统一的请求方法自动注入认证信息统一处理状态码统一处理后端返回的错误信息。这套抽象思路虽然简单但我后来接的新项目基本都能在一两天内完成登录模块的适配。如果一个系统换了一套认证方式只需要重新实现get_token()这一个方法其他逻辑全部可以复用。另外还有一点值得琢磨如果目标平台的JWT密钥泄露了怎么办。虽然政府和企业级系统的密钥通常保护得比较严密但从爬虫安全性的角度来说能不用自己构造JWT就尽量不要自己构造。某次我在测试环境里发现了一个平台使用弱密钥签名JWT理论上可以自己伪造任意用户的Token。但这类操作风险极高我一般只做提醒不建议任何人实际使用。模拟登录爬虫这件事本质上考验的是对HTTP协议和常见认证机制的理解。搞清楚了JWT的结构和交互流程你会发现登录接口千篇一律无非是参数不同、路径不同一个通用框架就能搞定一大半需求。最后再唠叨一句爬虫是个技术活但更重要是守规矩。JWT模拟登录能力是为了访问那些你本来就有权限访问的数据而不是帮你突破权限边界。我在做项目时一直坚持一个原则先确认自己有合法访问权限再动手写代码。技术没有善恶但使用技术的场景一定要有分寸。按照这个原则做事才能把爬虫这门手艺长期稳定地做下去。
阅读完成 · 觉得有帮助?
咨询建站