Flutter AppAuth在Android端登录后重定向异常求助
Flutter AppAuth + Keycloak Android端登录后重复打开浏览器问题排查
问题现象
自定义封装了AuthWrapper基于flutter_appauth实现认证逻辑,通过布尔流控制应用界面切换,iOS端运行正常,但Android端存在以下问题:
- 点击登录按钮后,Chrome正常加载Keycloak登录页,输入有效凭证完成登录
- 页面瞬间重定向回应用后,随即再次打开浏览器窗口
- 已测试模拟器、物理设备及多版本Android系统,修改Keycloak重定向URI为单字词等方案均无效
相关代码
lib/main.dart
import 'package:auth/auth/wrapper.dart'; import 'package:flutter/material.dart'; final authWrapper = AuthWrapper(); void main() { WidgetsFlutterBinding.ensureInitialized(); authWrapper.initialize(); runApp(const MainApp()); } class MainApp extends StatelessWidget { const MainApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Center( child: StreamBuilder<bool>( stream: authWrapper.authenticationStream, builder: (context, snapshot) { final isLoggedIn = snapshot.data ?? false; return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(isLoggedIn ? 'Hello World!' : 'Welcome!'), ElevatedButton( onPressed: isLoggedIn ? authWrapper.logout : authWrapper.login, child: Text(isLoggedIn ? 'Logout' : 'Login'), ) ], ); }, ), ), ), ); } }
lib/auth/wrapper.dart
import 'dart:async'; import 'dart:developer'; import 'package:flutter_appauth/flutter_appauth.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; const _appAuth = FlutterAppAuth(); const _secureStorage = FlutterSecureStorage(); const Map<String, String> authConfig = { 'clientId': 'domx_mobile_app', 'redirectUri': 'com.io.domx://auth', 'discoveryUrl': 'https://sso.domx-dev.com/auth/realms/domx' '/.well-known/openid-configuration', }; class AuthWrapper { factory AuthWrapper() => _instance ??= AuthWrapper._(); AuthWrapper._(); static AuthWrapper? _instance = AuthWrapper._(); bool _isInitialized = false; late final _streamController = StreamController<bool>(); TokenResponse? tokenResponse; bool get isTokenResponseValid => tokenResponse != null && tokenResponse?.accessToken != null && tokenResponse?.idToken != null; Stream<bool> get authenticationStream => _streamController.stream; bool get isInitialized => _isInitialized; String? get idToken => tokenResponse?.idToken; String? get refreshToken => tokenResponse?.refreshToken; void _assert() { const message = 'Make sure the package has been initialized prior to calling this method.'; assert(_isInitialized, message); } Future<void> initialize() async { try { final securedRefreshToken = await _secureStorage.read( key: 'refreshToken', ); if (securedRefreshToken == null) { log('No refresh token is stored'); _streamController.add(false); } else { tokenResponse = await _appAuth.token( TokenRequest( authConfig['clientId']!, authConfig['redirectUri']!, discoveryUrl: authConfig['discoveryUrl']!, refreshToken: securedRefreshToken, ), ); await _secureStorage.write( key: 'refreshToken', value: refreshToken, ); _streamController.add(isTokenResponseValid); } _isInitialized = true; } catch (error) { log( 'Initialization error', name: 'auth_wrapper', error: error, ); } } Future<void> login() async { _assert(); try { tokenResponse = await _appAuth.authorizeAndExchangeCode( AuthorizationTokenRequest( authConfig['clientId']!, authConfig['redirectUri']!, discoveryUrl: authConfig['discoveryUrl']!, scopes: ['openid', 'profile'], promptValues: ['login'], preferEphemeralSession: true, ), ); if (isTokenResponseValid && refreshToken != null) { await _secureStorage.write( key: 'refreshToken', value: tokenResponse!.refreshToken, ); } else { log('Invalid token response.'); } _streamController.add(isTokenResponseValid); } catch (error) { log( 'Login error', name: 'auth_wrapper', error: error, ); } } Future<void> logout() async { _assert(); try { await _appAuth.endSession(EndSessionRequest( idTokenHint: idToken, discoveryUrl: authConfig['discoveryUrl']!, postLogoutRedirectUrl: authConfig['redirectUri']!, preferEphemeralSession: true, )); await _secureStorage.delete(key: 'refreshToken'); _streamController.add(false); } catch (error) { log( 'Login error', name: 'auth_wrapper', error: error, ); } } }
Android build.gradle配置
//... android { //... defaultConfig { applicationId = "com.example.auth" minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion versionCode = flutterVersionCode.toInteger() versionName = flutterVersionName manifestPlaceholders += ['appAuthRedirectScheme': 'com.io.domx'] } //... } //...
排查与解决方向
1. 统一重定向Scheme配置
- 确认Keycloak客户端的Valid Redirect URIs和Valid Post Logout Redirect URIs完全匹配
com.io.domx://auth - 检查Android端
appAuthRedirectScheme与redirectUri的Scheme部分(com.io.domx)完全一致,当前配置已符合,但需确保Keycloak端无拼写错误
2. 移除Ephemeral Session配置
Android端preferEphemeralSession: true可能与系统浏览器重定向逻辑冲突,尝试在login()和logout()的请求中移除该参数:
// 登录请求修改 AuthorizationTokenRequest( authConfig['clientId']!, authConfig['redirectUri']!, discoveryUrl: authConfig['discoveryUrl']!, scopes: ['openid', 'profile'], promptValues: ['login'], // 注释掉该行 // preferEphemeralSession: true, )
3. 修复初始化流程的流事件发送
当前initialize()方法的catch块未发送认证状态流事件,可能导致状态异常触发重复登录:
catch (error) { log( 'Initialization error', name: 'auth_wrapper', error: error, ); // 添加该行,确保未登录状态被正确发送 _streamController.add(false); _isInitialized = true; }
4. 手动确认AndroidManifest Intent Filter
flutter_appauth通常会自动生成Intent Filter,但可手动验证AndroidManifest.xml的MainActivity下是否存在:
<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="com.io.domx" /> </intent-filter>
5. 升级flutter_appauth版本
旧版本可能存在Android端重定向bug,升级至最新稳定版尝试解决问题
内容的提问来源于stack exchange,提问作者George Kalogeropoulos
相关产品推荐
相关产品推荐

