Flutter InAppWebView安卓设备Cookie传递Auth Token失败问题
环境信息
| 技术 | 版本 |
|---|---|
| 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

