用Python requests搞定Cookie登录,我绕过了三个大坑才成功

背景:为什么我要跟Cookie死磕?

上个月接了个内部需求,要每天定时从公司用的一个项目管理平台(类似Jira)拉取任务数据,生成报表。这个平台没有提供官方API,只能通过网页端操作。手动操作太耗时,我就想写个Python脚本自动化。

一开始想得太简单:找到登录接口,POST用户名密码,拿到Cookie,然后用这个Cookie去访问数据页面。听起来很顺对吧?结果一上手就发现,这个网站的登录流程比我想的复杂得多------有CSRF token,登录后还有一次302重定向,而且Cookie的生效路径和域名都有讲究。我前后折腾了三天,把requestsSession对象、CookieJar以及HTTP状态码查了个遍,才终于让脚本稳定跑起来。

如果你也遇到需要处理复杂登录鉴权的网站,特别是那些用传统表单登录、有各种安全校验的,我踩过的这些坑或许能帮你省下不少时间。

问题分析:为什么直接发POST请求不行?

我最开始的代码长这样(已脱敏):

python 复制代码
import requests

login_url = 'https://example.com/login'
data_url = 'https://example.com/api/data'

payload = {
    'username': 'my_user',
    'password': 'my_pass'
}

# 尝试1:直接登录
resp = requests.post(login_url, data=payload)
print(resp.status_code)  # 输出200,好像成功了?

# 尝试用这个resp的cookies去访问数据
cookies = resp.cookies
data_resp = requests.get(data_url, cookies=cookies)
print(data_resp.status_code)  # 输出403!被拒绝了。

第一个200状态码给了我虚假的希望。后来仔细看data_resp.text,发现返回的是登录页面的HTML------意思就是根本没登录成功,服务器把我踢回去了。

排查过程:

  1. 检查登录响应 :我打印了resp.history,发现是一个空列表,说明没有重定向?不对,感觉有问题。
  2. 手动模拟对比:我用浏览器开发者工具,仔细对比了我的脚本请求和浏览器正常登录发出的请求。
  3. 发现关键差异
    • 缺失的Token :浏览器登录时,POST数据里除了账号密码,还有一个叫csrfmiddlewaretoken的字段,值是一长串字符。这个token是从登录页的HTML表单里提前获取的。
    • Cookie的接收 :浏览器在GET登录页面时,服务器就已经下发了一个sessionid的Cookie。后续的POST登录请求,会带上这个Cookie 一起发送。而我的脚本是直接POST,完全没理会登录页。
    • 重定向处理 :浏览器登录成功后,会收到一个302状态码,然后自动跳转到首页。我的脚本没有自动跟随重定向(requests默认是跟随的),但即便跟随了,因为前面的问题,跳转后的请求也缺乏正确的会话状态。

所以,核心问题不是"发送登录请求",而是模拟一次完整的浏览器会话:先获取页面和初始Cookie,提取隐藏的校验参数,再发起登录,并妥善处理登录后的跳转,最后维持这个会话状态去访问其他页面。

核心实现

1. 使用Session对象维持会话状态

这是我学到的第一个,也是最重要的教训:对于需要登录的连续请求,一定要用requests.Session()

Session对象会帮你自动管理Cookies。它会在同一个会话内,自动将服务器返回的Cookie保存下来,并在后续的请求中自动带上,完美模拟浏览器的行为。不用你再手动去resp.cookies然后塞到下一个请求里。

python 复制代码
import requests

# 创建会话,这是所有操作的基础
session = requests.Session()
# 可以设置一个通用的请求头,更像浏览器
session.headers.update({
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
})

这里有个坑 :有些网站会检查User-Agent,用默认的python-requests可能会被拒绝。所以最好一开始就设置一个常见的浏览器UA。

2. 获取登录页并解析CSRF Token

很多Web框架(如Django、Flask)为了防御CSRF攻击,会在表单里埋一个随机token。你必须先访问登录页(GET请求),拿到这个token,才能用于接下来的POST请求。

同时,这个GET请求会让服务器下发初始的会话Cookie(比如sessionid),Session对象会自动为我们保存。

python 复制代码
login_page_url = 'https://example.com/login'

