Flutter认证授权插件flutter_appauth的使用

发布于 1周前 作者 caililin 来自 Flutter

Flutter认证授权插件flutter_appauth的使用

Flutter AppAuth Plugin

pub package Build Status

Flutter AppAuth 是一个用于用户认证和授权的插件,它基于 AppAuth 构建。该插件支持PKCE扩展,这对于某些提供者是必需的。

重要提示

  • 此插件要求应用程序使用AndroidX。有关详细信息,请参阅Flutter AndroidX兼容性
  • 如果Chrome自定义标签在您的Android应用中无法正常工作,请确保您使用的是最新版本的此插件、Android Studio、Gradle分发版和Android Gradle插件。

来自身份提供商的教程

入门指南

首先创建插件实例:

FlutterAppAuth appAuth = FlutterAppAuth();

然后执行授权和认证请求:

final AuthorizationTokenResponse result = await appAuth.authorizeAndExchangeCode(
    AuthorizationTokenRequest(
        '<client_id>',
        '<redirect_url>',
        discoveryUrl: '<discovery_url>',
        scopes: ['openid', 'profile', 'email', 'offline_access', 'api'],
    ),
);

或者使用发行者URL代替发现URL:

final AuthorizationTokenResponse result = await appAuth.authorizeAndExchangeCode(
    AuthorizationTokenRequest(
        '<client_id>',
        '<redirect_url>',
        issuer: '<issuer>',
        scopes: ['openid', 'profile', 'email', 'offline_access', 'api'],
    ),
);

如果已知服务器端点,则可以显式指定它们:

final AuthorizationTokenResponse result = await appAuth.authorizeAndExchangeCode(
    AuthorizationTokenRequest(
        '<client_id>',
        '<redirect_url>',
        serviceConfiguration: AuthorizationServiceConfiguration(
            authorizationEndpoint: '<authorization_endpoint>',
            tokenEndpoint: '<token_endpoint>',
            endSessionEndpoint: '<end_session_endpoint>'
        ),
        scopes: [...]
    ),
);

检测用户取消

try {
  await appAuth.authorize(...); // 或 authorizeAndExchangeCode(...)
} on FlutterAppAuthUserCancelledException catch (e) {
  // 处理用户取消
}

刷新令牌

final TokenResponse result = await appAuth.token(TokenRequest(
    '<client_id>',
    '<redirect_url>',
    discoveryUrl: '<discovery_url>',
    refreshToken: '<refresh_token>',
    scopes: ['openid', 'profile', 'email', 'offline_access', 'api']
));

结束会话

await appAuth.endSession(EndSessionRequest(
    idTokenHint: '<idToken>',
    postLogoutRedirectUrl: '<postLogoutRedirectUrl>',
    serviceConfiguration: AuthorizationServiceConfiguration(
        authorizationEndpoint: '<authorization_endpoint>',
        tokenEndpoint: '<token_endpoint>',
        endSessionEndpoint: '<end_session_endpoint>'
    )
));

错误处理

try {
  await appAuth.authorize(...);
} on FlutterAppAuthPlatformException catch (e) {
  final FlutterAppAuthPlatformErrorDetails details = e.details;
  // 根据来自AppAuth的错误处理异常
} catch (e) {
  // 处理其他错误
}

完整示例代码

以下是一个完整的Flutter应用示例,演示了如何使用flutter_appauth进行OAuth2.0认证:

import 'dart:convert';
import 'dart:io' show Platform;
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:http/http.dart' as http;

