前言

欢迎加入开源鸿蒙跨平台社区: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 支持
Google
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 的安全增强措施:

  1. PKCE:防止 authorization code 被截获后滥用
  2. State 参数:防止 CSRF 攻击
  3. HTTPS 强制:防止中间人攻击
  4. 安全存储:flutter_secure_storage 加密存储 Token
  5. Scheme 命名:反向域名格式降低冲突风险

下一篇是本系列最后一篇——总结回顾与 Web 认证技术展望。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