# 第一次GET请求,获取页面和初始Cookie
login_page_response = session.get(login_page_url)
print(f"获取登录页状态码: {login_page_response.status_code}")

# 这里假设token在HTML中一个name='csrfmiddlewaretoken'的input标签里
# 我们使用lxml来解析(需要安装:pip install lxml)
from lxml import html

tree = html.fromstring(login_page_response.text)
# 尝试通过input标签的name属性获取csrf token
# 注意:不同网站token的字段名和获取方式可能不同,这里是常见情况
csrf_token = tree.xpath('//input[@name="csrfmiddlewaretoken"]/@value')
if csrf_token:
    csrf_token = csrf_token[0]
    print(f"成功提取CSRF Token: {csrf_token[:20]}...")  # 打印前20位
else:
    # 如果xpath没找到,可能需要检查网页源码,看看token藏在哪里
    # 也可能网站用了其他名称,如`authenticity_token`, `_token`等
    print("警告:未找到CSRF Token,可能需要调整解析方式!")
    # 有时token可能在meta标签里://meta[@name="csrf-token"]/@content

注意这个细节 :token的名称和获取方式因网站而异。必须用浏览器的"检查元素"功能,仔细查看登录表单的源码 ,找到那个隐藏的<input>字段。这是成功的第一步。

3. 构造登录请求并正确处理重定向

现在,我们有了初始Cookie(在session里),也有了CSRF Token,可以构造POST请求了。

python 复制代码
login_action_url = 'https://example.com/login'  # 通常和登录页是同一个URL
payload = {
    'username': 'your_username_here',
    'password': 'your_password_here',
    'csrfmiddlewaretoken': csrf_token,  # 关键:把上一步提取的token加进去
    # 可能还有其他隐藏字段,如`next`(登录后跳转地址)
}

print("正在发起登录POST请求...")
# 用session.post,它会自动带上GET登录页时获得的Cookie
login_response = session.post(login_action_url, data=payload)

# 重要:检查登录是否真的成功
print(f"登录POST请求状态码: {login_response.status_code}")
print(f"登录后重定向历史: {[r.status_code for r in login_response.history]}")
print(f"最终URL: {login_response.url}")

关键点分析

  • login_response.history:如果登录成功,服务器通常会返回302303状态码进行重定向。requests会自动跟随,并将这些中间响应记录在history列表里。最后一个响应才是重定向终点(如首页)的内容。所以,看到history里有302,且最终URL是登录后的页面(而不是登录页本身),是一个好迹象。
  • login_response.url:检查最终停留的页面URL,如果还是登录页的URL,那大概率是登录失败了(账号错误、token无效等)。

4. 验证登录状态并访问目标数据

如何确认session已经处于登录状态?一个简单的方法是尝试访问一个需要登录才能看的页面。

python 复制代码
# 访问一个登录后才能访问的页面,比如个人主页或API接口
profile_url = 'https://example.com/dashboard'
profile_response = session.get(profile_url)

print(f"访问个人页状态码: {profile_response.status_code}")
if profile_response.status_code == 200:
    # 可以进一步检查返回内容里是否有登录用户的特征信息
    if "欢迎回来" in profile_response.text or "Dashboard" in profile_response.text:
        print("✅ 登录状态验证成功!")
    else:
        print("⚠️  状态码200,但内容可能不对,需要检查。")
else:
    print(f"❌ 访问失败,状态码: {profile_response.status_code},登录可能未成功。")

验证成功后,你就可以用这个已经携带了有效登录Cookie的session对象,去任意访问需要鉴权的接口了。

python 复制代码
# 最终访问我们最初想要的数据接口
target_data_url = 'https://example.com/api/v1/tasks'
data_response = session.get(target_data_url)

if data_response.status_code == 200:
    data = data_response.json()  # 假设接口返回JSON
    print(f"成功获取数据,共 {len(data)} 条记录。")
    # ... 处理你的数据
else:
    print(f"获取数据失败: {data_response.status_code}")
    print(data_response.text)

完整代码示例

下面是一个整合了所有步骤、稍作抽象、可直接修改运行的完整脚本。请将your_username_hereyour_password_here以及URL替换成你的目标网站信息。

python 复制代码
import requests
from lxml import html
import time

