はじめに
本記事では、以下の状況を想定しています。
- FlutterのアプリケーションでAPIを飛ばす時にCookieを利用したい。
- APIで保存したCookieをWebViewでも使いたい。
環境
APIを飛ばす時にCookieを利用したい
既にFlutter(Dart)では、Cookieの仕組みが提供されていますので、それを利用します。 import 'dart:io';
CookieJarを利用すると、URIをキーにCookieの値を管理してくれます。
List<Cookie> loadForRequest(Uri uri)
で保存されたCookieを取得し、
void saveFromResponse(Uri uri, List<Cookie> cookies)
で、UriをキーにCookieを保存します。
HTTPクライアントでの準備です。
重要な箇所はコメントにて説明します。
class RestClient {
factory RestClient() {
return _restClient ??= RestClient._internal();
}
RestClient._internal() {
// interceptorsに、後述するIntercepterの実装クラスを渡すことで、
// リクエスト送信時、レスポンス受信時に何か処理を行うことができる。
_http.interceptors.add(CookieManager(_cookieJar));
}
static RestClient _restClient;
final _http = Dio();
final _cookieJar = CookieJar();
}
/// Interceptorを実装した独自クラスを定義する。
class CookieManager extends Interceptor {
CookieManager(this.cookieJar);
final CookieJar cookieJar;
/// リクエスト送信時に実行したい処理を記載
@override
Future onRequest(RequestOptions options) async {
final cookies = cookieJar.loadForRequest(options.uri)
..removeWhere((cookie) {
if (cookie.expires != null) {
// 期限切れなら削除する
return cookie.expires.isBefore(DateTime.now());
}
return false;
});
// オブジェクトのcookieをリクエストヘッダ用に文字列に変換する
final cookie = getCookies(cookies);
if (cookie.isNotEmpty) {
options.headers[HttpHeaders.cookieHeader] = cookie;
}
}
/// レスポンス受信時に、Cookieを保存する処理を実行
@override
Future onResponse(Response response) async => _saveCookies(response);
@override
Future onError(DioError err) async => _saveCookies(err.response);
void _saveCookies(Response response) {
if (response == null || response.headers == null) {
return;
}
// HttpHeadersに、レスポンスヘッダのCookieのKeyが定義されている。他にも様々なヘッダのKeyが記載されている。
final cookies = response.headers[HttpHeaders.setCookieHeader];
if (cookies == null) {
return;
}
// cookieの保存処理を実行
cookieJar.saveFromResponse(
response.request.uri,
cookies
.map((str) {
try {
// 補足1参照
final cookie = Cookie.fromSetCookieValue(str)
..domain = response.request.uri.host;
return cookie;
} on Exception catch (e) {
logger.warning(e);
return null;
}
})
.where((element) => element != null)
.toList(),
);
}
static String getCookies(List<Cookie> cookies) {
return cookies.map((cookie) => '${cookie.name}=${cookie.value}').join('; ');
}
}
補足1
Dart標準に用意されているCookie.fromSetCookieValue
は、Cookieの文字列をいい感じにCookieオブジェクトに変換してくれる便利なものです。
しかしながら、Cookieの文字列に []
などの記号が含まれていると、FormatException
をthrowするため、Cookieが保存されず、Cookieオブジェクトに変換されません。
対処方法は2つ考えられます。
- 投げられる例外を握り潰す。(上記例で記載されている方法)
ひとまず正常なCookieだけは生成されます。
- Cookieクラスを実装し直す。(参考: https://juejin.im/post/6844903934042046472)
例外が投げられる原因は、要するに、
Dartは RFC2616 に準拠した実装になってはいるが、必ずしもサーバ側はそうではないから。だそうです。
暫定対処として、Cookie.fromSetCookieValue
の定義元を辿ると、ソースコードが見れますので、それをコピペして、必要な処理を書き換える方法になります。
class MyCookie implements Cookie {
MyCookie(String name, String value)
: _name = _validateName(name),
_value = _validateValue(value),
httpOnly = true;
MyCookie.fromSetCookieValue(String value)
: _name = '',
_value = '' {
// Parse the 'set-cookie' header value.
_parseSetCookieValue(value);
}
// _parseSetCookieValueを実装する
}
改善のためのIssueは挙げられています。
https://github.com/dart-lang/sdk/issues/42902
APIで保存したCookieをWebViewでも使いたい。
webview_flutter を使用する場合、合わせて以下のパッケージも使用する必要があります。
webview_cookie_manager
※これがマージされれば、webview_cookie_managerは必要なくなるかもしれません。
https://github.com/flutter/plugins/pull/2999
// ...
final _cookieManager = WebviewCookieManager();
// ...
WebView(
initialUrl: url,
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (controller) async {
// cookies: List<Cookie>
// ClearしないとCookieが反映されない。
await _cookieManager.clearCookies();
if (cookies != null) {
await _cookieManager.setCookies(cookies);
}
// ...
},
)
Cookieがうまく読み込まれない場合は、initialUrlを指定するのではなく、controller.loadUrl
を使用するといいかもしれません。
// ...
final _cookieManager = WebviewCookieManager();
// ...
WebView(
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (controller) async {
// cookies: List<Cookie>
// ClearしないとCookieが反映されない。
await _cookieManager.clearCookies();
if (cookies != null) {
await _cookieManager.setCookies(cookies);
}
// Cookieを設定した後に、URLを読み込む
await controller.loadUrl(url);
// ...
},
)
flutter_webview_plugin で使用する場合は、文字列にしてheadersに指定するだけです。
// cookies: List<Cookie>
WebviewScaffold(
withLocalStorage: true,
withJavascript: true,
headers: {if (cookies != null) 'Cookie': cookies.map((cookie) => '${cookie.name}=${cookie.value}').join('; ');},
// ...
)