构建安全Flutter应用 - 6个实用技巧

视频

https://youtu.be/7pBJPmtCKX0

https://www.bilibili.com/video/BV1a4421Z75a/

前言

原文 https://ducafecat.com/blog/flutter-app-security-best-practices

随着越来越多的敏感用户数据在Flutter应用中流通,应用安全已成为首要关注点。本文为您总结6大关键Flutter应用安全最佳实践,帮助开发者筑牢应用安全防线,保护用户隐私。

Flutter应用开发,Flutter应用安全,Flutter开发安全最佳实践,Flutter用户数据保护,Flutter应用安全性

正文

保护你的秘钥

APP 常见秘钥有 对象存储 key,即时通讯签名key,谷歌三方key,firebase key 等等。

我们一般都写在代码的类似 config.dar 中,但是这是非常容易被逆向取出被盗用。

为了提高安全可以采用两种方式:

  • 读取系统环境变量

可以直接使用 Dart 标准库中的 dart:io 包来读取环境变量。

import 'dart:io';

String apiKey = Platform.environment['API_KEY'];
String dbUrl = Platform.environment['DB_URL'];

也可以使用 flutter_dotenv 包简化操作。

https://pub.dev/packages/flutter_dotenv

await dotenv.load(fileName: ".env");
dotenv.env['VAR_NAME'];
  • 程序中动态拉取配置key

http 拉取

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<Map<String, dynamic>> fetchConfig() async {
  final response = await http.get(Uri.parse('https://your-config-server.com/config'));
  if (response.statusCode == 200) {
    return jsonDecode(response.body);
  } else {
    throw Exception('Failed to fetch configuration');
  }
}

void main() async {
  final config = await fetchConfig();
  final apiKey = config['API_KEY'];
  // 使用 apiKey 进行其他操作
  runApp(MyApp());
}

配置服务器 , 如 Firebase Remote Config

import 'package:firebase_remote_config/firebase_remote_config.dart';

Future<void> fetchConfig() async {
  final remoteConfig = FirebaseRemoteConfig.instance;
  await remoteConfig.setConfigSettings(RemoteConfigSettings(
    fetchTimeout: const Duration(seconds: 10),
    minimumFetchInterval: const Duration(hours: 1),
  ));
  await remoteConfig.fetchAndActivate();
  final apiKey = remoteConfig.getString('API_KEY');
  // 使用 apiKey 进行其他操作
}

void main() async {
  await fetchConfig();
  runApp(MyApp());
}
  • 最小化你的配置

尽可能减少在应用程序中使用敏感的环境变量。

如果某些数据不是绝对必要的,最好不要将它们存储为环境变量。

用令牌保护你的数据

常见令牌技术有 OAuth2、JWT、安全秘钥,通常我们把令牌放在 HTTP Headers ,如 Authorization 或者 X-API-Key 这种。

令牌特点有 与访问者关联、设备关联、过期时间、生物认证、续签。

面是一个使用 Flutter 的 Dio 库和 JWT 进行身份验证的示例:

import 'package:dio/dio.dart';
import 'dart:convert';
import 'package:jwt_decoder/jwt_decoder.dart';

// 定义 JWT 令牌的键名
const String JWT_TOKEN_KEY = 'jwt_token';

// 创建 Dio 实例并设置拦截器
Dio dio = Dio()
  ..interceptors.add(InterceptorsWrapper(
    onRequest: (options, handler) async {
      // 检查是否有 JWT 令牌
      String? token = await _getJwtToken();
      if (token != null) {
        options.headers['Authorization'] = 'Bearer $token';
      }
      return handler.next(options);
    },
    onError: (DioError e, handler) async {
      // 处理 401 Unauthorized 错误
      if (e.response?.statusCode == 401) {
        // 尝试刷新 JWT 令牌
        if (await _refreshJwtToken()) {
          // 重试请求
          final options = e.requestOptions;
          final response = await dio.request(
            options.path,
            options: Options(
              method: options.method,
              headers: options.headers,
            ),
            data: options.data,
            queryParameters: options.queryParameters,
          );
          return handler.resolve(response);
        } else {
          // 令牌刷新失败,需要用户重新登录
          // 可以在这里添加相关的处理逻辑
        }
      }
      return handler.next(e);
    },
  ));

// 从本地存储中获取 JWT 令牌
Future<String?> _getJwtToken() async {
  // 从本地存储中获取 JWT 令牌
  final token = await _getTokenFromStorage();
  if (token != null && !JwtDecoder.isExpired(token)) {
    return token;
  }
  return null;
}