void main() => runApp(const MyApp());

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool _isBusy = false;
  final FlutterAppAuth _appAuth = const FlutterAppAuth();

  String? _codeVerifier;
  String? _nonce;
  String? _authorizationCode;
  String? _refreshToken;
  String? _accessToken;
  String? _idToken;
  String? _error;

  final TextEditingController _authorizationCodeTextController =
      TextEditingController();
  final TextEditingController _accessTokenTextController =
      TextEditingController();
  final TextEditingController _accessTokenExpirationTextController =
      TextEditingController();
  final TextEditingController _idTokenTextController = TextEditingController();
  final TextEditingController _refreshTokenTextController =
      TextEditingController();
  String? _userInfo;

  final String _clientId = 'interactive.public';
  final String _redirectUrl = 'com.duendesoftware.demo:/oauthredirect';
  final String _issuer = 'https://demo.duendesoftware.com';
  final String _discoveryUrl =
      'https://demo.duendesoftware.com/.well-known/openid-configuration';
  final String _postLogoutRedirectUrl = 'com.duendesoftware.demo:/';
  final List<String> _scopes = <String>[
    'openid',
    'profile',
    'email',
    'offline_access',
    'api'
  ];

  final AuthorizationServiceConfiguration _serviceConfiguration =
      const AuthorizationServiceConfiguration(
    authorizationEndpoint: 'https://demo.duendesoftware.com/connect/authorize',
    tokenEndpoint: 'https://demo.duendesoftware.com/connect/token',
    endSessionEndpoint: 'https://demo.duendesoftware.com/connect/endsession',
  );

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: SafeArea(
          child: SingleChildScrollView(
            child: Column(
              children: <Widget>[
                Visibility(
                  visible: _isBusy,
                  child: const LinearProgressIndicator(),
                ),
                const SizedBox(height: 8),
                ElevatedButton(
                  child: const Text('Sign in with no code exchange'),
                  onPressed: () => _signInWithNoCodeExchange(),
                ),
                const SizedBox(height: 8),
                ElevatedButton(
                  child: const Text(
                      'Sign in with no code exchange and generated nonce'),
                  onPressed: () => _signInWithNoCodeExchangeAndGeneratedNonce(),
                ),
                const SizedBox(height: 8),
                ElevatedButton(
                  onPressed: _authorizationCode != null ? _exchangeCode : null,
                  child: const Text('Exchange code'),
                ),
                const SizedBox(height: 8),
                ElevatedButton(
                  child: const Text('Sign in with auto code exchange'),
                  onPressed: () => _signInWithAutoCodeExchange(),
                ),
                if (Platform.isIOS || Platform.isMacOS)
                  Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: ElevatedButton(
                      child: const Text(
                        'Sign in with auto code exchange using ephemeral '
                        'session',
                        textAlign: TextAlign.center,
                      ),
                      onPressed: () => _signInWithAutoCodeExchange(
                          externalUserAgent: ExternalUserAgent
                              .ephemeralAsWebAuthenticationSession),
                    ),
                  ),
                if (Platform.isIOS)
                  Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: ElevatedButton(
                      child: const Text(
                        'Sign in with auto code exchange using '
                        'SFSafariViewController',
                        textAlign: TextAlign.center,
                      ),
                      onPressed: () => _signInWithAutoCodeExchange(
                          externalUserAgent:
                              ExternalUserAgent.sfSafariViewController),
                    ),
                  ),
                ElevatedButton(
                  onPressed: _refreshToken != null ? _refresh : null,
                  child: const Text('Refresh token'),
                ),
                const SizedBox(height: 8),
                ElevatedButton(
                  onPressed: _idToken != null
                      ? () async {
                          await _endSession();
                        }
                      : null,
                  child: const Text('End session'),
                ),
                if (Platform.isIOS || Platform.isMacOS)
                  Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: ElevatedButton(
                        onPressed: _idToken != null
                            ? () async {
                                await _endSession(
                                    externalUserAgent: ExternalUserAgent
                                        .ephemeralAsWebAuthenticationSession);
                              }
                            : null,
                        child:
                            const Text('End session using ephemeral session'),
                      )),
                if (Platform.isIOS)
                  Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: ElevatedButton(
                        onPressed: _idToken != null
                            ? () async {
                                await _endSession(
                                    externalUserAgent: ExternalUserAgent
                                        .sfSafariViewController);
                              }
                            : null,
                        child: const Text(
                            'End session using SFSafariViewController'),
                      )),
                const SizedBox(height: 8),
                if (_error != null) Text(_error ?? ''),
                const SizedBox(height: 8),
                const Text('authorization code'),
                TextField(
                  controller: _authorizationCodeTextController,
                ),
                const Text('access token'),
                TextField(
                  controller: _accessTokenTextController,
                ),
                const Text('access token expiration'),
                TextField(
                  controller: _accessTokenExpirationTextController,
                ),
                const Text('id token'),
                TextField(
                  controller: _idTokenTextController,
                ),
                const Text('refresh token'),
                TextField(
                  controller: _refreshTokenTextController,
                ),
                const Text('test api results'),
                Text(_userInfo ?? ''),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Future<void> _endSession(
      {ExternalUserAgent externalUserAgent =
          ExternalUserAgent.asWebAuthenticationSession}) async {
    try {
      _setBusyState();
      await _appAuth.endSession(EndSessionRequest(
          idTokenHint: _idToken,
          postLogoutRedirectUrl: _postLogoutRedirectUrl,
          serviceConfiguration: _serviceConfiguration,
          externalUserAgent: externalUserAgent));
      _clearSessionInfo();
    } catch (e) {
      _handleError(e);
    } finally {
      _clearBusyState();
    }
  }

  void _clearSessionInfo() {
    setState(() {
      _codeVerifier = null;
      _nonce = null;
      _authorizationCode = null;
      _authorizationCodeTextController.clear();
      _accessToken = null;
      _accessTokenTextController.clear();
      _idToken = null;
      _idTokenTextController.clear();
      _refreshToken = null;
      _refreshTokenTextController.clear();
      _accessTokenExpirationTextController.clear();
      _userInfo = null;
    });
  }

  Future<void> _refresh() async {
    try {
      _setBusyState();
      final TokenResponse result = await _appAuth.token(TokenRequest(
          _clientId, _redirectUrl,
          refreshToken: _refreshToken, issuer: _issuer, scopes: _scopes));
      _processTokenResponse(result);
      await _testApi(result);
    } catch (e) {
      _handleError(e);
    } finally {
      _clearBusyState();
    }
  }

  Future<void> _exchangeCode() async {
    try {
      _setBusyState();
      final TokenResponse result = await _appAuth.token(TokenRequest(
          _clientId, _redirectUrl,
          authorizationCode: _authorizationCode,
          discoveryUrl: _discoveryUrl,
          codeVerifier: _codeVerifier,
          nonce: _nonce,
          scopes: _scopes));
      _processTokenResponse(result);
      await _testApi(result);
    } catch (e) {
      _handleError(e);
    } finally {
      _clearBusyState();
    }
  }

  Future<void> _signInWithNoCodeExchange() async {
    try {
      _setBusyState();
      final AuthorizationResponse result = await _appAuth.authorize(
        AuthorizationRequest(_clientId, _redirectUrl,
            discoveryUrl: _discoveryUrl, scopes: _scopes, loginHint: 'bob'),
      );
      _processAuthResponse(result);
    } catch (e) {
      _handleError(e);
    } finally {
      _clearBusyState();
    }
  }

  Future<void> _signInWithNoCodeExchangeAndGeneratedNonce() async {
    try {
      _setBusyState();
      final Random random = Random.secure();
      final String nonce =
          base64Url.encode(List<int>.generate(16, (_) => random.nextInt(256)));
      final AuthorizationResponse result = await _appAuth.authorize(
        AuthorizationRequest(_clientId, _redirectUrl,
            discoveryUrl: _discoveryUrl,
            scopes: _scopes,
            loginHint: 'bob',
            nonce: nonce),
      );
      _processAuthResponse(result);
    } catch (e) {
      _handleError(e);
    } finally {
      _clearBusyState();
    }
  }

  Future<void> _signInWithAutoCodeExchange(
      {ExternalUserAgent externalUserAgent =
          ExternalUserAgent.asWebAuthenticationSession}) async {
    try {
      _setBusyState();
      final AuthorizationTokenResponse result =
          await _appAuth.authorizeAndExchangeCode(
        AuthorizationTokenRequest(_clientId, _redirectUrl,
            serviceConfiguration: _serviceConfiguration,
            scopes: _scopes,
            externalUserAgent: externalUserAgent),
      );
      _processAuthTokenResponse(result);
      await _testApi(result);
    } catch (e) {
      _handleError(e);
    } finally {
      _clearBusyState();
    }
  }

  void _handleError(Object e) {
    if (e is FlutterAppAuthUserCancelledException) {
      setState(() {
        _error = 'The user cancelled the flow!';
      });
    } else if (e is FlutterAppAuthPlatformException) {
      setState(() {
        _error = e.platformErrorDetails.toString();
      });
    } else if (e is PlatformException) {
      setState(() {
        _error = 'Error\n\nCode: ${e.code}\nMessage: ${e.message}\n'
            'Details: ${e.details}';
      });
    } else {
      setState(() {
        _error = 'Error: $e';
      });
    }
  }

  void _clearBusyState() {
    setState(() {
      _isBusy = false;
    });
  }

  void _setBusyState() {
    setState(() {
      _error = '';
      _isBusy = true;
    });
  }

  void _processAuthTokenResponse(AuthorizationTokenResponse response) {
    setState(() {
      _accessToken = _accessTokenTextController.text = response.accessToken!;
      _idToken = _idTokenTextController.text = response.idToken!;
      _refreshToken = _refreshTokenTextController.text = response.refreshToken!;
      _accessTokenExpirationTextController.text =
          response.accessTokenExpirationDateTime!.toIso8601String();
    });
  }

  void _processAuthResponse(AuthorizationResponse response) {
    setState(() {
      _codeVerifier = response.codeVerifier;
      _nonce = response.nonce;
      _authorizationCode =
          _authorizationCodeTextController.text = response.authorizationCode!;
      _isBusy = false;
    });
  }

  void _processTokenResponse(TokenResponse response) {
    setState(() {
      _accessToken = _accessTokenTextController.text = response.accessToken!;
      _idToken = _idTokenTextController.text = response.idToken!;
      _refreshToken = _refreshTokenTextController.text = response.refreshToken!;
      _accessTokenExpirationTextController.text =
          response.accessTokenExpirationDateTime!.toIso8601String();
    });
  }

  Future<void> _testApi(TokenResponse? response) async {
    final http.Response httpResponse = await http.get(
        Uri.parse('https://demo.duendesoftware.com/api/test'),
        headers: <String, String>{'Authorization': 'Bearer $_accessToken'});
    setState(() {
      _userInfo = httpResponse.statusCode == 200 ? httpResponse.body : '';
      _isBusy = false;
    });
  }
}

