跳转至

双因素认证

概述

双因素认证(2FA,Two-Factor Authentication)是 Instagram 提供的账号安全功能。 开启后,仅凭用户名和密码无法完成登录,还需要额外提供一个只有账号持有者才能获取的一次性验证码。

Instagram 支持两种 2FA 方式:

方式 说明 igapi 支持
TOTP(基于时间的一次性密码) 通过 Google Authenticator 等 App 生成的 6 位验证码,每 30 秒刷新一次 全自动(传入密钥)或手动
SMS(短信验证码) Instagram 向绑定手机发送的 6 位验证码 仅支持手动输入

重要:双因素认证仅在 WebClient 中支持完整流程。Client(Android)遇到 2FA 要求会抛出 TwoFactorRequired 异常,但不提供 verify_two_factor() 方法。如果账号开启了 2FA,请使用 WebClient 登录。


快速开始

以下示例展示两种最常见的 2FA 使用场景,后续章节有更详细的说明。

场景一:拥有 TOTP 密钥(推荐)

import asyncio
import igapi

async def main():
    web = igapi.WebClient()
    await web.login(
        "your_username",
        "your_password",
        two_fa_secret="JBSWY3DPEHPK3PXP",  # Base32 编码的 TOTP 密钥
    )
    print("登录成功,session 已就绪")

asyncio.run(main())

场景二:手动输入验证码

import asyncio
import igapi

async def main():
    web = igapi.WebClient()

    try:
        await web.login("your_username", "your_password")
    except igapi.TwoFactorRequired:
        code = input("请输入 6 位验证码: ").strip()
        await web.verify_two_factor(code)

    print("登录成功")

asyncio.run(main())

自动 2FA(推荐)

如果你在设置 2FA 时保存了 Base32 格式的 TOTP 密钥,可以将其传入 login()two_fa_secret 参数。 库会在内部自动生成当前时间窗口的验证码并完成整个 2FA 流程,对调用方完全透明。

import asyncio
import igapi
import os

async def main():
    web = igapi.WebClient()

    # 全自动登录:库内部生成 TOTP 验证码,无需任何手动操作
    await web.login(
        os.environ["IG_USERNAME"],
        os.environ["IG_PASSWORD"],
        two_fa_secret=os.environ["IG_TOTP_SECRET"],  # Base32 编码的 TOTP 密钥
    )

    print(f"登录成功: {web.export_account().username}")
    session_str = web.export_account_string()

asyncio.run(main())

自动 2FA 的内部流程

  1. 调用 login(username, password, two_fa_secret=...),服务端返回需要 2FA 的信号
  2. 库使用传入的 two_fa_secret 生成当前时间窗口的 6 位 TOTP 验证码(SHA1 算法,30 秒周期)
  3. 自动调用 2FA 验证端点,提交验证码
  4. 验证通过后,login() 正常返回,整个过程对调用方透明

完整示例(含错误处理)

import asyncio
import igapi
import os

async def auto_login() -> igapi.WebClient:
    """使用 TOTP 密钥自动完成 2FA 登录"""
    web = igapi.WebClient()

    try:
        await web.login(
            os.environ["IG_USERNAME"],
            os.environ["IG_PASSWORD"],
            two_fa_secret=os.environ["IG_TOTP_SECRET"],
        )
        return web

    except ValueError as e:
        raise RuntimeError(f"凭证或 TOTP 密钥错误: {e}") from e
    except igapi.ChallengeRequired as e:
        raise RuntimeError(
            f"需要完成安全挑战,请访问: {e.challenge_info.get('checkpoint_url')}"
        ) from e
    except ConnectionError as e:
        raise RuntimeError(f"网络连接失败,请检查代理配置: {e}") from e

async def main():
    web = await auto_login()

asyncio.run(main())

手动 2FA

当未提供 two_fa_secret,或账号使用 SMS 验证码时,login() 会抛出 TwoFactorRequired 异常。 此时需要捕获该异常,引导用户输入验证码,再调用 verify_two_factor() 完成登录。

import asyncio
import igapi

async def main():
    web = igapi.WebClient()

    try:
        await web.login("your_username", "your_password")
        print("登录成功(无 2FA)")

    except igapi.TwoFactorRequired as e:
        # 从异常属性中获取 2FA 信息
        info = e.two_factor_info
        print(f"账号 {info.get('username')} 需要双因素认证")

        # 判断 2FA 方式并提示用户
        if info.get("totp_two_factor_on"):
            print("请打开 Authenticator App 查看验证码")
        elif info.get("sms_two_factor_on"):
            print("请查收手机短信验证码")
        else:
            print("请输入验证码")

        code = input("请输入 6 位验证码: ").strip()

        try:
            await web.verify_two_factor(code)
            print("验证成功,已登录")
        except ValueError as ve:
            print(f"验证码错误: {ve}")

