用 Python requests 搞定网站登录鉴权,我踩过的三个 Cookie 大坑

5 阅读1分钟

背景:一个看似简单的自动化需求

上个月,我接了个内部需求:每天自动从公司的某个运营后台拉取最新的数据报表。这个后台是个典型的 Web 系统,需要先登录,登录后跳转到仪表盘,然后才能访问那些有权限的报表页面。

第一反应就是用 Python 的 requests 库。这活儿听起来挺简单的,不就是发个 POST 请求登录,然后拿着返回的 Cookie 去访问其他页面嘛。我一开始信心满满,觉得一两个小时就能搞定。结果,光是搞明白这个网站的 Cookie 鉴权机制,就花了我整整两天,中间遇到了各种意想不到的问题。今天就把这个踩坑和填坑的过程完整记录下来,如果你也打算用 requests 处理需要登录的网站,希望我的经历能帮你省点时间。

问题分析:为什么我的登录状态总是失效?

我最开始的思路非常直接:

  1. requests.post 向登录接口发送用户名和密码。
  2. 从登录响应的 headers 或者 cookies 属性里把 Cookie 拿出来。
  3. 在后续请求的 headers 里手动加上 Cookie: xxx=yyy

写了个简单的脚本跑了一下,登录接口返回了 200,甚至看到了 "success": true 的字样。我心中一喜,以为成功了。紧接着,我用拿到的 Cookie 去请求仪表盘页面,结果返回的却是登录页的 HTML!状态码是 200,但内容明显不对。

这就奇怪了,明明登录成功了,为什么身份没带过去呢?

我的排查过程是这样的:

  1. 对比浏览器行为:我首先用 Chrome 的开发者工具,完整地录制了一次手动登录和访问的流程。我发现,浏览器在登录成功后,服务器通过 Set-Cookie 响应头设置了不止一个 Cookie,而是好几个,比如 sessionid, csrftoken, 还有一个叫 remember_token 的。
  2. 检查请求头:我注意到浏览器在后续请求中,会自动、完整地带上所有这些 Cookie,而我的脚本只提取了其中一个(通常是第一个)。
  3. 检查 Cookie 作用域:有些 Cookie 的 DomainPath 属性比较特别,我的脚本可能没有正确处理这些属性,导致 Cookie 没有在正确的请求中被发送。
  4. 检查会话保持:我猛然意识到,我犯了一个低级错误:我每次发请求都创建了一个新的 requests.Session(),而 Session 对象才是用来在多次请求间保持 Cookie 的关键!我用的是孤立的 requests.get/post,而不是同一个 Session 实例的 get/post 方法。

问题根源找到了:我没有使用 requests.Session() 来管理 Cookie,并且手动处理 Cookie 的方式过于粗糙,无法模拟浏览器的完整行为

核心实现

第一步:使用 Session 对象,让 Cookie 管理自动化

requests.Session() 是解决这个问题的核心。它会自动存储服务器通过 Set-Cookie 头下发的所有 Cookie,并在后续所有同会话的请求中,自动、正确地附加这些 Cookie。这完美模拟了浏览器的行为。

import requests

# 创建一个会话对象,这是关键!
session = requests.Session()

# 登录
login_url = "https://example.com/login"
login_data = {
    'username': 'your_username',
    'password': 'your_password'
}
# 注意:这里用的是 session.post,不是 requests.post
login_response = session.post(login_url, data=login_data)

print(f"登录状态码: {login_response.status_code}")
# 可以打印一下会话里的 Cookie 看看
print(f"当前会话Cookie: {session.cookies.get_dict()}")

这里有个坑:有些网站登录可能需要先访问登录页获取一个初始的 Cookie(比如 CSRF token),然后再用这个 Cookie 去提交登录表单。如果登录失败,记得检查是否需要这个前置步骤。

第二步:处理登录时的额外令牌(如 CSRF Token)

很多现代网站(尤其是 Django、Flask 等框架搭建的)为了安全,会在登录表单里埋一个 CSRF(跨站请求伪造)令牌。这个令牌通常放在表单的一个隐藏输入框里,或者作为一个 Cookie 下发,登录 POST 请求时必须带上它。

我的目标网站就用了这一套。解决方案是:

  1. GET 一下登录页面。
  2. 从页面 HTML 中解析出 CSRF token(或者从初始 Cookie 里拿)。
  3. 将这个 token 加入到登录的 POST 数据中。
