Flutter三方库适配OpenHarmony【flutter_web_auth】— 安全增强与生产环境最佳实践
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.netflutter_web_auth 本身只是一个"浏览器桥梁",安全不安全取决于你怎么用它。裸用 OAuth2 授权码模式有 Scheme 劫持风险,加上PKCE就安全了;不验证State参数有 CSRF 风险,验证了就没事。这篇把生产环境中必须做的安全措施都讲一遍。PKCE:防止 auth
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
flutter_web_auth 本身只是一个"浏览器桥梁",安全不安全取决于你怎么用它。裸用 OAuth2 授权码模式有 Scheme 劫持风险,加上 PKCE 就安全了;不验证 State 参数有 CSRF 风险,验证了就没事。这篇把生产环境中必须做的安全措施都讲一遍。
一、PKCE(Proof Key for Code Exchange)
1.1 为什么需要 PKCE
没有 PKCE 的风险:
1. 恶意 App 注册了相同的 callbackUrlScheme
2. 用户完成认证后,authorization code 被恶意 App 截获
3. 恶意 App 用 code 换取 access_token
4. 用户账号被盗
1.2 PKCE 的工作原理
1. App 生成随机的 code_verifier(43-128 字符)
2. App 计算 code_challenge = SHA256(code_verifier) 的 Base64URL 编码
3. App 把 code_challenge 发给授权服务器
4. 授权服务器返回 authorization code
5. App 用 code + code_verifier 换取 token
6. 授权服务器验证 SHA256(code_verifier) == code_challenge
7. 验证通过,返回 token
即使恶意 App 截获了 code,没有 code_verifier 也无法换取 token。
1.3 Dart 实现
import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';
class PkceHelper {
static String generateCodeVerifier() {
final random = Random.secure();
final bytes = List<int>.generate(32, (_) => random.nextInt(256));
return base64Url.encode(bytes).replaceAll('=', '');
}
static String generateCodeChallenge(String codeVerifier) {
final bytes = utf8.encode(codeVerifier);
final digest = sha256.convert(bytes);
return base64Url.encode(digest.bytes).replaceAll('=', '');
}
}
// 使用
final codeVerifier = PkceHelper.generateCodeVerifier();
final codeChallenge = PkceHelper.generateCodeChallenge(codeVerifier);
final authUrl = Uri.https('auth.example.com', '/authorize', {
'response_type': 'code',
'client_id': clientId,
'redirect_uri': '$callbackUrlScheme://callback',
'code_challenge': codeChallenge,
'code_challenge_method': 'S256',
'state': state,
});
final result = await FlutterWebAuth.authenticate(
url: authUrl.toString(),
callbackUrlScheme: callbackUrlScheme,
);
// 用 code + code_verifier 换 token
final code = Uri.parse(result).queryParameters['code'];
final tokenResponse = await http.post(tokenEndpoint, body: {
'grant_type': 'authorization_code',
'code': code,
'code_verifier': codeVerifier, // 关键:发送 code_verifier
'client_id': clientId,
'redirect_uri': '$callbackUrlScheme://callback',
});
1.4 PKCE 支持情况
| OAuth 提供商 | PKCE 支持 |
|---|---|
| ✅ | |
| GitHub | ❌(但在推进中) |
| Microsoft | ✅ |
| Auth0 | ✅ |
| Okta | ✅ |
📌 即使 OAuth 提供商不强制要求 PKCE,也建议使用。PKCE 是 OAuth 2.1 的强制要求,未来所有提供商都会支持。
二、State 参数防 CSRF 攻击
2.1 CSRF 攻击场景
1. 攻击者用自己的账号完成 OAuth 认证,获取 code
2. 攻击者构造链接:myapp://callback?code=attacker_code
3. 诱导受害者点击这个链接
4. 受害者的 App 用 attacker_code 换取 token
5. 受害者的 App 登录了攻击者的账号
6. 受害者在"自己的"App 中输入的数据,实际上存到了攻击者的账号里
2.2 State 参数的防御
// 生成随机 state
final state = _generateRandomString(32);
// 发送 state 到授权服务器
final authUrl = Uri.https('auth.example.com', '/authorize', {
// ...
'state': state,
});
// 验证返回的 state
final result = await FlutterWebAuth.authenticate(...);
final returnedState = Uri.parse(result).queryParameters['state'];
if (returnedState != state) {
throw SecurityException('State mismatch! Possible CSRF attack.');
}
2.3 State 的生成
String _generateRandomString(int length) {
final random = Random.secure();
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
return List.generate(length, (_) => chars[random.nextInt(chars.length)]).join();
}