class WebsiteLoginFetcher:
    """一个用于处理带Cookie登录的示例类"""

    def __init__(self, base_url):
        self.base_url = base_url.rstrip('/')
        self.session = requests.Session()
        # 设置一个像浏览器的请求头
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
        })

    def get_csrf_token(self, login_page_url):
        """从登录页面HTML中提取CSRF Token"""
        print(f"[1] 正在获取登录页面并提取Token: {login_page_url}")
        try:
            resp = self.session.get(login_page_url, timeout=10)
            resp.raise_for_status()  # 如果状态码不是200,抛出异常
        except requests.exceptions.RequestException as e:
            print(f"  获取登录页失败: {e}")
            return None

        # 使用lxml解析HTML
        tree = html.fromstring(resp.text)
        
        # 尝试几种常见的CSRF Token存放位置
        token = None
        # 方式1: 在input隐藏字段中
        token_elements = tree.xpath('//input[@name="csrfmiddlewaretoken"]/@value')
        if not token_elements:
            # 方式2: 其他常见名称
            for name in ['csrf_token', '_token', 'authenticity_token', 'csrf']:
                token_elements = tree.xpath(f'//input[@name="{name}"]/@value')
                if token_elements:
                    break
        if not token_elements:
            # 方式3: 在meta标签中
            token_elements = tree.xpath('//meta[@name="csrf-token"]/@content')

        if token_elements:
            token = token_elements[0]
            print(f"  成功提取Token: {token[:30]}...")
        else:
            print("  警告:未能在页面中找到CSRF Token。可能需要手动分析页面结构。")
            # 打印部分页面源码以便调试
            # print(resp.text[:2000])
        return token

    def login(self, login_url, username, password, extra_form_data=None):
        """执行登录操作"""
        print(f"[2] 正在执行登录...")
        # 1. 获取Token
        csrf_token = self.get_csrf_token(login_url)
        if not csrf_token:
            print("  登录中止:无法获取CSRF Token。")
            return False

        # 2. 构造登录数据
        login_data = {
            'username': username,
            'password': password,
            'csrfmiddlewaretoken': csrf_token,  # 关键字段
        }
        if extra_form_data:
            login_data.update(extra_form_data)

        # 3. 发起登录POST请求
        try:
            # 注意:有些网站登录后不希望自动重定向,可以设置`allow_redirects=False`先看看
            resp = self.session.post(login_url, data=login_data, timeout=15, allow_redirects=True)
        except requests.exceptions.RequestException as e:
            print(f"  登录请求失败: {e}")
            return False

        print(f"  登录POST状态码: {resp.status_code}")
        print(f"  重定向历史: {[h.status_code for h in resp.history]}")
        print(f"  最终URL: {resp.url}")

        # 4. 简单判断登录是否成功
        # 条件1: 有重定向历史(通常302)
        # 条件2: 最终URL不是登录页本身
        if resp.history and login_url not in resp.url:
            print("  ✅ 登录流程看起来成功(发生了重定向)。")
            return True
        else:
            print("  ❌ 登录可能失败(未发生预期重定向或仍停留在登录页)。")
            # 可以打印响应内容的前500字符帮助调试
            # print("响应内容预览:", resp.text[:500])
            return False

    def fetch_protected_data(self, data_url):
        """使用已登录的session访问受保护的数据"""
        print(f"[3] 正在访问受保护数据: {data_url}")
        try:
            resp = self.session.get(data_url, timeout=10)
            print(f"  数据请求状态码: {resp.status_code}")
            if resp.status_code == 200:
                return resp.text  # 或者 resp.json()
            else:
                print(f"  请求失败,响应内容: {resp.text[:200]}")
                return None
        except requests.exceptions.RequestException as e:
            print(f"  数据请求异常: {e}")
            return None