import re

# 1. 先获取登录页面,让服务器设置初始Cookie(如csrftoken)
login_page_url = "https://example.com/login"
login_page_response = session.get(login_page_url)

# 2. 从响应HTML中查找CSRF token(假设它在name='csrfmiddlewaretoken'的input里)
# 这是一个简单的正则示例,实际中建议用BeautifulSoup等解析库更稳健
html_content = login_page_response.text
csrf_token_match = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', html_content)

if csrf_token_match:
    csrf_token = csrf_token_match.group(1)
    print(f"获取到CSRF Token: {csrf_token}")
else:
    # 也可能token在Cookie里,名为`csrftoken`
    csrf_token = session.cookies.get('csrftoken')
    print(f"从Cookie获取CSRF Token: {csrf_token}")

# 3. 将token加入登录数据
login_data['csrfmiddlewaretoken'] = csrf_token

# 4. 再次发送登录请求(注意,session已经持有初始Cookie)
login_response = session.post(login_url, data=login_data)

注意这个细节:如果 CSRF token 来自 Cookie,有些服务器要求你在 POST 请求的 headers 里额外添加 X-CSRFToken: token_value。具体规则要看网站的实现,需要抓包观察浏览器是怎么做的。

第三步:验证登录成功与访问受保护页面

登录请求成功(返回200)并不代表鉴权成功。更可靠的验证方法是:

  1. 检查响应内容是否包含登录成功的标志(如跳转、特定文字)。
  2. 或者,直接用这个 session 去访问一个登录后才能访问的页面(如用户主页、仪表盘),看是否能拿到正确数据。
# 方法1:检查登录响应(例如,成功后会跳转到/dashboard)
if login_response.status_code == 200 and '/dashboard' in login_response.url:
    print("登录成功,已跳转至仪表盘。")
else:
    print("登录可能失败,检查账号密码或Token。")
    # 打印响应内容有助于调试
    # print(login_response.text[:500])

# 方法2:直接访问受保护页面进行验证
dashboard_url = "https://example.com/dashboard"
dashboard_response = session.get(dashboard_url)

if dashboard_response.status_code == 200:
    # 进一步检查页面内容是否包含登录后的元素,而不是登录表单
    if "欢迎回来" in dashboard_response.text or "仪表盘" in dashboard_response.text:
        print("成功访问受保护页面!登录状态有效。")
        # 这里可以开始你的数据抓取逻辑了
        # ...
    else:
        print("访问了页面,但内容不对,可能登录状态无效。")
else:
    print(f"访问受保护页面失败,状态码: {dashboard_response.status_code}")

第四步:持久化与复用 Cookie(应对长时间运行)

如果你的脚本需要长时间运行,或者需要避免频繁登录,可以把登录后的 Cookie 保存到文件,下次启动时直接加载。

requests.Session()cookies 属性是一个 RequestsCookieJar 对象,它可以很方便地和 Python 的 http.cookiejar 模块配合,实现保存和加载。

import http.cookiejar
import os

COOKIE_FILE = 'session_cookies.txt'

def save_cookies(session):
    """将当前会话的Cookie保存到文件"""
    # 创建一个LWPCookieJar,它可以保存为Mozilla格式的cookie文件
    jar = http.cookiejar.LWPCookieJar(COOKIE_FILE)
    # 将session中的cookies导入到这个jar中
    for cookie in session.cookies:
        jar.set_cookie(cookie)
    jar.save(ignore_discard=True, ignore_expires=True) # 忽略过期和丢弃的cookie
    print(f"Cookie已保存至 {COOKIE_FILE}")

def load_cookies(session):
    """从文件加载Cookie到会话中"""
    if os.path.exists(COOKIE_FILE):
        # 清空现有cookie,加载文件中的
        session.cookies.clear()
        jar = http.cookiejar.LWPCookieJar(COOKIE_FILE)
        jar.load(ignore_discard=True, ignore_expires=True)
        session.cookies.update(jar)
        print(f"已从 {COOKIE_FILE} 加载Cookie")
        return True
    return False

# 使用示例:脚本启动时尝试加载Cookie
if not load_cookies(session):
    # 如果加载失败或文件不存在,则执行登录流程
    print("未找到有效Cookie,执行登录...")
    # ... (这里是完整的登录代码)
    # 登录成功后保存Cookie
    save_cookies(session)
