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

Flutter InAppWebView安卓设备Cookie传递Auth Token失败问题

问题:Flutter InAppWebView在Android上设置认证Cookie不生效

环境信息

技术版本
Flutter版本3.16.8
插件版本^6.0.0
安卓版本所有安卓设备(minSdk 19)

问题描述

我在Flutter应用中尝试通过将OAuth的accessToken和refreshToken设置为Cookie,实现应用与Web应用之间的用户认证跳转。分别尝试了两种方案:使用CookieManager、在initialUserScripts中执行JavaScript,两种方案都出现相同问题:iOS设备上InAppWebView加载Angular前端时用户已认证,但Android设备上用户会被要求重新登录。另外,设置Cookie后打印内容确认了Cookie已存在。

方案一:使用CookieManager

class WebViewDetailScreenArguments extends Equatable {
  final String title;
  final String uri;
  final bool isServiceBaseUri;

  const WebViewDetailScreenArguments({
    required this.uri,
    this.title = '',
    this.isServiceBaseUri = false,
  });

  @override
  bool get stringify => true;

  @override
  List<Object> get props => [title, uri, isServiceBaseUri];
}

class WebViewDetailScreenWrapperProvider extends StatelessWidget {
  final WebViewDetailScreenArguments arguments;

  const WebViewDetailScreenWrapperProvider({super.key, required this.arguments});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) =>
          WebviewCubit()..initWithAuth(path: arguments.uri, isServiceBaseUri: arguments.isServiceBaseUri),
      child: WebViewDetailScreen(
        title: arguments.title,
      ),
    );
  }
}

class WebViewDetailScreen extends StatefulWidget {
  final String title;

  const WebViewDetailScreen({
    super.key,
    required this.title,
  });

  @override
  State<WebViewDetailScreen> createState() => _WebViewDetailScreenState();
}

class _WebViewDetailScreenState extends State<WebViewDetailScreen> {
  @override
  Widget build(BuildContext context) {
    final GlobalKey webViewKey = GlobalKey();
    CookieManager _cookieManager = CookieManager.instance();

    final InAppWebViewSettings settings = InAppWebViewSettings(
      isInspectable: kDebugMode,
      thirdPartyCookiesEnabled: true,
      javaScriptEnabled: true,
      sharedCookiesEnabled: true,
    );

    return Scaffold(
      extendBodyBehindAppBar: true,
      appBar: TitleAppBar(
        isBackEnabled: true,
        isDarkActionBar: false,
        centerItem: Text(
          widget.title,
          style: context.textStyles.h5.semiBold.onContainer,
        ),
        onBackPressedCallback: () => Navigator.pop(context),
        leadingText: context.i18n.tooltipBackText,
        tooltipBackText: context.i18n.tooltipBackText,
        componentsConfig: context.componentStyles,
        assetConfig: context.assets,
      ),
      body: BlocBuilder<WebviewCubit, WebviewState>(
        builder: (context, state) {
          if (state is WebviewLoaded) {
            return InAppWebView(
              key: webViewKey,
              initialUrlRequest: URLRequest(url: WebUri(state.uri), httpShouldHandleCookies: true),
              initialSettings: settings,
              onWebViewCreated: (InAppWebViewController controller) async {
                /// Deleting the cookies or not here does not make a difference
                await _cookieManager.deleteCookies(url: WebUri(state.uri));

                await _cookieManager.setCookie(
                    url: WebUri(state.uri),
                    name: 'accessToken',
                    domain: WebUri(state.uri).host, /// I tried hardcoding all URIs and domains with no luck
                    isSecure: true,
                    value: state.accessToken);
                await _cookieManager.setCookie(
                    url: WebUri(state.uri),
                    domain: WebUri(state.uri).host,
                    name: 'refreshToken',
                    isSecure: true,
                    value: state.refreshToken);
                await _cookieManager.setCookie(
                    url: WebUri(state.logoutUri),
                    domain: WebUri(state.uri).host,
                    name: 'logoutRedirectUri',
                    isSecure: true,
                    value: state.refreshToken);

                final accessCookie = await _cookieManager.getCookie(url: WebUri(state.uri), name: 'accessToken');
                final refreshCookie = await _cookieManager.getCookie(url: WebUri(state.uri), name: 'refreshToken');
                print(accessCookie);
                print(refreshCookie);
              },
            );
          } else if (state is WebviewFailure) {
            return Placeholder();
          } else {
            return AppBackgroundContainer(
              colorConfig: context.colorsWatch,
              child: Center(
                child: CircularProgressIndicator(),
              ),
            );
          }
        },
      ),
    );
  }
}

方案二:使用JavaScript

class WebViewDetailScreenArguments extends Equatable {
  final String title;
  final String uri;
  final bool isServiceBaseUri;

  const WebViewDetailScreenArguments({
    required this.uri,
    this.title = '',
    this.isServiceBaseUri = false,
  });

  @override
  bool get stringify => true;

  @override
  List<Object> get props => [title, uri, isServiceBaseUri];
}

class WebViewDetailScreenWrapperProvider extends StatelessWidget {
  final WebViewDetailScreenArguments arguments;

  const WebViewDetailScreenWrapperProvider(
      {super.key, required this.arguments});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => WebviewCubit()
        ..initWithAuth(
            path: arguments.uri, isServiceBaseUri: arguments.isServiceBaseUri),
      child: WebViewDetailScreen(
        title: arguments.title,
      ),
    );
  }
}