# ========== 使用示例 ==========
if __name__ == '__main__':
    # !!!请替换为你的实际信息 !!!
    BASE_URL = 'https://example.com'  # 网站根地址
    LOGIN_PAGE_URL = f'{BASE_URL}/login'  # 登录页面地址
    USERNAME = 'your_username_here'
    PASSWORD = 'your_password_here'
    TARGET_DATA_URL = f'{BASE_URL}/api/some_data'  # 你想爬的数据地址

    fetcher = WebsiteLoginFetcher(BASE_URL)

    # 执行登录
    is_logged_in = fetcher.login(LOGIN_PAGE_URL, USERNAME, PASSWORD)
    
    if is_logged_in:
        print("\n登录成功!开始获取数据...")
        # 可以加个短暂延迟,避免请求过快
        time.sleep(1)
        data = fetcher.fetch_protected_data(TARGET_DATA_URL)
        if data:
            print("数据获取成功!")
            # 这里处理你的数据,比如解析JSON或HTML
            # print(data[:500]) # 打印前500字符看看
        else:
            print("数据获取失败。")
    else:
        print("\n登录失败,请检查账号、密码、网络或网站结构是否变化。")

踩坑记录

  1. 坑1:忽略初始GET请求和CSRF Token

    • 现象:直接POST账号密码,返回200但实际未登录。
    • 原因:没获取登录页下发的初始Cookie和表单里的隐藏Token。
    • 解决 :老老实实先GET登录页,用lxmlBeautifulSoup解析出Token,并用Session对象维持Cookie。
  2. 坑2:重定向(302)处理不当

    • 现象 :登录请求返回200,但history为空,url还是登录页。
    • 原因 :网站可能用了JavaScript跳转,或者登录失败时也返回200并展示错误信息在登录页。requests的自动重定向只针对3xx状态码。
    • 解决 :不要只看最终状态码。检查response.history是否有302,并对比response.url是否跳转到了登录后的页面(如首页、仪表盘)。更可靠的方法是登录后立即访问一个需要登录的页面来验证。
  3. 坑3:Cookie作用域(Domain/Path)问题

    • 现象 :登录成功,用session访问/dashboard正常,但访问/api/v1/data却返回403。
    • 原因 :服务器设置的Cookie可能指定了Path(如Path=/web)或Domainrequests.SessionCookieJar会严格按照这些属性来匹配和发送Cookie。如果API的路径不在Cookie的Path范围内,请求就不会带上这个Cookie。
    • 解决 :用print(session.cookies)查看所有Cookie的详细信息。如果真是作用域问题,可能需要调整请求的URL,或者与后端沟通(如果是自己的项目)。对于爬虫,有时需要"欺骗"一下,但需注意合规性。
  4. 坑4:Session未正确复用

    • 现象 :我把登录的代码写在一个函数里,函数内部创建了session并登录成功。但在函数外部,我又用了一个新的requests.get去访问数据,结果失败。
    • 原因requests.get()是独立的函数调用,不会使用函数内部创建的那个session对象的状态。
    • 解决 :将session对象作为类的属性(如上面的示例)或全局变量,确保在整个流程中使用的都是同一个session实例。

小结

搞定Python requests的Cookie登录,核心就两点:Session对象维持状态完整模拟浏览器的"GET取Token -> POST带Token登录 -> 跟随跳转"流程 。过程中最花时间的往往不是写代码,而是用开发者工具仔细分析网站的请求细节。下次再遇到,你就知道该从哪里下手了。如果想更深入,可以研究一下如何处理更复杂的认证(如OAuth 2.0)、验证码自动识别,或者使用selenium应对纯JavaScript渲染的登录场景。

相关推荐
MIXLLRED2 小时前
Python模块详解(一)—— socket 和 threading 模块
开发语言·python·socket·threading
Jay-r2 小时前
OpenClaw养龙虾工具安全风险分析:五大隐患及防护建议引言
网络·python·安全·web安全·ai助手·openclaw
C蔡博士3 小时前
最近点对问题(Closest Pair of Points)
java·python·算法
APIshop3 小时前
Java调用亚马逊商品详情API接口完全指南
java·开发语言·python
nimadan123 小时前
**豆包seed写剧本2025指南,AI编剧工具实战应用解析**
人工智能·python
沉下去,苦磨练!3 小时前
python的if __name__ == ‘__main__‘
python
killer Curry3 小时前
Polar CTF PWN 简单(1)(持续更新)
笔记·python·算法
DeepModel3 小时前
【概率分布】卡方分布的原理、推导与实战应用
python·算法·概率论
6+h3 小时前
【java】System类详解
java·开发语言·python