else:
    # 如果加载成功,验证Cookie是否还有效
    test_response = session.get(dashboard_url)
    if test_response.status_code != 200 or "登录" in test_response.text:
        print("Cookie已失效,重新登录...")
        # 重新登录并覆盖旧的Cookie文件
        # ... (执行登录)
        save_cookies(session)

这里有个大坑ignore_expires=True 参数意味着我们会加载和使用过期的 Cookie。在实际使用时,你应该添加更完善的逻辑来判断 Cookie 是否真的过期失效(比如通过访问一个测试页面),而不是盲目使用。

完整代码示例

下面是一个整合了上述所有步骤的、相对完整的示例脚本。你需要将 your_username, your_password 以及相关的 URL 替换成你的目标网站信息。

import requests
import re
import http.cookiejar
import os
import time

# 配置信息
LOGIN_PAGE_URL = "https://example.com/login"  # 登录页地址
LOGIN_POST_URL = "https://example.com/login"  # 登录表单提交地址(可能和登录页相同)
DASHBOARD_URL = "https://example.com/dashboard"  # 登录后才能访问的页面
USERNAME = 'your_username'
PASSWORD = 'your_password'
COOKIE_FILE = 'my_session_cookies.txt'

def create_session():
    """创建一个配置好的requests Session"""
    session = requests.Session()
    # 设置一个合理的User-Agent,模拟浏览器
    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'
    })
    return session

def load_cookies(session):
    """尝试从文件加载Cookie"""
    if os.path.exists(COOKIE_FILE):
        try:
            jar = http.cookiejar.LWPCookieJar(COOKIE_FILE)
            jar.load(ignore_discard=True, ignore_expires=True)
            session.cookies.update(jar)
            print(f"[+] 从 {COOKIE_FILE} 加载了Cookie")
            return True
        except Exception as e:
            print(f"[-] 加载Cookie文件失败: {e}")
    return False

def save_cookies(session):
    """保存Cookie到文件"""
    try:
        jar = http.cookiejar.LWPCookieJar(COOKIE_FILE)
        for cookie in session.cookies:
            jar.set_cookie(cookie)
        jar.save(ignore_discard=True, ignore_expires=True)
        print(f"[+] Cookie已保存至 {COOKIE_FILE}")
    except Exception as e:
        print(f"[-] 保存Cookie失败: {e}")

def login(session):
    """执行登录流程"""
    print("[*] 开始登录流程...")
    
    # 1. 获取登录页面,捕获初始Cookie和CSRF Token
    print("[*] 获取登录页面...")
    resp_get = session.get(LOGIN_PAGE_URL)
    # 简单通过正则查找CSRF token,生产环境建议用BeautifulSoup
    csrf_token = None
    token_match = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', resp_get.text)
    if token_match:
        csrf_token = token_match.group(1)
        print(f"[*] 从HTML找到CSRF token: {csrf_token[:20]}...")
    else:
        csrf_token = session.cookies.get('csrftoken')
        if csrf_token:
            print(f"[*] 从Cookie找到CSRF token: {csrf_token}")
        else:
            print("[!] 警告:未找到CSRF token,登录可能会失败")
    
    # 2. 准备登录数据
    login_data = {
        'username': USERNAME,
        'password': PASSWORD,
    }
    if csrf_token:
        login_data['csrfmiddlewaretoken'] = csrf_token
        # 如果token来自cookie,有时还需要加这个头
        # session.headers.update({'X-CSRFToken': csrf_token})
    
    # 3. 提交登录
    print("[*] 提交登录表单...")
    resp_post = session.post(LOGIN_POST_URL, data=login_data)
    
    # 4. 简单验证登录是否成功
    if resp_post.status_code == 200:
        # 检查是否跳转到了非登录页,或者页面内容包含登录成功关键词
        if DASHBOARD_URL in resp_post.url or "logout" in resp_post.text.lower():
            print("[+] 登录成功!")
            return True
        else:
            print("[-] 登录请求成功,但可能未正确跳转(密码错误?)。")
            # 打印部分响应内容辅助调试
            print(f"响应URL: {resp_post.url}")
            print(f"响应内容片段: {resp_post.text[:300]}")
    else:
        print(f"[-] 登录请求失败,状态码: {resp_post.status_code}")
    return False

