You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中基于Custom Tabs的OAuth2认证流程:如何获取会话Cookie

Flutter中基于Custom Tabs的OAuth2认证流程:如何获取会话Cookie

首先得说,你这套BFF+Keycloak的架构思路非常靠谱——把敏感的access token存在后端Redis,移动端只持有会话Cookie,确实比直接在App里存token安全太多,完全不用怀疑这个架构选择。

回到你的问题:用Custom Tabs/Safari View Controller完成认证后,怎么拿到网关返回的会话Cookie并在后续请求中复用?这里分两种可行方案,结合平台特性给你拆解:

方案一:让网关在重定向时附带Session ID到回调URI(最简单,需后端配合)

如果你的Spring Cloud Gateway可以调整重定向逻辑,这是最省心的办法:

  • 当网关完成授权码交换、生成会话Cookie后,重定向到自定义URI时,把会话ID作为查询参数附加进去,比如:yourapp://auth/callback?sessionId=xxx-xxx-xxx
  • Flutter应用在处理这个回调URI时,直接从queryParameters里提取sessionId,然后手动构造Cookie并存到HTTP客户端的CookieJar中。

示例代码(Flutter层):

void handleAuthCallback(Uri callbackUri) {
  final sessionId = callbackUri.queryParameters['sessionId'];
  if (sessionId == null) return;
  
  // 把Session ID存到dio的CookieJar里
  final cookieJar = CookieJar();
  cookieJar.saveFromResponse(
    Uri.parse('https://your-gateway-domain'),
    [Cookie('SESSIONID', sessionId)..domain = 'your-gateway-domain'],
  );
  
  // 后续用dio请求时会自动带上Cookie
  final dio = Dio()..interceptors.add(CookieManager(cookieJar));
}

这个方案的好处是不用写原生代码,全Flutter层面就能搞定,而且自定义URI的回调是本地处理的,Session ID不会经过公开网络,安全性没问题。

方案二:通过原生代码同步系统浏览器的Cookie(无需后端修改)

如果不想改动后端,就需要利用Android/iOS的原生API,从Custom Tabs/Safari View Controller的Cookie存储中拿到网关设置的会话Cookie,再同步到Flutter的HTTP客户端。

步骤1:配置自定义URI回调

先确保你的App已经配置了自定义URI scheme,让认证完成后能回到Flutter:

  • Android:在AndroidManifest.xml的主Activity里添加intent-filter:
<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="yourapp" android:host="auth" android:path="/callback" />
</intent-filter>
  • iOS:在Info.plist里添加URL Types配置:
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>yourapp</string>
        </array>
    </dict>
</array>

步骤2:用Custom Tabs/Safari View Controller启动认证

推荐用flutter_custom_tabs插件,它同时支持Android和iOS的浏览器组件:

import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';

Future<void> startAuthFlow() async {
  final authUrl = Uri.parse('https://your-gateway-domain/auth/redirect-to-keycloak');
  try {
    await launchUrl(
      authUrl,
      customTabsOptions: CustomTabsOptions(
        urlBarHidingEnabled: true,
        closeButton: CustomTabsCloseButton(icon: CustomTabsCloseButtonIcon.back),
      ),
      safariVCOptions: SafariViewControllerOptions(
        preferredBarTintColor: Colors.blue,
        preferredControlTintColor: Colors.white,
      ),
    );
  } catch (e) {
    print('启动认证流程失败: $e');
  }
}

步骤3:原生代码获取Cookie并传递给Flutter

Android端(Kotlin)

在MainActivity里注册MethodChannel,调用系统CookieManager获取指定域名的Cookie:

import android.webkit.CookieManager
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity() {
    private val CHANNEL = "com.your.app/cookie"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            if (call.method == "getSessionCookie") {
                val domain = call.argument<String>("domain") ?: ""
                val cookieManager = CookieManager.getInstance()
                val allCookies = cookieManager.getCookie(domain)
                // 提取你的会话Cookie(比如名字是SESSIONID)
                val sessionCookie = allCookies?.split(";")?.firstOrNull { it.trim().startsWith("SESSIONID=") }
                result.success(sessionCookie)
            } else {
                result.notImplemented()
            }
        }
    }
}

iOS端(Swift)

在AppDelegate里注册MethodChannel,用WKHTTPCookieStore获取Safari的Cookie:

import UIKit
import Flutter
import WebKit

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    private let CHANNEL = "com.your.app/cookie"

    override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let controller = window?.rootViewController as! FlutterViewController
        let channel = FlutterMethodChannel(name: CHANNEL, binaryMessenger: controller.binaryMessenger)
        
        channel.setMethodCallHandler { [weak self] call, result in
            guard let self = self else { return }
            if call.method == "getSessionCookie" {
                guard let domain = call.arguments as? String else {
                    result.success(nil)
                    return
                }
                // 去掉https://前缀,匹配Cookie的domain
                let cookieDomain = domain.replacingOccurrences(of: "https://", with: "")
                WKHTTPCookieStore.default().getAllCookies { cookies in
                    let sessionCookie = cookies.first {
                        $0.name == "SESSIONID" && $0.domain.contains(cookieDomain)
                    }
                    result.success(sessionCookie?.description.split(separator: ";").first)
                }
            } else {
                result.notImplemented()
            }
        }
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}

步骤4:Flutter层接收Cookie并存储

通过MethodChannel调用原生方法获取Cookie,然后存到HTTP客户端的CookieJar:

import 'package:flutter/services.dart';
import 'package:dio/dio.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:cookie_jar/cookie_jar.dart';

Future<String?> _getSessionCookie(String domain) async {
  const platform = MethodChannel('com.your.app/cookie');
  try {
    return await platform.invokeMethod('getSessionCookie', {'domain': domain});
  } on PlatformException catch (e) {
    print('获取Cookie失败: ${e.message}');
    return null;
  }
}

void handleAuthCallback(Uri callbackUri) async {
  final gatewayDomain = 'https://your-gateway-domain';
  final sessionCookie = await _getSessionCookie(gatewayDomain);
  if (sessionCookie == null) return;
  
  // 存储到CookieJar
  final cookieJar = CookieJar();
  await cookieJar.saveFromResponse(
    Uri.parse(gatewayDomain),
    [Cookie.fromSetCookieValue(sessionCookie)],
  );
  
  // 配置dio自动携带Cookie
  final dio = Dio()..interceptors.add(CookieManager(cookieJar));
  // 测试请求受保护的API
  final response = await dio.get('$gatewayDomain/api/protected');
  print('受保护API响应: ${response.data}');
}

关键注意事项

  1. HttpOnly Cookie:网关设置的Cookie如果是HttpOnly的,不用担心——原生的CookieManager/WKHTTPCookieStore可以正常获取,HttpOnly只是限制JavaScript访问,不影响原生API。
  2. Cookie Domain/Path:确保网关设置的Cookie的domain和path正确,否则原生代码可能找不到对应的Cookie。
  3. 会话有效期:和后端约定好会话Cookie的有效期,以及失效后的处理逻辑(比如跳转重新认证)。

备注:内容来源于stack exchange,提问作者Riccardo Bellini

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.21 08:58:05