ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Python模拟登录携程:从HTTP请求到session管理实战

用Python模拟登录携程:从HTTP请求到session管理实战 很多人学Python学到第三四周就开始琢磨写爬虫然后几乎毫无例外地卡在登录上。我自己带过不少新人也看过很多代码大家把请求发出去返回来的却是登录页或者浏览器里能正常登录脚本一跑就报错。与其反复试错不如专门做一个“用户登录接口”的练习项目拿携程登录这种真实场景当切入点把HTTP请求、session管理、参数构造、返回解析这些基本功一次性打通。这篇文章就围绕这样一个练习展开先分析携程网页登录背后的请求链路再自己动手设计一个登录接口最后用Python把模拟登录流程完整串起来。1. 为什么拿用户登录接口当Python练手项目1.1 新手练手最常卡在哪一步我在各种技术群里看到最多的问题就是“为什么我用requests登录不了”、“为什么返回的页面还是登录状态”、“cookie到底要怎么带”仔细看代码基本都犯了同一个毛病只盯着一个POST请求完全不关心前置页面、会话状态、动态参数是怎么来的。携程登录是个很好的教学例子。它的请求流程不算特别复杂但该有的环节都有先访问登录页拿到初始cookie再获取用户名密码输入框对应的参数提交登录请求最后通过跳转或接口确认状态。如果能把这条链路完整跑通你就能理解大多数Web登录系统的通用逻辑而不是死记某家网站的代码。另一个常见的坑是抄代码。网上确实能找到很多“携程模拟登录”的脚本但这类脚本通常活不过三个月因为网站端的参数名、加密方式、风控策略都在变。今天能用下周可能就失效。与其抄一份随时会坏的代码不如把登录接口的底层逻辑搞清楚到时候换一个网站也能快速上手。1.2 这个练习能带动哪些Python基础能力一个登录接口练习覆盖的能力点比大多数人想象的要广。它不是“会写requests.post”就完事而是会逼着你把以下东西全部串起来HTTP基础GET/POST、请求头、状态码、cookie/session机制。数据序列化表单提交、JSON格式、URL编码。参数加密MD5、SHA256、加盐等常见概念。会话保持requests.Session的用法以及为什么不能每次new一个requests。异常处理网络超时、返回结构变化、字段缺失时的防御。模块化设计把请求操作封装成类或函数而不是一坨脚本写到底。下面这个表格是我在带新人时常用的一个“技能清单”每完成一项就勾掉一项能力点练习中对应的任务是否接触HTTP请求方法用POST提交用户名密码是请求头构造设置User-Agent、Referer是cookie管理使用Session保持状态是数据解析从JSON或HTML中提取登录结果是参数加密对密码做哈希处理是接口设计自己写一个登录后端接口是调试排错通过抓包对比失败原因是我一直认为登录接口是“第一个有完整业务语义”的接口。它不像那些纯返回文本的测试接口登录成功和失败会直接影响后续所有请求所以写完以后你能立刻看到自己的代码是对是错反馈非常直接。1.3 学习和练习的边界先说清楚几个原则先讲清楚不仅是为了安全也是为了让学习效率更高。第一不鼓励对线上站点做高频请求或大量账号尝试。真实网站有风控机制频繁登录轻则验证码升级重则账号被临时锁定。练习时完全可以在本地用自己的Flask接口模拟或者使用公开的测试环境。第二验证码是用来保护系统的不是用来“绕过”的。学习验证码的生成和校验逻辑可以但不要试图做任何绕过操作。本地练习可以直接关闭验证码功能或者使用测试接口提供的固定验证码效果是一样的。第三本篇文章里的“模拟登录”重点在理解HTTP请求与用户登录接口的交互过程而不是把某个网站的逆向细节逐一抄出来。参数名的变化只需要掌握观察方法不需要去对抗风控。2. 拆解携程网页登录背后的请求链路2.1 打开浏览器开发者工具先看懂登录请求想做模拟登录第一步永远不是写代码而是打开浏览器按F12亲手“看”一次登录过程。以携程的PC端登录为例在Network面板里勾上Preserve log然后输入账号密码点击登录。这时候会看到一大批请求不要慌先过滤出包含login、auth、passport、sso这类关键词的请求。通常登录提交是一个POST请求请求地址一般是passport.ctrip.com或类似域名下的接口。点开这个POST请求重点看两个地方Headers里面会有Content-Type、User-Agent、Referer、Origin等请求头字段。Payload或Form Data里面是真正提交的用户名、密码、验证码以及一些辅助字段。很多人一上来就复制Form Data里的字段名这是好事但还不够。你要学会问三个问题这些字段是用户输入的还是页面生成的所有字段都是必填的吗有没有字段是上一次GET请求时服务端下发的举个典型例子登录页第一次打开时服务端会在页面里写入一个动态token或者设置一个cookie提交登录时必须把这个动态值原样带上。如果跳过GET这步直接POST服务端会判定请求不合法。这种现象在很多登录系统里都存在携程的某些登录入口也有类似机制。所以看请求时要顺着完整链路看不要只看最后一下点击。从输入账号密码之前到登录成功跳转之后整条链路上的请求都要过一遍。2.2 session与cookie如何维持登录状态理解登录绕不开session和cookie。用大白话解释你把账号密码交给服务器服务器验证通过后给你发一张“门票”也就是cookie。之后你每次访问需要登录的页面浏览器都会自动把这张门票带上服务器看见门票就认得你。在requests库里完成这个动作的对象叫Session。它的作用就像一个“浏览器档口”会自动帮你保存服务端Set-Cookie过来的内容并在后续请求中自动带上。import requests session requests.Session() # 第一次访问登录页服务端可能在响应中种下cookie login_page session.get(https://example.com/login) print(session.cookies.get_dict()) # 携带同一个session提交登录cookie不会丢 login_data {username: your_name, password: 123456} resp session.post(https://example.com/api/login, datalogin_data)很多新手犯的错误是混用session和requests独立请求# 错误示范 requests.get(https://example.com/login) requests.post(https://example.com/api/login, datalogin_data)这样发送POST时之前GET阶段拿到的cookie完全没有被带上服务端自然认为你是一个陌生请求。这个现象模拟了90%的“登录失败”问题。2.3 登录成功后真正判断登录状态的不是“请求成功”HTTP状态码200只能说明请求发送成功了不代表账号密码正确也不代表登录成功。真正的登录成功判断往往要看后续请求。比较常见的做法是登录提交接口返回一个包含用户信息的JSON或者服务端返回一个跳转地址前端再带着cookie访问一个“获取当前用户信息”的接口。如果这个用户信息接口能返回你的昵称或用户ID那才算真正登录成功。比如你可以模拟这样一个验证流程profile_resp session.get(https://example.com/api/user/info) if profile_resp.status_code 200: data profile_resp.json() if data.get(code) 0: print(登录成功当前用户:, data.get(data, {}).get(nickname)) else: print(登录失败:, data.get(message))这个思路也提醒我们设计登录接口练习时不要只盯着“提交登录”那一个接口要把“登录后的状态校验”也一起纳入。这样你才能知道自己的session到底有没有生效。3. 自己动手写一个用户登录接口3.1 接口协议先定清楚请求与返回学调用登录接口之前最好先自己写一个登录接口。身份互换一下你才能理解服务端到底在做什么校验。我们先定义接口协议这是开发中非常推荐的步骤别一上来就写代码。请求方式POST /api/login Content-Type: application/json请求体{ username: test_user, password: e9d71f5ee7c92d6dc9e92ffdad17b8bd49418f98, captcha: 8f3j }返回体{ code: 0, message: 登录成功, data: { token: abc123, nickname: 测试用户 } }这里我建议采用“HTTP状态码 业务码”的双层结构。HTTP状态码只负责表示请求本身是否正常业务码负责表示业务逻辑正确与否。好处是后续写调用方代码时逻辑更清晰。我常用的一套业务码约定code含义0登录成功1001用户名或密码错误1002验证码错误或过期1003参数缺失1004请求过于频繁这样的设计在实际项目中很常见。你作为客户端去调别人接口时也要先找到对方的“业务码文档”而不是看到200就觉得万事大吉。3.2 密码不能明文传输加密与加盐自己设计登录接口时要记住一个基本安全常识前端提交密码不能明文直接传也不能在数据库里存明文。这里涉及两个方向传输端使用HTTPS必要时对密码做RSA加密。存储端对密码做哈希处理通常还要加盐。练习项目不必用RSA但可以用SHA256加盐来体验一下。所谓“加盐”就是在原始密码后面拼接一段随机字符串再做哈希防止两个相同密码的哈希值相同。示例代码import hashlib import os def hash_password(password: str, salt: str None) - tuple: if salt is None: salt os.urandom(16).hex() salted password salt digest hashlib.sha256(salted.encode(utf-8)).hexdigest() return digest, salt服务端保存的是digest和salt。用户登录时取出该用户名对应的salt把用户提交的明文密码拼上salt再做一次哈希和数据库中的digest比较。这里想多说一句很多新手觉得加密只是“把代码写得神秘一点”其实加密的目的是让数据库泄露时攻击者无法直接拿到用户的原始密码。你写练习时当然可以简单点但这个意识要建立起来。3.3 Flask实现最小可运行的登录接口下面写一个最小可运行的Flask登录接口。为了保持代码简洁我直接在内存里放了一个用户表并把验证码固定为测试码。from flask import Flask, request, jsonify import hashlib app Flask(__name__) # 模拟数据库实际项目中应使用MySQL/Redis等存储 users { test_user: { password_hash: 9e6a8e6a8e6a8e6a8e6a8e6a8e6a8e6a8e6a8e6a8e6a8e6a8e6a8e6a8e6a8, # 示例值 salt: fixed_salt_value, nickname: 测试用户 } } def sha256_with_salt(password: str, salt: str) - str: return hashlib.sha256((password salt).encode(utf-8)).hexdigest() app.route(/api/login, methods[POST]) def login(): data request.get_json(forceTrue) username data.get(username, ) password data.get(password, ) captcha data.get(captcha, ) if not username or not password or not captcha: return jsonify({code: 1003, message: 参数缺失}), 400 # 测试环境验证码固定为 8888 if captcha ! 8888: return jsonify({code: 1002, message: 验证码错误或过期}), 401 user users.get(username) if not user: return jsonify({code: 1001, message: 用户名或密码错误}), 401 password_hash sha256_with_salt(password, user[salt]) if password_hash ! user[password_hash]: return jsonify({code: 1001, message: 用户名或密码错误}), 401 token hashlib.md5((username secret).encode(utf-8)).hexdigest() return jsonify({ code: 0, message: 登录成功, data: { token: token, nickname: user[nickname] } }) if __name__ __main__: app.run(host127.0.0.1, port5000)这个接口已经包含了参数校验、验证码校验、密码比对、token生成四个核心步骤。当你用requests去调用它时就形成了一次完整的登录流程训练。4. 从接口测试到模拟登录的完整实现4.1 准备环境与依赖在开始写模拟登录客户端之前先把环境准备好。假设你已经安装了Python然后创建虚拟环境并安装依赖。python -m venv venv source venv/bin/activate # Windows下是 venv\Scripts\activate pip install requests flask如果pip下载速度慢可以临时使用国内镜像源pip install requests flask -i https://pypi.tuna.tsinghua.edu.cn/simple这里要提醒一下Python版本建议3.8及以上requests库会用到比较新的特性。如果你刚接触Python还没有安装好环境先把Python装好再继续。安装完成之后在命令行输入python --version确认版本能正常输出版本号就说明环境没问题。4.2 用Session封装一个登录器在真实项目里模拟登录很少写成几百行顺序执行的脚本更常见的是用一个类来封装。下面我用一个“携程登录Demo”风格的类来演示但请求地址和参数名都改成示例值方便你理解结构。真实对接时把你抓包得到的URL和字段填进去就行。import requests class LoginClient: def __init__(self): self.session requests.Session() self.session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://example.com/login }) self.base_url https://example.com def get_login_page(self): # 先访问登录页拿到初始cookie和可能的动态token resp self.session.get(f{self.base_url}/login) print(initial cookies:, self.session.cookies.get_dict()) return resp def get_captcha(self, captcha_idNone): # 部分验证码系统需要先获取一个captchaId提交时带上 resp self.session.get(f{self.base_url}/captcha) data resp.json() return data.get(captchaId) def login(self, username: str, password: str): captcha_id self.get_captcha() login_data { username: username, password: password, captchaId: captcha_id, captcha: 8888, rememberMe: false } resp self.session.post(f{self.base_url}/api/login, jsonlogin_data) print(login response:, resp.text) return resp def get_profile(self): # 登录之后用同一个session请求受保护接口 resp self.session.get(f{self.base_url}/api/user/info) return resp.json()这个类把“保持会话”这个最核心的逻辑用session对象隐藏起来了。你在调用login方法之后不需要手动把cookie塞进get_profile请求session会自动处理。这也是我在练习里最想让大家体会的一点登录器不是一个“会发请求的函数”而是一个“管理会话状态的对象”。这个思想在你以后对接任何需要认证的API时都用得上无论是携程还是其他平台。4.3 验证登录是否成功的三层判断模拟登录完成之后怎么判断到底成功没有我习惯做三层验证每一层都有不同目的。第一层看HTTP状态码。如果请求返回200说明请求被服务端正确处理了如果返回401或403说明认证出了问题如果返回500通常是服务端逻辑错误或参数格式不对。第二层看业务码。如果服务端返回JSON里面通常有code字段。code等于0才是成功1001、1002等等都代表不同业务失败原因。第三层看受保护接口是否能访问。这是最硬核的判断标准。登录成功后用同一个session去请求一个需要登录才能访问的接口如果能拿到用户数据说明你的登录状态真实有效如果拿到的还是登录页说明前面某个环节出了毛病。第三层验证的代码很简单client LoginClient() client.get_login_page() client.login(test_user, 123456) profile client.get_profile() if profile.get(code) 0: print(登录验证通过用户:, profile[data][nickname]) else: print(登录未生效返回信息:, profile)我见过很多同事调试登录脚本时只看到第一层返回200就停了结果后续请求全部失败。养成三层验证的习惯能帮你少踩很多坑。5. 我在这个练习里踩过的坑5.1 只发POST不先访问登录页登录永远失败这个坑出现的频率最高。很多人拿到一个登录接口的URL就直接session.post提交用户名密码结果返回一个错误码。原因很简单登录页本身会种下一些初始cookie这些cookie是后续登录请求的前置条件。如果不先GET一次登录页服务端可能认为你是一个没有访问过页面、突然出现的机器人直接拒绝。正确的顺序永远是先GET登录页再POST登录数据最后GET用户信息。这三个动作必须在同一个session里完成。我建议在代码里加一个流程控制或者至少在日志中打印出当前session的cookie状态。当你登录失败时第一件事不是改参数而是确认前置cookie有没有拿到。5.2 请求头少了一个字段被拦了半小时又一次我在调试一个登录接口时明明参数和cookie都正确但服务端一直返回“非法的请求来源”。最后逐项对比浏览器里的请求头发现少了Referer字段。很多服务端都会校验Referer或Origin用来确认请求是从自己的页面发出的。如果你用requests直接发POST默认不会带Referer。解决问题的方法很笨但很有效把浏览器请求头里重要的字段全部复制到代码里。通常需要关注这几个User-Agent标记客户端类型缺失或太旧容易被识别为脚本。Referer标记从哪个页面跳转过来有些登录接口必须带上。Origin跨域请求时校验用。Content-Type告诉服务端提交的格式。self.session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Referer: https://example.com/login, Origin: https://example.com, Content-Type: application/json })5.3 验证码的正确处理方式这里想重点说说验证码。很多初级教程都在教“如何识别验证码”或者“如何打码”但我更建议把验证码当作接口流程的一个普通参数来理解而不是跳过去对抗它。一个正常的验证码流程是这样的客户端请求验证码图片接口。服务端生成图片同时把验证码与本次会话绑定常见做法是把captchaId返回给前端并存到服务端缓存中。用户输入验证码后提交登录时带上captchaId和captcha。服务端校验captchaId和captcha是否匹配同时校验是否过期。如果你的练习环境没法关闭验证码那就每次都先用session请求一次验证码接口把captchaId和图片保存下来然后人工看图填码。千万不要把“识别验证码”和“绕过验证码”混为一谈。我见过的最多的错误是直接POST登录但完全没带验证码相关参数或者带了captcha却漏了captchaId。这两个参数是成对出现的少了任何一个都会校验失败。5.4 返回结构飘忽不定JSON解析别写死登录接口的返回结构有时候不是固定的。比如登录成功时返回一个嵌套很深的JSON登录失败时返回一个简单的字符串。如果你在代码里写死response.json()[data][token]一旦失败就会直接抛KeyError。稳妥的做法是先判断返回类型和关键字段是否存在try: resp_json resp.json() except Exception: print(响应不是JSON原始内容:, resp.text) return if resp_json.get(code) 0: token resp_json.get(data, {}).get(token, ) else: print(接口返回错误:, resp_json.get(message))这种防御式写法在真实项目中非常重要因为登录接口不像文档里写得那么“守规矩”。5.5 时间与随机数的坑有些登录接口会在参数里加入时间戳和随机数用于防止请求重放。这时候如果直接复制浏览器抓包里的值下一次请求肯定失效。正确做法是动态生成import time import random timestamp int(time.time() * 1000) nonce str(random.randint(100000, 999999)) login_data { username: test_user, password: 123456, timestamp: timestamp, nonce: nonce }我在练习中遇到这个问题时一度以为是加密算法不对排查了很久才发现是时间戳用了固定值。从那以后凡是与时间相关的参数我都先检查是不是动态生成的。最后再分享一点个人经验每次带新人我第一周就会让他写一个用户登录接口看完他的代码大概就能判断出他之后会遇到哪些问题。平心而论这个练习不是为了让谁去抓别人的数据而是因为登录接口能把Python基础、HTTP协议、会话管理、异常处理这些知识全部串起来。你先自己用Flask写一个后端登录接口再用requests去调用它最后再对照真实站点的抓包结果调整参数名这个顺序别搞反。一个小技巧先用Postman或Apipost这类工具把请求调通再翻译成Python代码比直接在代码里调试省力很多。另外拿到真实站点接口时先看请求头、再看Form Data、最后看返回体不要凭感觉猜参数。保持这个习惯你在任意网站的登录接口上都能快速上手而不是被某一个网站的临时方案困住。
返回列表