// 刷新 JWT 令牌
Future<bool> _refreshJwtToken() async {
  try {
    // 发送刷新令牌请求,获取新的 JWT 令牌
    final response = await dio.post('/refresh-token');
    final newToken = response.data['token'];
    // 将新的 JWT 令牌保存到本地存储
    await _saveTokenToStorage(newToken);
    return true;
  } catch (e) {
    // 刷新令牌失败
    return false;
  }
}

// 将 JWT 令牌保存到本地存储
Future<void> _saveTokenToStorage(String token) async {
  // 将 JWT 令牌保存到本地存储
  await _setTokenInStorage(JWT_TOKEN_KEY, token);
}

// 从本地存储中获取 JWT 令牌
Future<String?> _getTokenFromStorage() async {
  // 从本地存储中获取 JWT 令牌
  return await _getTokenFromStorage(JWT_TOKEN_KEY);
}

// 一些辅助函数,用于操作本地存储
Future<void> _setTokenInStorage(String key, String value) async {
  // 将 JWT 令牌保存到本地存储
}

Future<String?> _getTokenFromStorage(String key) async {
  // 从本地存储中获取 JWT 令牌
}

在这个示例中,我们使用 Dio 库创建了一个 HTTP 客户端,并添加了两个拦截器:

  1. 请求拦截器: 在每个请求中添加 Authorization 标头,包含 JWT 令牌。
  2. 错误拦截器: 当收到 401 Unauthorized 错误时,尝试刷新 JWT 令牌并重试请求。如果刷新令牌失败,则需要用户重新登录。

我们还定义了一些辅助函数,用于从本地存储中获取和保存 JWT 令牌。

加密你的有价值数据

数据的加密安全是最直接的保护措施,一般要关心动态、静态的数据安全。

  • 动态如通讯数据可以用 https ,传输过程中是加密的,抓包后数据无效。
  • 动态就是存储,如有些离线数据可以用 flutter_secure_storage 组件保存。
  • 还有些重要数据你可以用对称加密,或者自研的存储技术。

这里我简单说下 flutter_secure_storage 库的加密原理。

flutter_secure_storage 库是基于各个平台(Android 和 iOS)的原生安全存储机制来实现的。它使用了以下加密技术:

Android: 在 Android 上,flutter_secure_storage 使用 Android KeyStore API 来存储数据。KeyStore 是一个用于存储和管理加密密钥的 Android 系统服务。它使用硬件级别的 TEE (Trusted Execution Environment) 来存储和处理加密密钥,确保数据的安全性。数据在存储到 KeyStore 之前会先进行加密,密钥也存储在 KeyStore 中。

iOS: 在 iOS 上,flutter_secure_storage 使用 Keychain API 来存储数据。Keychain 是 iOS 系统提供的一个安全的加密存储机制,用于存储敏感信息,如密码、加密密钥等。与 Android KeyStore 类似,Keychain 也使用硬件级别的安全特性(如 Secure Enclave)来存储和处理密钥,确保数据的安全性。

具体来说,flutter_secure_storage 库在使用这些平台原生的安全存储机制时,会执行以下步骤:

  • 生成一个随机的加密密钥。
  • 使用该密钥对要存储的数据进行加密。
  • 将加密后的数据存储到平台的安全存储机制中(KeyStore 或 Keychain)。

当需要读取数据时,flutter_secure_storage 会从安全存储中读取加密后的数据,并使用之前生成的密钥进行解密。

这种方式可以确保数据在设备上的存储过程中一直处于加密状态,即使设备被盗或者系统被入侵,也无法直接读取到明文数据。这种硬件级别的加密机制确保了数据的高度安全性。

需要注意的是,flutter_secure_storage 本身并不涉及任何网络传输,它只负责设备内部数据的安全存储。

代码例子:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

// Create storage
AndroidOptions _getAndroidOptions() => const AndroidOptions(
        encryptedSharedPreferences: true,
      );
final storage = FlutterSecureStorage(aOptions: _getAndroidOptions());

// Read value
String value = await storage.read(key: key);

// Read all values
Map<String, String> allValues = await storage.readAll();

// Delete value
await storage.delete(key: key);

// Delete all
await storage.deleteAll();

// Write value
await storage.write(key: key, value: value);

安全用户会话

  • 未经授权不能读取 flutter_secure_storage 中存储的数据。