三、HTTPS 强制与证书校验
3.1 为什么必须用 HTTPS
HTTP 的风险:
1. 中间人可以看到 authorization code
2. 中间人可以篡改重定向 URL
3. 中间人可以注入恶意代码
3.2 在 OpenHarmony 上的配置
// module.json5
{
"module": {
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" }
]
}
}
OpenHarmony 默认允许 HTTP 请求。生产环境中应该在应用层面强制使用 HTTPS:
// Dart 层强制 HTTPS
void authenticate(String authUrl) {
final uri = Uri.parse(authUrl);
if (uri.scheme != 'https') {
throw ArgumentError('Authentication URL must use HTTPS');
}
// ...
}
3.3 证书固定(Certificate Pinning)
| 级别 | 做法 | 安全性 |
|---|---|---|
| 基础 | 使用 HTTPS | 中 |
| 增强 | 证书固定 | 高 |
| 最高 | 证书固定 + 公钥固定 | 最高 |
💡 对于 OAuth 认证 URL,通常不需要证书固定——因为认证页面是在系统浏览器中打开的,浏览器会自己处理证书验证。证书固定主要用于 App 直接发起的 HTTP 请求(如换取 token)。
四、Token 安全存储
4.1 不安全的存储方式
// ❌ 不安全:SharedPreferences 明文存储
final prefs = await SharedPreferences.getInstance();
prefs.setString('access_token', token);
// ❌ 不安全:写入文件
File('token.txt').writeAsStringSync(token);
4.2 安全的存储方式
// ✅ 安全:flutter_secure_storage
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
final storage = FlutterSecureStorage();
// 存储
await storage.write(key: 'access_token', value: accessToken);
await storage.write(key: 'refresh_token', value: refreshToken);
// 读取
final token = await storage.read(key: 'access_token');
// 删除(登出时)
await storage.delete(key: 'access_token');
await storage.delete(key: 'refresh_token');
4.3 各平台的安全存储底层
| 平台 | flutter_secure_storage 底层 | 安全级别 |
|---|---|---|
| Android | EncryptedSharedPreferences | 高 |
| iOS | Keychain | 高 |
| OpenHarmony | 需要适配(可用 Preferences + 加密) | 中 |
4.4 Token 生命周期管理
class TokenManager {
final storage = FlutterSecureStorage();
Future<void> saveTokens(String accessToken, String refreshToken, int expiresIn) async {
await storage.write(key: 'access_token', value: accessToken);
await storage.write(key: 'refresh_token', value: refreshToken);
final expiry = DateTime.now().add(Duration(seconds: expiresIn));
await storage.write(key: 'token_expiry', value: expiry.toIso8601String());
}
Future<String?> getValidToken() async {
final expiry = await storage.read(key: 'token_expiry');
if (expiry != null && DateTime.parse(expiry).isAfter(DateTime.now())) {
return await storage.read(key: 'access_token');
}
// Token 过期,尝试刷新
return await _refreshToken();
}
Future<void> clearTokens() async {
await storage.deleteAll();
}
}
五、callbackUrlScheme 的命名规范
5.1 推荐的命名格式
| 格式 | 示例 | 冲突风险 |
|---|---|---|
| 反向域名 | com.example.myapp |
低 |
| 反向域名 + 后缀 | com.example.myapp.oauth |
最低 |
| 简单名称 | myapp |
高 |
| 随机字符串 | a1b2c3d4 |
低但不可读 |
5.2 防冲突策略
// ✅ 推荐:使用反向域名
const callbackUrlScheme = 'com.example.myapp';
// ✅ 更好:加上用途后缀
const callbackUrlScheme = 'com.example.myapp.oauth';
// ❌ 不推荐:简单名称
const callbackUrlScheme = 'myapp'; // 容易与其他 App 冲突
5.3 多 OAuth 提供商的 Scheme 管理
class AuthSchemes {
static const google = 'com.example.myapp.google';
static const github = 'com.example.myapp.github';
static const microsoft = 'com.example.myapp.microsoft';
}
每个 OAuth 提供商使用不同的 Scheme,避免回调混淆。
📌 每个 Scheme 都需要在 module.json5 的 skills 中声明:
"uris": [
{ "scheme": "com.example.myapp.google" },
{ "scheme": "com.example.myapp.github" },
{ "scheme": "com.example.myapp.microsoft" }
]
六、生产环境安全检查清单
6.1 必须做的
- 使用 PKCE(code_challenge + code_verifier)
- 验证 State 参数
- 认证 URL 使用 HTTPS
- Token 使用安全存储(不是 SharedPreferences)
- callbackUrlScheme 使用反向域名格式
- client_secret 不放在客户端代码中
6.2 建议做的
- Token 过期自动刷新
- 登出时清除所有 Token
- 认证失败时清除旧 Token
- 记录安全审计日志
- 设置合理的 Token 有效期
6.3 不要做的
| 不要做 | 原因 |
|---|---|
| client_secret 放在客户端 | 可以被反编译提取 |
| Token 明文存储 | 可以被其他 App 读取 |
| 使用 HTTP | 中间人攻击 |
| 不验证 State | CSRF 攻击 |
| 使用简单 Scheme | Scheme 劫持 |
七、完整的安全认证流程
7.1 代码模板
class SecureAuthService {
final storage = FlutterSecureStorage();
Future<String?> authenticate() async {
// 1. 生成 PKCE
final codeVerifier = PkceHelper.generateCodeVerifier();
final codeChallenge = PkceHelper.generateCodeChallenge(codeVerifier);
// 2. 生成 State
final state = _generateRandomString(32);
// 3. 构造安全的授权 URL
final authUrl = Uri.https('auth.example.com', '/authorize', {
'response_type': 'code',
'client_id': clientId,
'redirect_uri': '$callbackUrlScheme://callback',
'scope': 'openid profile email',
'state': state,
'code_challenge': codeChallenge,
'code_challenge_method': 'S256',
});
// 4. 打开浏览器认证
final result = await FlutterWebAuth.authenticate(
url: authUrl.toString(),
callbackUrlScheme: callbackUrlScheme,
);
// 5. 验证 State
final params = Uri.parse(result).queryParameters;
if (params['state'] != state) {
throw SecurityException('State mismatch');
}
// 6. 用 code + code_verifier 换 Token(通过后端)
final token = await _exchangeCode(params['code']!, codeVerifier);
// 7. 安全存储 Token
await storage.write(key: 'access_token', value: token);
return token;
}
}
总结
本文讲解了 flutter_web_auth 的安全增强措施:
- PKCE:防止 authorization code 被截获后滥用
- State 参数:防止 CSRF 攻击
- HTTPS 强制:防止中间人攻击
- 安全存储:flutter_secure_storage 加密存储 Token
- Scheme 命名:反向域名格式降低冲突风险
下一篇是本系列最后一篇——总结回顾与 Web 认证技术展望。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
更多推荐



所有评论(0)