如何编写一个 GitHub 二步验证客户端?(附仓库地址及apk下载链接)

1 阅读4分钟

如何编写一个 GitHub 二步验证客户端?(附仓库地址及apk下载链接)

一、原理介绍

GitHub 二步验证使用 TOTP(Time-based One-Time Password)算法,基于 RFC 6238 标准。

简单来说:

  1. 开启二步验证时,GitHub 生成一个密钥,通过二维码展示
  2. 用户用客户端扫描二维码,保存密钥
  3. 登录时,客户端用密钥 + 当前时间生成 6 位验证码
  4. GitHub 用同样的算法验证

验证码每 30 秒刷新一次,时间窗口内的验证码是固定的。

二、整体流程

flowchart TD
    A[用户开启 GitHub 2FA] --> B[GitHub 生成二维码]
    B --> C[客户端扫描二维码]
    C --> D[解析 otpauth:// URL]
    D --> E[提取密钥 Secret]
    E --> F[安全存储到本地]

    G[用户登录 GitHub] --> H[要求输入验证码]
    H --> I[打开客户端]
    I --> J[从本地读取密钥]
    J --> K[计算当前时间窗口]
    K --> L[HMAC-SHA1 计算哈希]
    L --> M[动态截取生成 6 位码]
    M --> N[显示验证码]
    N --> O[用户输入验证]

三、项目结构

lib/
├── main.dart                 # 应用入口
├── models/
│   └── account.dart          # 账户数据模型
├── services/
│   ├── totp_service.dart     # TOTP 核心算法
│   ├── storage_service.dart  # 安全存储
│   └── account_provider.dart # 状态管理
├── screens/
│   ├── home_page.dart        # 主页面(显示验证码)
│   ├── scan_qr_page.dart     # 扫描二维码页面
│   └── add_account_page.dart # 添加账户页面
└── widgets/
    └── code_card.dart        # 验证码卡片组件

四、依赖配置

dependencies:
  flutter:
    sdk: flutter

  # TOTP 算法实现
  otp: ^3.1.4

  # Base32 编解码
  base32: ^2.2.0

  # 二维码扫描
  mobile_scanner: ^6.0.2

  # 安全存储(iOS Keychain / Android 加密存储)
  flutter_secure_storage: ^9.2.2

  # 状态管理
  provider: ^6.1.2

  # 唯一 ID 生成
  uuid: ^4.5.1

五、核心实现

5.1 数据模型

首先定义账户模型,存储每个 2FA 账户的信息:

class Account {
  final String id;          // 唯一标识
  final String name;        // 账户名称,如 "GitHub"
  final String email;       // 用户标识
  final String secret;      // 密钥(Base32 编码)
  final int digits;         // 验证码位数,默认 6
  final int interval;       // 时间间隔,默认 30 秒
  final String algorithm;   // 算法:SHA1/SHA256/SHA512
  final String? issuer;     // 发行者

  Account({
    String? id,
    required this.name,
    required this.email,
    required this.secret,
    this.digits = 6,
    this.interval = 30,
    this.algorithm = 'SHA1',
    this.issuer,
  });
}

5.2 二维码扫描与解析

GitHub 的二维码内容格式:

otpauth://totp/GitHub:username?secret=JBSWY3DPEHPK3PXP&issuer=GitHub

解析流程:

flowchart LR
    A[扫描二维码] --> B[获取 URL 字符串]
    B --> C{是否 otpauth:// 协议?}
    C -->|否| D[返回错误]
    C -->|是| E[分离路径和参数]
    E --> F[解析账户名]
    E --> G[解析密钥 secret]
    E --> H[解析其他参数]
    F --> I[返回 OtpAuthData]
    G --> I
    H --> I

代码实现:

static OtpAuthData? parseOtpAuthUrl(String url) {
  // 检查协议
  if (!url.startsWith('otpauth://totp/')) {
    return null;
  }

  // 分离路径和查询参数
  final uriString = url.replaceFirst('otpauth://totp/', '');
  final parts = uriString.split('?');

  // 解析标签(issuer:email)
  final label = Uri.decodeComponent(parts[0]);
  String name, email, issuer;
  if (label.contains(':')) {
    final labelParts = label.split(':');
    issuer = labelParts[0];
    email = labelParts[1];
    name = issuer;
  } else {
    email = label;
    name = label;
  }

  // 解析查询参数
  final queryParams = Uri.splitQueryString(parts[1]);
  final secret = queryParams['secret'] ?? '';
  final digits = int.parse(queryParams['digits'] ?? '6');
  final interval = int.parse(queryParams['period'] ?? '30');
  final algorithm = queryParams['algorithm'] ?? 'SHA1';

  return OtpAuthData(
    name: name,
    email: email,
    secret: secret.toUpperCase(),
    digits: digits,
    interval: interval,
    algorithm: algorithm,
  );
}

5.3 TOTP 验证码生成

这是整个项目的核心,TOTP 算法流程:

flowchart TD
    A[获取当前时间戳] --> B[除以 30 得到时间窗口 T]
    B --> C[T 转换为 8 字节大端]
    C --> D[Base32 解码密钥]
    D --> E[HMAC-SHA1 计算]
    E --> F[得到 20 字节哈希]
    F --> G[取最后 1 字节的低 4 位作为 offset]
    G --> H[从 offset 开始取 4 字节]
    H --> I[去掉最高位符号位]
    I --> J[对 10^6 取模]
    J --> K[得到 6 位验证码]

代码实现:

static String generateCode({
  required String secret,
  int digits = 6,
  int interval = 30,
  String algorithm = 'SHA1',
}) {
  // 选择算法
  Algorithm algo;
  switch (algorithm.toUpperCase()) {
    case 'SHA256':
      algo = Algorithm.SHA256;
      break;
    case 'SHA512':
      algo = Algorithm.SHA512;
      break;
    default:
      algo = Algorithm.SHA1;
  }

  // 生成验证码
  final code = OTP.generateTOTPCode(
    secret,
    DateTime.now().millisecondsSinceEpoch,
    length: digits,
    interval: interval,
    algorithm: algo,
    isGoogle: true,  // ⚠️ 必须设置!
  );

  return code.toString().padLeft(digits, '0');
}

5.4 一个巨坑!

开发过程中遇到一个致命问题:验证码始终不对

排查后发现 otp 包的实现有问题:

// otp 包源码
var secretList = Uint8List.fromList(utf8.encode(secret));  // 默认行为
if (isGoogle) {
  secretList = base32.decode(secret.toUpperCase());  // 正确行为
}

问题:默认把密钥当 UTF-8 字符串编码,而不是 Base32 解码!

解决:必须设置 isGoogle: true,才会正确解码 Base32 密钥。

GitHub/Google Authenticator 的密钥都是 Base32 编码的,不设置这个参数,验证码永远错误。

5.5 安全存储

密钥必须安全存储,不能明文保存:

class StorageService {
  final FlutterSecureStorage _storage = FlutterSecureStorage(
    aOptions: AndroidOptions(encryptedSharedPreferences: true),
  );

  // 保存账户列表
  Future<void> saveAccounts(List<Account> accounts) async {
    final jsonList = accounts.map((a) => a.toJson()).toList();
    await _storage.write(
      key: 'accounts',
      value: jsonEncode(jsonList),
    );
  }

  // 加载账户列表
  Future<List<Account>> loadAccounts() async {
    final jsonString = await _storage.read(key: 'accounts');
    if (jsonString == null) return [];

    final jsonList = jsonDecode(jsonString) as List;
    return jsonList.map((json) => Account.fromJson(json)).toList();
  }
}

存储方式:

平台存储方式
iOSKeychain
AndroidEncryptedSharedPreferences
macOSKeychain
WindowsCredential Manager

5.6 状态管理与 UI 刷新

验证码每 30 秒刷新一次,需要定时更新 UI:

class HomePage extends StatefulWidget {
  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  Timer? _refreshTimer;

  @override
  void initState() {
    super.initState();
    // 每秒刷新一次
    _refreshTimer = Timer.periodic(Duration(seconds: 1), (_) {
      setState(() {});
    });
  }

  @override
  void dispose() {
    _refreshTimer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // 显示验证码和倒计时
  }
}

六、完整项目

仓库地址:github.com/FuShang114/…

下载地址:github.com/FuShang114/…

七、构建方法

# 克隆仓库
git clone https://github.com/FuShang114/Free2FA.git
cd Free2FA

# 安装依赖
flutter pub get

# 构建 APK
flutter build apk --release

APK 输出路径:build/app/outputs/flutter-apk/app-release.apk

八、总结

开发一个 TOTP 客户端的关键点:

  1. 理解 TOTP 算法 - 基于 HMAC-SHA1,时间窗口 30 秒
  2. 正确解析二维码 - otpauth:// 协议格式
  3. Base32 解码密钥 - otp 包必须设置 isGoogle: true
  4. 安全存储 - 使用平台级加密存储
  5. 定时刷新 - 验证码每 30 秒更新

希望这篇文章对你有帮助,欢迎 Star!