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

Flutter中用aad_oauth实现微软账号免重复输入登录

问题描述

我希望实现:用户退出我的Flutter应用后,点击“Sign In”按钮时,能显示已登录账号选择界面,无需重新输入微软凭据。但当前使用aad_oauth包实现的登录功能,用户退出后需从头输入凭据,而参考应用可保留已登录账号记录。以下是我的代码配置:

auth_service.dart

class AuthService extends ChangeNotifier {
  static final Config config = Config(
    tenant: '...',
    clientId: '...',
    scope: "${dotenv.env['AppScope']!} openid profile offline_access user.read",
    redirectUri: (Platform.isAndroid)
        ? dotenv.env['androidRedirectUri']!
        : dotenv.env['iOSRedirectUri'],
    navigatorKey: navigatorKey,
    loader: const Center(child: CircularProgressIndicator()),
  );

  final AadOAuth oauth = AadOAuth(config);

  bool _isLoadingUser = true;
  bool get isLoadingUser => _isLoadingUser;
  set setIsLoadingUser(bool value) {
    _isLoadingUser = value;
    notifyListeners();
  }

  bool _isAuthenticated = false;
  bool get isAuthenticated => _isAuthenticated;

  AuthService();

  Future<void> initializeAuthService() async {
    try {
      if (await hasCachedAccountInformation()) {
        log('User is already authenticated');
        await oauth.refreshToken();
        _isAuthenticated = true;
      } else {
        log('User is not authenticated');
      }
    } catch (e) {
      log('Error initializing AuthService: $e');
    } finally {
      _isLoadingUser = false;
      notifyListeners();
    }
  }

  Future<void> login() async {
    final result = await oauth.login();
    result.fold(
      (l) => log(l.toString()),
      (r) => log('Logged in successfully, your access token: $r'),
    );
    var accessToken = await oauth.getAccessToken();
    if (accessToken != null) {
      log('Access token: $accessToken');
      _isLoadingUser = false;
      _isAuthenticated = true;
      notifyListeners();
    }
  }

  Future<void> logout() async {
    try {
      log('Logging out');
      await oauth.logout();
      _isAuthenticated = false;
      notifyListeners();
    } catch (e) {
      log('Error logging out: ${e.toString()}');
    }
  }

  Future<bool> hasCachedAccountInformation() async {
    var hasCachedAccountInformation = await oauth.hasCachedAccountInformation;
    log('HasCachedAccountInformation: $hasCachedAccountInformation');
    return hasCachedAccountInformation;
  }

  Future<String?> getCachedAccessToken() async {
    var accessToken = await oauth.getAccessToken();
    log('Got Access token: $accessToken');
    return accessToken;
  }

  refreshToken() async {
    return await oauth.refreshToken();
  }
}

auth_gate.dart

class AuthGate extends StatelessWidget {
  AuthGate({super.key});

  // ... 省略其他代码

  @override
  Widget build(BuildContext context) {
    // ... 省略其他逻辑
    // User is not authenticated
    return LayoutBuilder(builder: (context, constraints) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // ... 省略其他组件
            ElevatedButton(
              onPressed: () async {
                await showAlertDialog(
                    context, authService, isMobileWidth);
              },
              child: Text(
                'SIGN IN',
                style: isMobileWidth
                    ? primaryButtonText
                    : primaryButtonText.copyWith(
                        fontSize: tabletFontSize,
                      ),
              ),
            ),
          ],
        ),
      );
    });
  }

  showAlertDialog(
    BuildContext context,
    AuthService authService,
    bool isMobileWidth,
  ) {
    // ... 省略弹窗创建代码
    // 原弹窗逻辑
  }
}
解决方案

1. 修改登出逻辑,保留账号缓存

默认的oauth.logout()会清除所有账号缓存,导致无法显示历史账号。改为仅清除当前会话令牌,保留账号信息:

Future<void> logout() async {
  try {
    log('Logging out');
    // 仅清除当前令牌,不删除账号缓存
    await oauth.clearToken();
    _isAuthenticated = false;
    notifyListeners();
  } catch (e) {
    log('Error logging out: ${e.toString()}');
  }
}

2. 添加获取缓存账号列表的方法

在AuthService中新增方法,读取aad_oauth存储的账号信息:

import 'dart:convert';

// 新增账号信息模型(根据实际存储结构调整)
class AccountInfo {
  final String? username;
  final Map<String, dynamic>? idToken;

  AccountInfo({this.username, this.idToken});

  factory AccountInfo.fromJson(Map<String, dynamic> json) {
    return AccountInfo(
      username: json['username'],
      idToken: json['idToken'],
    );
  }
}

// 在AuthService中新增方法
Future<List<AccountInfo>?> getCachedAccounts() async {
  try {
    final storage = await oauth.storage;
    final accountsJson = await storage.read('msal_accounts');
    if (accountsJson != null) {
      final List<dynamic> accountsList = jsonDecode(accountsJson);
      return accountsList.map((json) => AccountInfo.fromJson(json)).toList();
    }
    return null;
  } catch (e) {
    log('Error getting cached accounts: $e');
    return null;
  }
}

3. 实现账号选择弹窗

替换原有的登录确认弹窗,改为显示缓存账号列表供用户选择:

// 在AuthGate中新增账号选择弹窗方法
Future<void> showAccountSelectionDialog(BuildContext context, AuthService authService, bool isMobileWidth) async {
  final accounts = await authService.getCachedAccounts();
  
  if (accounts == null || accounts.isEmpty) {
    // 无缓存账号,直接启动登录流程
    authService.setIsLoadingUser = true;
    await authService.login();
    authService.setIsLoadingUser = false;
    return;
  }

  // 显示账号选择弹窗
  showDialog(
    context: context,
    builder: (BuildContext context) {
      return AlertDialog(
        title: const Text('选择登录账号'),
        content: SingleChildScrollView(
          child: ListBody(
            children: accounts.map((account) {
              return ListTile(
                title: Text(account.username ?? '未知账号'),
                subtitle: Text(account.idToken?['name'] ?? ''),
                onTap: () async {
                  Navigator.of(context).pop();
                  authService.setIsLoadingUser = true;
                  // 使用选中的账号快速登录
                  await authService.oauth.loginWithAccount(account);
                  authService.setIsLoadingUser = false;
                  // 更新认证状态
                  authService._isAuthenticated = await authService.hasCachedAccountInformation();
                  authService.notifyListeners();
                },
              );
            }).toList(),
          ),
        ),
        actions: [
          TextButton(
            child: const Text('使用其他账号'),
            onTap: () async {
              Navigator.of(context).pop();
              authService.setIsLoadingUser = true;
              await authService.login();
              authService.setIsLoadingUser = false;
            },
          ),
        ],
      );
    },
  );
}

然后修改Sign In按钮的点击事件,调用新的弹窗方法:

ElevatedButton(
  onPressed: () async {
    await showAccountSelectionDialog(context, authService, isMobileWidth);
  },
  child: Text(
    'SIGN IN',
    style: isMobileWidth
        ? primaryButtonText
        : primaryButtonText.copyWith(
            fontSize: tabletFontSize,
          ),
  ),
)

4. 调整初始化逻辑

确保应用启动时仅检查认证状态,不自动登录:

Future<void> initializeAuthService() async {
  try {
    // 仅检查缓存账号状态,不自动刷新令牌登录
    _isAuthenticated = await hasCachedAccountInformation();
  } catch (e) {
    log('Error initializing AuthService: $e');
    _isAuthenticated = false;
  } finally {
    _isLoadingUser = false;
    notifyListeners();
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 20:29:50