def test_session(session):
    """测试当前会话是否有权访问受保护页面"""
    print("[*] 测试会话有效性...")
    try:
        resp = session.get(DASHBOARD_URL, timeout=10)
        if resp.status_code == 200 and "登录" not in resp.text:
            print("[+] 会话有效,可以访问受保护页面。")
            return True
        else:
            print("[-] 会话可能已失效。")
            return False
    except Exception as e:
        print(f"[-] 测试请求异常: {e}")
        return False

def main():
    """主函数"""
    # 创建会话
    session = create_session()
    
    # 尝试加载已有Cookie
    cookie_loaded = load_cookies(session)
    
    need_login = True
    if cookie_loaded:
        # 如果加载了Cookie,测试一下是否还有效
        if test_session(session):
            need_login = False
        else:
            print("[*] 已加载的Cookie失效,需要重新登录。")
    
    # 如果需要登录,则执行登录流程
    if need_login:
        if login(session):
            # 登录成功后,再次测试确认
            if test_session(session):
                # 保存新的Cookie
                save_cookies(session)
            else:
                print("[!] 登录成功但无法访问受保护页面,请检查代码逻辑。")
                return
        else:
            print("[!] 登录失败,请检查账号密码和网络。")
            return
    
    # --- 从这里开始,session已经具备登录状态,可以执行你的业务逻辑 ---
    print("\n[+] 登录状态就绪,开始执行数据采集任务...")
    # 示例:访问某个报表API或页面
    # report_url = "https://example.com/api/report"
    # report_resp = session.get(report_url)
    # ... 处理报表数据
    
    # 最后关闭会话(虽然不强制,但是个好习惯)
    session.close()
    print("[*] 任务完成。")

if __name__ == '__main__':
    main()

踩坑记录

  1. 坑一:手动拼接 Cookie 字符串,漏掉了关键 Cookie。

    • 现象:登录成功,但访问其他页面返回登录界面。
    • 原因:服务器通过 Set-Cookie 设置了多个 Cookie(如 sessionid, csrftoken),我只提取了第一个,或者手动拼接时格式错误。
    • 解决:放弃手动管理,改用 requests.Session(),让它自动处理所有 Cookie 的存储和发送。
  2. 坑二:忽略了 CSRF 保护机制。

    • 现象:登录 POST 请求返回 403 状态码,或者返回成功但实际没登录。
    • 原因:网站有 CSRF 保护,登录表单需要携带一个动态的 token,这个 token 可能在第一次 GET 登录页的响应里,也可能在 Cookie 里。
    • 解决:先 GET 一次登录页,用正则或解析库(如 BeautifulSoup)从 HTML 表单中提取 csrfmiddlewaretoken,或者从初始响应的 Cookie 中获取 csrftoken,并将其加入到 POST 数据或请求头中。
  3. 坑三:每次请求都新建 Session,状态无法保持。

    • 现象:代码逻辑看起来没错,但每个请求都是独立的,登录状态无法传递。
    • 原因:错误地使用了 requests.get()requests.post() 这种顶级函数,它们每次都会创建新的会话。正确的做法是创建一个 session = requests.Session() 对象,然后一直使用 session.get()session.post()
    • 解决:全局使用同一个 Session 实例。
  4. 坑四:Cookie 文件加载后未验证有效性,直接使用。

    • 现象:昨天还能跑的脚本,今天直接报错或抓不到数据。
    • 原因:Cookie 是有生命周期的(过期时间)。我直接将保存的 Cookie 加载进来就用,可能其中的登录会话早已过期。
    • 解决:在加载 Cookie 后,增加一个验证步骤,例如访问一个简单的、需要登录的测试接口或页面,根据响应判断当前 Cookie 是否依然有效,无效则触发重新登录流程。

小结

搞定 requests 的 Cookie 登录鉴权,核心就两点:一是用对 Session 对象实现状态保持,二是摸清目标网站的鉴权流程(尤其是 CSRF 这类安全机制)。把这两个点吃透,大部分需要登录的网站自动化访问就难不倒你了。下一步可以研究如何应对更复杂的验证码、动态 JS 加载的 token,或者使用像 selenium 这样的工具来模拟更真实的浏览器行为。