class WebViewDetailScreen extends StatefulWidget {
  final String title;

  const WebViewDetailScreen({
    super.key,
    required this.title,
  });

  @override
  State<WebViewDetailScreen> createState() => _WebViewDetailScreenState();
}

class _WebViewDetailScreenState extends State<WebViewDetailScreen> {
  @override
  Widget build(BuildContext context) {
    final GlobalKey webViewKey = GlobalKey();

    InAppWebViewSettings settings = InAppWebViewSettings(
      isInspectable: kDebugMode,
      thirdPartyCookiesEnabled: true,
      javaScriptEnabled: true,
      sharedCookiesEnabled: true,
    );

    return Scaffold(
      extendBodyBehindAppBar: true,
      appBar: TitleAppBar(
        isBackEnabled: true,
        isDarkActionBar: false,
        centerItem: Text(
          widget.title,
          style: context.textStyles.h5.semiBold.onContainer,
        ),
        onBackPressedCallback: () => Navigator.pop(context),
        leadingText: context.i18n.tooltipBackText,
        tooltipBackText: context.i18n.tooltipBackText,
        componentsConfig: context.componentStyles,
        assetConfig: context.assets,
      ),
      body: BlocBuilder<WebviewCubit, WebviewState>(
        builder: (context, state) {
          if (state is WebviewLoaded) {
            return InAppWebView(
              key: webViewKey,
              initialUrlRequest: URLRequest(
                  url: WebUri(state.uri), httpShouldHandleCookies: true),
              initialSettings: settings,
              initialUserScripts: UnmodifiableListView<UserScript>([
                UserScript(
                    source:
                        'document.cookie="accessToken=${state.accessToken}";',
                    injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START),
                UserScript(
                    source:
                        'document.cookie="refreshToken=${state.refreshToken}";',
                    injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START),
                UserScript(
                    source:
                        'document.cookie="logoutRedirectUri=${state.logoutUri}";',
                    injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START),
                UserScript(
                    source: 'console.log(document.cookie)";;',
                    injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START),
              ]),
            );
          } else if (state is WebviewFailure) {
            return Placeholder();
          } else {
            return AppBackgroundContainer(
              colorConfig: context.colorsWatch,
              child: Center(
                child: CircularProgressIndicator(),
              ),
            );
          }
        },
      ),
    );
  }
}

预期与实际行为

  • 预期:iOS和Android设备上,通过InAppWebView进入Angular前端时用户均已完成认证。
  • 实际:iOS正常,Android会要求用户重新登录。

解决方案

1. 完善Cookie属性设置(针对CookieManager方案)

Android WebView对Cookie的属性要求更严格,需要补充path、sameSite、maxAge等属性,确保Cookie能被正确携带:

await _cookieManager.setCookie(
  url: WebUri(state.uri),
  name: 'accessToken',
  domain: WebUri(state.uri).host,
  path: '/', // 设置根路径,确保所有子路径都能访问
  isSecure: true,
  sameSite: HTTPCookieSameSitePolicy.NONE, // 跨域场景需设为NONE,同时保证isSecure=true
  maxAge: 3600 * 24 * 7, // 设置有效期,避免Cookie立即过期
  value: state.accessToken
);
// refreshToken同理补充这些属性

2. 调整Cookie设置时机

不要在onWebViewCreated中设置Cookie,而是在onLoadStart回调中设置,确保WebView准备好接收Cookie:

InAppWebView(
  // ...其他配置
  onLoadStart: (controller, url) async {
    if (url != null) {
      await _cookieManager.deleteCookies(url: url);
      // 重新设置所有Cookie
      await _cookieManager.setCookie(...);
      await _cookieManager.setCookie(...);
    }
  },
)

3. 针对JavaScript方案补充Cookie属性

通过JS设置Cookie时,同样需要添加必要属性,否则Android WebView可能拒绝存储:

UserScript(
  source: 'document.cookie="accessToken=${state.accessToken}; path=/; secure; SameSite=None; max-age=604800";',
  injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START
)

注意:SameSite=None必须配合secure属性使用,否则Cookie不会生效。

4. 检查Android WebView的Cookie策略

在Android原生代码中确保WebView允许第三方Cookie,可在MainActivity中添加:

import android.webkit.CookieManager;
import android.os.Build;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    CookieManager cookieManager = CookieManager.getInstance();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        cookieManager.setAcceptThirdPartyCookies(getWebView(), true);
    } else {
        cookieManager.setAcceptCookie(true);
    }
}

5. 验证Cookie是否被实际发送

使用Chrome DevTools调试Android WebView,检查网络请求的Cookie头是否包含设置的accessToken和refreshToken:

  • 连接设备,打开Chrome浏览器输入chrome://inspect
  • 找到对应的WebView实例,查看网络请求的Request Headers

关键原因分析

Android WebView对Cookie的SameSite属性、路径、有效期等要求比iOS更严格,若缺少必要属性,Cookie会被视为无效,不会在请求中携带。另外,设置Cookie的时机过早(WebView未完全初始化)也可能导致Cookie未被正确应用。

内容的提问来源于stack exchange,提问作者Fkruger

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 02:37:04