如何编写一个 GitHub 二步验证客户端?(附仓库地址及apk下载链接)
一、原理介绍
GitHub 二步验证使用 TOTP(Time-based One-Time Password)算法,基于 RFC 6238 标准。
简单来说:
- 开启二步验证时,GitHub 生成一个密钥,通过二维码展示
- 用户用客户端扫描二维码,保存密钥
- 登录时,客户端用密钥 + 当前时间生成 6 位验证码
- 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[用户输入验证]
三、项目结构
bash
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 # 验证码卡片组件
四、依赖配置
yaml
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 账户的信息:
dart
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 的二维码内容格式:
ruby
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
代码实现:
dart
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 位验证码]
代码实现:
dart
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 包的实现有问题:
dart
// 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 安全存储
密钥必须安全存储,不能明文保存:
dart
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();
}
}
存储方式:
| 平台 | 存储方式 |
|---|---|
| iOS | Keychain |
| Android | EncryptedSharedPreferences |
| macOS | Keychain |
| Windows | Credential Manager |
5.6 状态管理与 UI 刷新
验证码每 30 秒刷新一次,需要定时更新 UI:
dart
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/...
七、构建方法
bash
# 克隆仓库
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 客户端的关键点:
- 理解 TOTP 算法 - 基于 HMAC-SHA1,时间窗口 30 秒
- 正确解析二维码 -
otpauth://协议格式 - Base32 解码密钥 -
otp包必须设置isGoogle: true - 安全存储 - 使用平台级加密存储
- 定时刷新 - 验证码每 30 秒更新
希望这篇文章对你有帮助,欢迎 Star!