asyncio.run(main())

verify_two_factor() 的工作原理

login() 抛出 TwoFactorRequired 时,客户端会在内部暂存以下信息:

  • 用户名
  • 密码
  • 服务端返回的 two_factor_identifier(标识本次 2FA 会话)

调用 verify_two_factor(code) 时,这些信息会被自动读取并组合成最终的验证请求, 无需用户重复传入用户名和密码。验证成功后,内部暂存的登录上下文会被清除。

交互式登录(支持重试)

适用于命令行工具或交互式脚本,最多允许重试 3 次:

import asyncio
import igapi

async def interactive_login(username: str, password: str) -> igapi.WebClient:
    """交互式登录,支持手动输入 2FA 验证码,最多重试 3 次"""
    web = igapi.WebClient()

    try:
        await web.login(username, password)
        print("登录成功")
        return web

    except igapi.TwoFactorRequired as e:
        info = e.two_factor_info
        method = "TOTP" if info.get("totp_two_factor_on") else "SMS"
        print(f"账号已开启 2FA({method}),请输入验证码")

        for attempt in range(1, 4):
            code = input(f"验证码(第 {attempt}/3 次): ").strip()
            try:
                await web.verify_two_factor(code)
                print("2FA 验证成功")
                return web
            except ValueError:
                if attempt < 3:
                    print("验证码错误,请重试")
                else:
                    raise RuntimeError("验证码连续错误 3 次,登录失败")

    except igapi.ChallengeRequired as e:
        url = e.challenge_info.get("checkpoint_url", "未知")
        raise RuntimeError(f"需要完成安全挑战,请访问: {url}")

    except ValueError as e:
        raise RuntimeError(f"用户名或密码错误: {e}")

async def main():
    web = await interactive_login("your_username", "your_password")

asyncio.run(main())

Android 客户端的 2FA 限制

igapi.Client(Android 客户端)在登录流程中遇到 2FA 要求时,会直接抛出 TwoFactorRequired 异常,但 不提供 verify_two_factor() 方法,无法在同一客户端上完成 2FA 验证流程。

import asyncio
import igapi

async def main():
    client = igapi.Client()

    try:
        await client.login("your_username", "your_password")

    except igapi.TwoFactorRequired as e:
        # Client 不支持完成 2FA,只能获取信息后退出或切换到 WebClient
        info = e.two_factor_info
        print(f"账号 {info.get('username')} 开启了 2FA,Android 客户端无法完成验证")
        print("请改用 igapi.WebClient 完成登录")
        raise

asyncio.run(main())

推荐处理方案

对于开启了 2FA 的账号,有以下两种推荐方案:

方案一:直接使用 WebClient(推荐)

WebClient 功能完整,支持 2FA,且支持与 AndroidClient 相同的大部分 API。

import asyncio
import igapi

async def main():
    web = igapi.WebClient()
    await web.login(
        "your_username",
        "your_password",
        two_fa_secret="BASE32_TOTP_SECRET",
    )

asyncio.run(main())

方案二:用 WebClient 登录后导出 session,再用 AndroidClient 恢复

适用于必须使用 AndroidClient 的场景,但 session 格式不兼容,此方案不可行。 Android 和 Web 的 session 格式不同,无法互相恢复。两者必须分别维护各自的 session。

对比项 Client(Android) WebClient(Web)
2FA 支持 仅抛出异常,无法完成 完整支持(自动/手动)
verify_two_factor() 不存在 存在
推荐用于开启了 2FA 的账号

异常详解

TwoFactorRequired

igapi.TwoFactorRequired 是 igapi 定义的自定义异常类,继承自 Exception,在登录需要 2FA 验证时抛出。

属性:

属性 类型 说明
two_factor_info dict 服务端返回的 2FA 信息字典

two_factor_info 字典结构:

字段 类型 说明
two_factor_identifier str 本次 2FA 会话的唯一标识符
username str 正在登录的用户名
sms_two_factor_on bool \| None 账号是否开启了 SMS 2FA
totp_two_factor_on bool \| None 账号是否开启了 TOTP 2FA
phone_verification_settings dict \| None 手机验证的详细设置(可选)

