tags: Flutter
、env
、key
[Note] Flutter 如何隱藏 API Key
前言
介紹 Flutter 常見的 API keys 使用方法,主要有三種,可以依情境選擇需要的方式。
內文主要是依照 How to Store API Keys in Flutter 整理出的方法,文章寫得很詳細,非常推薦閱讀 👍
Hard-coding
直接把 key 值寫在 .dart
檔案中。
先在 api_key.dart
新增 API key,在需要使用的檔案中,引入 api_key.dart
:
// api_key.dart
final apiKey = 'AIzaSyBeSYQ8cn6IIwRBbB4hPrn';
// Step 1. import key 所在的檔案
import 'api_key.dart';
class LocationService {
Future<String> getPlaceId(String input) async {
// Step 2.直接使用 key 值
final String url =
'https://maps.googleapis.com/maps/api/place/findplacefromtext/json?input=$input&inputtype=textquery&key=$apiKey';
var response = await http.get(Uri.parse(url));
var json = convert.jsonDecode(response.body);
var placeId = json['candidates'][0]['place_id'] as String;
return placeId;
}
}
需要注意的地方就是,千萬不要把 api_key.dart
commit 進 git,需要另外把 api_key.dart
加入 .gitignore
檔案中:
# 在 .gitignore 中的檔案,都不會進入 git 版控紀錄
api_key.dart
優點
- 快速使用
缺點
- 無法根據環境去 mapping 對應的 key
- key 以明文方式儲存(plaintext),安全性不高
- 可能會因失誤,造成 key 值外洩 e.g. 忘記加進
.gitignore
⚠️ 提醒:
api_key.dart
如曾經被 commit 進 git,後續再把它加入到 .gitignore
,也沒用,因為都可以在 git history 找到相關 key 資訊,唯一能做的就是銷毀原先的 key,再新增一個新的。
–dart-define / --dart-define-from-file
在編譯階段,使用 --dart-define
或 --dart-define-from-file
將 key 傳入程式中使用。
–dart-define
在程式中使用 String.fromEnvironment(<key-name>)
取得 key,並改用下面指令去 run 程式,將 key 傳入進程式:
class LocationService {
Future<String> getPlaceId(String input) async {
// get api key
const apiKey = String.fromEnvironment('API_KEY');
if (apiKey.isEmpty) {
throw AssertionError('API_KEY is not set');
}
// use api key
final String url = 'https://maps.googleapis.com/maps/api/place/findplacefromtext/json?input=$input&inputtype=textquery&key=$apiKey';
...
}
$flutter run --dart-define API_KEY=AIzaSyBeSYQ8cn
優點
- source code 不會存在 Hard-coding key
缺點
-
複數 keys 時,難以管理
$flutter run \ --dart-define A_API_KEY= ... \ --dart-define B_API_KEY= ... \ --dart-define C_API_KEY= ...
-
在編譯後,key 仍然會被嵌入到發佈版本(release binary)的二進制文件中
針對發佈版本(release binary)需要對 Dart 代碼進行混淆(混淆可以讓代碼難以被理解和解讀),降低被反向破譯的風險,可以參考官方推薦做法。
--dart-define-from-file
在 Flutter 3.7
後,可以將 API keys 存成一個 json 檔案(需要進 .gitignore
),再改用下面指令去 run 程式,將 json 內容傳入進程式:
// api-keys.json
{
"GOOGLE_MAP_KEY": "...",
"A_API_KEY": "...",
"B_API_KEY": "..."
}
$flutter run --dart-define-from-file=api-keys.json
程式中一樣使用 String.fromEnvironment(<key-name>)
取得 key 值:
const apiKey = String.fromEnvironment('GOOGLE_MAP_KEY');
if (apiKey.isEmpty) {
// handle error
}
// use api key
...
--dart-define-from-file
與 launch.json
組合技 (VSCode)
在 .vscode/launch.json
中的 args
(會代入在 command line),加入下列參數:
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Development",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
// add here
"args": ["--dart-define-from-file", "api-keys.json"]
}
]
}
也可以針對環境,有不一樣的 API keys,i.e. Production 環境使用 api-keys.prod.json
:
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Development",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": ["--dart-define-from-file", "api-keys.json"]
},
{
"name": "Launch Production",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": ["--dart-define-from-file", "api-keys.prod.json"]
}
]
}
關於 launch.json(組態檔)
launch.json
是定義調試過程中所需的各種設定,如程式入口、命令列參數、環境變數等。
除了手動新增檔案,也可以透過 VSCode 自動生成一個,點選 Run and Debug
> create a launch.json file
> Dart & Flutter
:
新增的檔案內容與上面範例有點不同,因為這份檔案把 flutterMode 三個運行模式(debug
/profile
/release
) 都列出來了,要加上各個環境 keys 的版本如下:
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "flutter-appworks",
"request": "launch",
"type": "dart",
"args": ["--dart-define-from-file", "api-keys.json"]
},
{
"name": "flutter-appworks (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile",
"args": ["--dart-define-from-file", "api-keys.json"]
},
{
"name": "flutter-appworks (release mode)",
"request": "launch",
"type": "dart",
"flutterMode": "release",
"args": ["--dart-define-from-file", "api-keys.prod.json"]
}
]
}
[補充 QA]
Q. version
為什麼是 0.2.0 呢?
目前沒找到確切為什麼會是 0.2.0 的原因,但官網範例也都是使用 0.2.0 版本,應該是目前 default 值。
最後也有詢問 chatGPT,指出如果使用的是舊版本的 VSCode 或希望使用不同的配置文件語法,可以將 “version” 更改為 “0.1.0” 或其他版本號。
:::
💡 補充
如果是使用其他 IDE,例如 IntelliJ 或 Android Studio,可參考 Run/debug configurations。
優點
- 方便管理多組 keys
- 可針對不同環境(dev/prod),設置不同 key 的 json 檔案
缺點
- json 檔案需要加入
.gitignore
清單中 (json 中的 key 也是屬於 hardcode)
.env 檔案定義 key
把 key 值統一管理在 .env
檔案中,會需要配合額外的 package,例如: envied
Step 1. 安裝 envied 套件
envied
可以幫助我們生成一個 Dart class,包含 .env
檔案中的值。
$ flutter pub add envied
$ flutter pub add --dev envied_generator
$ flutter pub add --dev build_runner
Step 2. 新增 .env
# .env
API_KEY=AIzaSyBeSYQ8cn6IIwRBbB4hPrn
Step 3. 新增 env.dart
env.g.dart
後面會用指令產生,目前會報錯,可以忽略:
// lib/env.dart
import 'package:envied/envied.dart';
part 'env.g.dart';
@Envied(path: '.env')
abstract class Env {
@EnviedField(varName: 'API_KEY')
static const apiKey = _Env.apiKey;
}
以上寫法已經足夠使用了,但為了提高安全性,可以使用套件提供的混淆(Obfuscation)功能,需要在 @EnviedField
加上 obfuscate: true
:
// lib/env.dart
import 'package:envied/envied.dart';
part 'env.g.dart';
@Envied(path: '.env')
abstract class Env {
// Add here
@EnviedField(varName: 'API_KEY', obfuscate: true)
static final apiKey = _Env.apiKey;
}
💡 補充:
如果使用 obfuscate
flag,需要將 const
改成 final
,否則會報錯。
Step 4. 產生 env.g.dart
$flutter pub run build_runner build --delete-conflicting-outputs
執行完後,會產生一份 env.g.dart
:
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'env.dart';
// **************************************************************************
// EnviedGenerator
// **************************************************************************
class _Env {
static const List<int> _enviedkeyapiKey = [
2433719592,
1273530568,
....
];
static const List<int> _envieddataapiKey = [
2433719657,
1273530497,
....
];
static final apiKey = String.fromCharCodes(
List.generate(_envieddataapiKey.length, (i) => i, growable: false)
.map((i) => _envieddataapiKey[i] ^ _enviedkeyapiKey[i])
.toList(growable: false),
);
}
💡 補充:
如果不在 @EnviedField
加上 obfuscate: true
,產出來的 env.g.dart
內容如下:
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'env.dart';
// **************************************************************************
// EnviedGenerator
// **************************************************************************
class _Env {
static const apiKey = 'AIzaSyBeSYQ8cn6IIwRBbB4hPrn';
}
這樣 apiKey
會以明碼方式存在於 dart 檔案中。
Step 5. 將 *.env*
與 env.g.dart
加入 .gitignore
這兩份檔案基本都有 key 的資訊,都需要加入 .gitignore
內:
# .gitignore
env.g.dart
*.env*
Step 6. 程式內使用 API keys
引入 env.dart
檔案,使用 Env.<key-name>
取得 key 的值:
// 引入 env.dart
import 'package:flutter_appworks/env.dart';
class LocationService {
// use api key
final key = Env.apiKey;
Future<String> getPlaceId(String input) async {
final String url =
'https://maps.googleapis.com/maps/api/place/findplacefromtext/json?input=$input&inputtype=textquery&key=$key';
var response = await http.get(Uri.parse(url));
var json = convert.jsonDecode(response.body);
var placeId = json['candidates'][0]['place_id'] as String;
return placeId;
}
💡 補充:
envied
v.s. flutter_dotenv
這兩個套件都可以用來處理 flutter 環境變數的需求,差別是使用 env
檔案的方式:
envied
code generation + 程式碼混淆功能,可以讓 API keys 更加安全flutter_dotenv
將env
加入 assets 資料夾中,在 runtime 時候讀取,因此有機會可以從 APK 獲取 API keys
如果 env
沒放什麼重要的 key,那麼兩個套件都可以使用,反之,就直接選擇 envied
了。
優點
- 安全度高 (code obfuscation)
- API keys 集中管理
- 可支持多個環境設定
尚未親測過,但根據該 issue 討論,作者回應是可以做得到的。
缺點
-
步驟比較繁複:增加環境變數時,class 要手動新增項目和重新產
env.g.dart
因為筆者是寫前端,所以是跟 web 做比較,web 也是用
.env
管理環境變數,新增變數時,只要重新 run 專案,不需要再手動做什麼了。
總結
如果需要接第三方的 API,都會遇到如何在 client 去管理這些 API Key 的問題,基本只需要謹記這兩點,再依自己需求去做選擇即可:
- 重要的 key 檔案,加入
.gitignore
,不進版控 - release version 需要做程式碼混淆(code obfuscation)
另外有些套件是需要分別在 Android 和 iOS 的 AndroidManifest.xml
、AppDelegate.swift
寫入 key 值,例如: Google Maps,這時候可以考慮選擇 --dart-define
,支持只定義一次 key,就讓雙平台可以從設定檔中讀取到 key,可以參考這篇文章的做法 - How to setup dart-define for keys and secrets on Android and iOS in Flutter apps。