以上代码展示了如何使用flutter_appauth插件进行OAuth2.0认证,并提供了多个按钮来测试不同的认证流程。


更多关于Flutter认证授权插件flutter_appauth的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter认证授权插件flutter_appauth的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,flutter_appauth 是一个用于在 Flutter 应用中实现 OAuth 2.0 和 OpenID Connect 认证的插件。以下是一个简单的代码示例,展示了如何使用 flutter_appauth 插件进行认证授权。

首先,确保你已经在 pubspec.yaml 文件中添加了 flutter_appauth 依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_appauth: ^x.y.z  # 请替换为最新版本号

然后,运行 flutter pub get 来获取依赖。

接下来是代码示例:

import 'package:flutter/material.dart';
import 'package:flutter_appauth/flutter_appauth.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: AuthScreen(),
    );
  }
}

class AuthScreen extends StatefulWidget {
  @override
  _AuthScreenState createState() => _AuthScreenState();
}

class _AuthScreenState extends State<AuthScreen> {
  final FlutterAppAuth _auth = FlutterAppAuth();

  Future<void> _authorizeAndExchangeToken() async {
    try {
      // 配置授权请求
      final AuthorizationTokenResponse result = await _auth.authorizeAndExchangeCode(
        AuthorizationRequest(
          clientId: 'YOUR_CLIENT_ID',
          redirectUri: Uri.parse('YOUR_REDIRECT_URI'), // 确保这个 URI 在你的授权服务器上注册
          endpoint: AuthorizationEndpoint(
            baseUrl: Uri.parse('https://YOUR_AUTHORIZATION_SERVER'),
            authorizationEndpoint: '/oauth/authorize',
            tokenEndpoint: '/oauth/token',
          ),
          scopes: ['read', 'write'], // 替换为你的实际作用域
        ),
      );

      // 打印返回的 token 信息
      print('Authorization Token Response: $result');

      // 你可以在这里处理 token,比如保存到本地或者发送到服务器
    } catch (e) {
      print('Error: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter AppAuth Example'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: _authorizeAndExchangeToken,
          child: Text('Authorize'),
        ),
      ),
    );
  }
}

在上面的代码中:

  1. FlutterAppAuth 插件被用于处理 OAuth 2.0 授权。
  2. AuthorizationRequest 包含了客户端 ID、重定向 URI、授权端点和请求的作用域。
  3. authorizeAndExchangeCode 方法会启动授权流程,并在用户完成授权后返回授权令牌响应(AuthorizationTokenResponse)。

注意事项

  • 请确保将 YOUR_CLIENT_IDYOUR_REDIRECT_URIhttps://YOUR_AUTHORIZATION_SERVER 替换为你实际的客户端 ID、重定向 URI 和授权服务器 URL。
  • 确保重定向 URI 在你的授权服务器上已经注册,并且与你在代码中使用的 URI 匹配。
  • 根据你的需求,可能需要调整作用域(scopes)和其他参数。

这个示例展示了基本的认证授权流程,你可以根据需要进行扩展和定制。

回到顶部