使用示例:

async def main():
    try:
        await web.login("your_username", "your_password")
    except igapi.TwoFactorRequired as e:
        info = e.two_factor_info

        identifier = info["two_factor_identifier"]
        username = info["username"]
        has_sms = info.get("sms_two_factor_on", False)
        has_totp = info.get("totp_two_factor_on", False)

        print(f"用户 {username} 需要 2FA")
        print(f"  短信验证: {has_sms}")
        print(f"  TOTP 验证: {has_totp}")
        print(f"  会话标识: {identifier}")

ChallengeRequired

igapi.ChallengeRequired 在登录触发 Instagram 安全挑战时抛出(如邮箱验证、手机验证等),与 2FA 是不同的机制。

属性:

属性 类型 说明
challenge_info dict 服务端返回的挑战信息字典,通常包含 checkpoint_url

处理示例:

async def main():
    try:
        await web.login("your_username", "your_password")
    except igapi.ChallengeRequired as e:
        url = e.challenge_info.get("checkpoint_url", "未知")
        print(f"需要完成安全挑战,请在浏览器中访问: {url}")
        # 完成挑战后,通常需要重新创建客户端并登录

2FA 决策流程图

下图展示了 login() 调用后,igapi 内部的 2FA 处理决策路径:

flowchart TD
    A([调用 web.login]) --> B{服务端响应}

    B -->|登录成功| C([登录完成,正常返回])
    B -->|密码错误| D([抛出 ValueError])
    B -->|安全挑战| E([抛出 ChallengeRequired])
    B -->|需要 2FA| F{是否传入 two_fa_secret}

    F -->|是| G[自动生成 TOTP 验证码]
    G --> H[调用 2FA 验证端点]
    H -->|验证通过| C
    H -->|验证码错误| I([抛出 ValueError])

    F -->|否| J[暂存登录上下文]
    J --> K([抛出 TwoFactorRequired])
    K --> L{调用方响应}

    L -->|调用 verify_two_factor| M[提交用户输入的验证码]
    M -->|验证通过| C
    M -->|验证码错误| N([抛出 ValueError])
    M -->|标识符过期| O([抛出 RuntimeError,需重新 login])

    L -->|未调用| P([登录流程中止])

常见问题

Q: 我的账号使用 SMS 验证码,能实现完全自动化登录吗?

A: 无法自动化,因为 SMS 验证码需要手机实时接收。如果需要自动化登录,建议在账号安全设置中将 2FA 方式改为 TOTP(验证器 App),然后保存设置时显示的 Base32 密钥,再使用 two_fa_secret 参数实现全自动登录。

Q: 怎么获取 TOTP 密钥(two_fa_secret)?

A: 在 Instagram 中进入「设置 → 账号安全 → 双因素认证 → 验证器 App」,设置时页面会显示或允许复制一个 Base32 格式的密钥字符串(通常是 16-32 个大写字母和数字组成的字符串)。这就是 two_fa_secret。请务必在首次设置时保存,之后将无法再次查看该密钥。

Q: verify_two_factor() 调用提示 RuntimeError: 请先调用 login(),怎么解决?

A: 这是因为调用 verify_two_factor() 之前,login() 要么没有被调用,要么没有抛出 TwoFactorRequired(意味着账号未开启 2FA 或登录已成功)。请确保调用链为:login() 抛出 TwoFactorRequired → 捕获异常 → 调用 verify_two_factor(code),且三个步骤在同一个 WebClient 实例上完成。

Q: 自动 2FA 失败,提示验证码错误,但手动输入同一 App 显示的验证码可以成功,为什么?

A: 自动 TOTP 依赖系统时钟准确性。如果运行 igapi 的机器时钟与标准时间偏差超过几秒,生成的验证码可能与服务端预期不符。请确保系统时间已通过 NTP 服务同步。

Q: TOTP 密钥(two_fa_secret)应该怎么安全存储?

A: TOTP 密钥的安全级别等同于账号密码,控制了该密钥就相当于控制了账号的 2FA。请通过环境变量或加密密钥库传入,绝对不要硬编码在源代码中或提交到版本控制系统。

Q: 验证码已过期(two_factor_identifier 失效),怎么办?

A: 服务端返回的 two_factor_identifier 通常数分钟内有效。如果用户长时间未输入验证码,verify_two_factor() 可能会失败。此时需要在同一个 WebClient 实例上重新调用 login(),获取新的 two_factor_identifier 后再提交验证码。