如你切换了用户,读取他的个人资料 profile ,收藏记录 等等,这是不被允许的。

你可以在存储的时候加入用户的签名进行校验。

有些铭感数据当用户切换时进行清除。

  • 会话过期、自动登出、单点登录控制、必须有个限制和周期。

如银行交易程序,在活跃时有效一旦离开,重新回来时需要重新登录。

企业程序7天不登录自动登出。

通讯程序同时只能有一个人登录。

依赖包更新

我们在 pubspec.yaml 中定义的依赖包需要经常去升级。

执行 flutter pub outdated 检查包

❯ flutter pub outdated

Showing outdated packages.
[*] indicates versions that are not the latest available.

Package Name                 Current     Upgradable  Resolvable  Latest

direct dependencies:
another_xlider               *1.1.2      -           3.0.2       3.0.2
badges                       *3.1.1      -           3.1.2       3.1.2
chewie                       *1.7.1      -           1.8.1       1.8.1
crypto                       *3.0.2      -           3.0.3       3.0.3
cupertino_icons              *1.0.5      -           1.0.8       1.0.8
dio                          *5.3.2      -           5.5.0+1     5.5.0+1
dropdown_button2             *1.7.2      -           2.3.9       2.3.9
encrypt                      *5.0.1      -           5.0.3       5.0.3
extended_image               *8.0.2      -           8.2.1       8.2.1
flutter_inappwebview         *5.8.0      -           *5.8.0      6.0.0
flutter_local_notifications  *14.0.0+1   -           17.2.1+2    17.2.1+2
flutter_markdown             *0.6.18     -           0.7.3       0.7.3
flutter_native_splash        *2.3.1      -           2.4.1       2.4.1
flutter_screenutil           *5.9.0      -           5.9.3       5.9.3
flutter_svg                  *2.0.7      -           2.0.10+1    2.0.10+1
get                          *4.6.5      -           4.6.6       4.6.6
intl                         *0.18.0     -           0.19.0      0.19.0
modal_bottom_sheet           *3.0.0-pre  -           3.0.0       3.0.0
package_info_plus            *4.1.0      -           8.0.0       8.0.0
permission_handler           *10.4.3     -           11.3.1      11.3.1
photo_view                   *0.14.0     -           0.15.0      0.15.0
pinput                       *2.2.31     -           5.0.0       5.0.0
shared_preferences           *2.2.0      -           2.2.3       2.2.3
tencent_cloud_chat_sdk       *5.1.5      -           *6.1.33     8.0.5897
url_launcher                 *6.1.14     -           6.3.0       6.3.0
video_player                 *2.7.1      -           2.9.1       2.9.1
webview_flutter              *4.4.1      -           4.8.0       4.8.0
wechat_assets_picker         *8.6.3      -           9.1.0       9.1.0
wechat_camera_picker         *3.8.0      -           4.3.1       4.3.1

dev_dependencies:
flutter_launcher_icons       *0.12.0     -           0.13.1      0.13.1
flutter_lints                *2.0.0      -           4.0.0       4.0.0
No resolution was found. Try running `flutter pub upgrade --dry-run` to explore why.

升级检查提示 flutter pub upgrade --dry-run

❯ flutter pub upgrade --dry-run                                                                                                                                          ─╯
Resolving dependencies... (2.4s)
Note: intl is pinned to version 0.19.0 by flutter_localizations from the flutter SDK.
See https://dart.dev/go/sdk-version-pinning for details.


Because video_ducafecat_flutter_v3 depends on flutter_localizations from sdk which depends on intl 0.19.0, intl 0.19.0 is required.
So, because video_ducafecat_flutter_v3 depends on intl 0.18.0, version solving failed.


You can try the following suggestion to make the pubspec resolve:
* Try upgrading your constraint on intl: flutter pub add intl:^0.19.0

日志和监控

加入程序的异常监控,发现用户行为可疑问题。

常用工具有:

小结

Flutter应用安全已成为当前开发的重中之重。本文为Flutter开发者总结了6大关键的应用安全最佳实践,内容涵盖了密钥管理、访问控制、数据加密、会话安全、依赖更新等关键领域。遵循这些实践,开发者就能确保自家Flutter应用拥有堪称"铜墙铁壁"的安全防护,为用户提供可靠的隐私保护。

感谢阅读本文

如果有什么建议,请在评论中让我知道。我很乐意改进。


flutter 学习路径


© 猫哥
ducafecat.com

end


独立开发者_猫哥
669 声望128 粉丝