Flutter应用授权插件shanbe_flutter_appauth的使用
概述
flutter_appauth
是一个用于 Flutter 应用的身份验证和授权的插件。它通过桥接原生的 AppAuth(https://appauth.io)库来实现 OAuth 2.0 和 OpenID Connect 的认证流程。该插件支持 PKCE 扩展,这对于一些身份提供商来说是必需的。
注意事项:
-
AndroidX 兼容性:
- 该插件要求应用使用 AndroidX。如果您在使用 Flutter 工具时创建了新的项目,可以通过添加
--androidx
参数来启用 AndroidX。 - 如果您的 Android 应用无法使用 Chrome Custom Tabs,请确保更新插件、Android Studio、Gradle 分发版本以及 Android Gradle 插件。
- 该插件要求应用使用 AndroidX。如果您在使用 Flutter 工具时创建了新的项目,可以通过添加
-
身份提供商文档:
- 推荐查看您所使用的身份提供商的文档,以了解其支持的功能,例如如何登出、支持的
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 配置
-
在
android/app/build.gradle
文件中指定自定义的 Scheme:android { ... defaultConfig { ... manifestPlaceholders = [ 'appAuthRedirectScheme': 'io.identityserver.demo' ] } }
确保
<your_custom_scheme>
全部小写,避免大写字母可能导致的重定向问题。 -
如果目标 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 配置
-
在
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 回复