Flutter应用授权插件shanbe_flutter_appauth的使用

概述

flutter_appauth 是一个用于 Flutter 应用的身份验证和授权的插件。它通过桥接原生的 AppAuth(https://appauth.io)库来实现 OAuth 2.0 和 OpenID Connect 的认证流程。该插件支持 PKCE 扩展,这对于一些身份提供商来说是必需的。

注意事项:

  1. AndroidX 兼容性

    • 该插件要求应用使用 AndroidX。如果您在使用 Flutter 工具时创建了新的项目,可以通过添加 --androidx 参数来启用 AndroidX。
    • 如果您的 Android 应用无法使用 Chrome Custom Tabs,请确保更新插件、Android Studio、Gradle 分发版本以及 Android Gradle 插件。
  2. 身份提供商文档

    • 推荐查看您所使用的身份提供商的文档,以了解其支持的功能,例如如何登出、支持的 prompt 参数值等。

使用教程

以下是一个完整的示例,展示如何使用 flutter_appauth 插件进行身份验证。


示例代码

示例代码:example/lib/main.dart

import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_appauth/flutter_appauth.dart';

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

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool _isBusy = false;
  final FlutterAppAuth _appAuth = FlutterAppAuth();
  String? _codeVerifier;
  String? _authorizationCode;
  String? _refreshToken;
  String? _accessToken;
  final TextEditingController _authorizationCodeTextController =
      TextEditingController();
  final TextEditingController _accessTokenTextController =
      TextEditingController();
  final TextEditingController _accessTokenExpirationTextController =
      TextEditingController();

  final TextEditingController _idTokenTextController = TextEditingController();
  final TextEditingController _refreshTokenTextController =
      TextEditingController();
  String _userInfo = '';

  // 替换为您的客户端 ID 和重定向 URI
  final String _clientId = 'interactive.public';
  final String _redirectUrl = 'io.identityserver.demo:/oauthredirect';
  final String _issuer = 'https://demo.identityserver.io';
  final String _discoveryUrl =
      'https://demo.identityserver.io/.well-known/openid-configuration';
  final List<String> _scopes = [
    'openid',
    'profile',
    'email',
    'offline_access',
    'api'
  ];

  final AuthorizationServiceConfiguration _serviceConfiguration =
      const AuthorizationServiceConfiguration(
          'https://demo.identityserver.io/connect/authorize',
          'https://demo.identityserver.io/connect/token');

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: SingleChildScrollView(
          child: Column(
            children: <Widget>[
              Visibility(
                visible: _isBusy,
                child: const LinearProgressIndicator(),
              ),
              ElevatedButton(
                child: const Text('Sign in with no code exchange'),
                onPressed: _signInWithNoCodeExchange,
              ),
              ElevatedButton(
                child: const Text('Exchange code'),
                onPressed: _authorizationCode != null ? _exchangeCode : null,
              ),
              ElevatedButton(
                child: const Text('Sign in with auto code exchange'),
                onPressed: () => _signInWithAutoCodeExchange(),
              ),
              if (Platform.isIOS)
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: ElevatedButton(
                    child: const Text(
                      'Sign in with auto code exchange using ephemeral session (iOS only)',
                      textAlign: TextAlign.center,
                    ),
                    onPressed: () =>
                        _signInWithAutoCodeExchange(preferEphemeralSession: true),
                  ),
                ),
              ElevatedButton(
                child: const Text('Refresh token'),
                onPressed: _refreshToken != null ? _refresh : null,
              ),
              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> _refresh() async {
    try {
      _setBusyState();
      final TokenResponse? result = await _appAuth.token(TokenRequest(
          _clientId, _redirectUrl,
          refreshToken: _refreshToken,
          discoveryUrl: _discoveryUrl,
          scopes: _scopes));
      _processTokenResponse(result);
      await _testApi(result);
    } catch (_) {
      _clearBusyState();
    }
  }

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

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

      if (result != null) {
        _processAuthResponse(result);
      }
    } catch (_) {
      _clearBusyState();
    }
  }

  Future<void> _signInWithAutoCodeExchange(
      {bool preferEphemeralSession = false}) async {
    try {
      _setBusyState();

      final AuthorizationTokenResponse? result =
          await _appAuth.authorizeAndExchangeCode(
        AuthorizationTokenRequest(
          _clientId,
          _redirectUrl,
          serviceConfiguration: _serviceConfiguration,
          scopes: _scopes,
          preferEphemeralSession: preferEphemeralSession,
        ),
      );

      if (result != null) {
        _processAuthTokenResponse(result);
        await _testApi(result);
      }
    } catch (_) {
      _clearBusyState();
    }
  }

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

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

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

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

  void _processTokenResponse(TokenResponse? response) {
    setState(() {
      _accessToken = _accessTokenTextController.text = response!.accessToken!;
      _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.identityserver.io/api/test'),
        headers: {'Authorization': 'Bearer $_accessToken'});
    setState(() {
      _userInfo = httpResponse.statusCode == 200 ? httpResponse.body : '';
      _isBusy = false;
    });
  }
}

安装与配置

Android 配置

  1. android/app/build.gradle 文件中指定自定义的 Scheme:

    android {
        ...
        defaultConfig {
            ...
            manifestPlaceholders = [
                    'appAuthRedirectScheme': 'io.identityserver.demo'
            ]
        }
    }
    

    确保 <your_custom_scheme> 全部小写,避免大写字母可能导致的重定向问题。

  2. 如果目标 API 30 或更高(Android 11 及以上),在 AndroidManifest.xml 中添加以下内容:

    <queries>
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.BROWSABLE" />
            <data android:scheme="https" />
        </intent>
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.APP_BROWSER" />
            <data android:scheme="https" />
        </intent>
    </queries>
    

iOS 配置

  1. ios/Runner/Info.plist 文件中指定自定义的 Scheme:

    <key>CFBundleURLTypes</key>
    <array>
        <dict>
            <key>CFBundleTypeRole</key>
            <string>Editor</string>
            <key>CFBundleURLSchemes</key>
            <array>
                <string>io.identityserver.demo</string>
            </array>
        </dict>
    </array>

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

1 回复

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


shanbe_flutter_appauth 是一个用于在 Flutter 应用中实现 OAuth2 和 OpenID Connect 授权的插件。它基于 AppAuth 库,支持 Android 和 iOS 平台。使用这个插件,你可以轻松地在 Flutter 应用中集成第三方身份验证服务,如 Google、Facebook、GitHub 等。

安装插件

首先,你需要在 pubspec.yaml 文件中添加 shanbe_flutter_appauth 插件的依赖:

dependencies:
  flutter:
    sdk: flutter
  shanbe_flutter_appauth: ^0.0.1

然后运行 flutter pub get 来安装插件。

配置 Android 和 iOS

Android

android/app/src/main/AndroidManifest.xml 文件中,确保你已经添加了以下权限:

<uses-permission android:name="android.permission.INTERNET"/>

此外,你还需要在 android/app/build.gradle 文件中设置 minSdkVersion 至少为 21:

defaultConfig {
    minSdkVersion 21
    // 其他配置
}

iOS

ios/Runner/Info.plist 文件中,确保你已经添加了以下键值对:

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

使用插件

1. 导入插件

在你的 Dart 文件中导入 shanbe_flutter_appauth 插件:

import 'package:shanbe_flutter_appauth/shanbe_flutter_appauth.dart';

2. 初始化插件

创建一个 ShanbeFlutterAppAuth 实例:

final shanbeFlutterAppAuth = ShanbeFlutterAppAuth();

3. 配置授权请求

你需要配置授权请求的参数,包括 clientIdredirectUrlscopes 等。以下是一个示例:

final AuthorizationRequest request = AuthorizationRequest(
  'your_client_id',
  'your_redirect_url',
  discoveryUrl: 'https://your.discovery.url',
  scopes: ['openid', 'profile', 'email'],
);

4. 发起授权请求

使用 authorizeAndExchangeCode 方法发起授权请求并获取访问令牌:

try {
  final AuthorizationTokenResponse result = await shanbeFlutterAppAuth.authorizeAndExchangeCode(request);
  print('Access Token: ${result.accessToken}');
  print('ID Token: ${result.idToken}');
  print('Refresh Token: ${result.refreshToken}');
} catch (e) {
  print('Error: $e');
}

5. 刷新访问令牌

如果需要刷新访问令牌,可以使用 refreshToken 方法:

try {
  final TokenResponse result = await shanbeFlutterAppAuth.refreshToken(
    RefreshTokenRequest(
      'your_client_id',
      'your_refresh_token',
      'your_redirect_url',
      discoveryUrl: 'https://your.discovery.url',
    ),
  );
  print('New Access Token: ${result.accessToken}');
} catch (e) {
  print('Error: $e');
}

6. 注销

如果需要注销用户,可以使用 logout 方法:

try {
  await shanbeFlutterAppAuth.logout(
    EndSessionRequest(
      idTokenHint: 'your_id_token',
      postLogoutRedirectUrl: 'your_post_logout_redirect_url',
      discoveryUrl: 'https://your.discovery.url',
    ),
  );
  print('Logged out successfully');
} catch (e) {
  print('Error: $e');
}

示例代码

以下是一个完整的示例代码,展示了如何使用 shanbe_flutter_appauth 插件进行授权、刷新令牌和注销操作:

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

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

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

class AuthScreen extends StatefulWidget {
  [@override](/user/override)
  _AuthScreenState createState() => _AuthScreenState();
}

class _AuthScreenState extends State<AuthScreen> {
  final shanbeFlutterAppAuth = ShanbeFlutterAppAuth();

  Future<void> _authorize() async {
    final request = AuthorizationRequest(
      'your_client_id',
      'your_redirect_url',
      discoveryUrl: 'https://your.discovery.url',
      scopes: ['openid', 'profile', 'email'],
    );

    try {
      final result = await shanbeFlutterAppAuth.authorizeAndExchangeCode(request);
      print('Access Token: ${result.accessToken}');
      print('ID Token: ${result.idToken}');
      print('Refresh Token: ${result.refreshToken}');
    } catch (e) {
      print('Error: $e');
    }
  }

  Future<void> _refreshToken() async {
    try {
      final result = await shanbeFlutterAppAuth.refreshToken(
        RefreshTokenRequest(
          'your_client_id',
          'your_refresh_token',
          'your_redirect_url',
          discoveryUrl: 'https://your.discovery.url',
        ),
      );
      print('New Access Token: ${result.accessToken}');
    } catch (e) {
      print('Error: $e');
    }
  }

  Future<void> _logout() async {
    try {
      await shanbeFlutterAppAuth.logout(
        EndSessionRequest(
          idTokenHint: 'your_id_token',
          postLogoutRedirectUrl: 'your_post_logout_redirect_url',
          discoveryUrl: 'https://your.discovery.url',
        ),
      );
      print('Logged out successfully');
    } catch (e) {
      print('Error: $e');
    }
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Shanbe Flutter AppAuth'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(
              onPressed: _authorize,
              child: Text('Authorize'),
            ),
            ElevatedButton(
              onPressed: _refreshToken,
              child: Text('Refresh Token'),
            ),
            ElevatedButton(
              onPressed: _logout,
              child: Text('Logout'),
            ),
          ],
        ),
      ),
    );
  }
}
回到顶部