Flutter网页认证插件authwebview的使用

Flutter网页认证插件authwebview的使用

AuthWebView 是一个用于在 Flutter 应用中处理 OAuth 认证流程的插件。该插件最初由 Rapider.ai 开发,并已作为开源解决方案提供给 Flutter 社区。

特性

  • 支持 OAuth 2.0 认证流程并包含 PKCE(Proof Key for Code Exchange)
  • 可定制的 OAuth 提供商
  • 遵循内置的安全最佳实践
  • 安全的令牌存储
  • 处理授权码交换为访问令牌
  • 提供简单直观的 API
  • 自定义认证过程中的加载小部件
  • 错误处理和认证错误回调
  • Rapider.ai 中经过生产测试

入门指南

前提条件

  • 已安装 Flutter SDK
  • 已设置 Flutter 项目

安装

pubspec.yaml 文件中添加以下依赖:

dependencies:
  authwebview: ^1.0.4

然后运行 flutter pub get 来安装包。

使用

在 Dart 代码中导入 authwebview 包:

import 'package:authwebview/authwebview.dart';

定义一个 OAuth 提供商:

final provider = OAuthProvider(
  name: 'Google',
  discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
  clientId: 'your-client-id',
  redirectUrl: 'your-redirect-url',
  scopes: ['openid', 'profile', 'email'],
);

执行 OAuth 流程:

final result = await OAuthService.performOAuthFlow(
  context,
  provider,
  loadingWidget: CircularProgressIndicator(),
);

if (result != null) {
  // 认证成功,访问令牌在结果中可用
  print(result.accessToken);
} else {
  // 认证失败或用户取消了操作
}

处理认证错误:

final result = await OAuthService.performOAuthFlow(
  context,
  provider,
  loadingWidget: CircularProgressIndicator(),
  onError: (error) {
    // 处理认证错误
    print('Authentication error: $error');
  },
);

安全最佳实践

PKCE 实现

该插件默认实现 PKCE,以增强安全性。PKCE 可防止授权码拦截攻击。

令牌存储

  • 不要将令牌存储在 SharedPreferences 或本地存储中,除非加密
  • 使用 Flutter Secure Storage 或平台特定的安全存储解决方案
  • 正确实现令牌刷新机制

WebView 安全

  • 总是验证重定向 URL
  • 实施适当的 state 验证
  • 登出后清除 WebView 缓存和 cookie

其他建议

  • 实施适当的 SSL/TLS 证书验证
  • 使用合适的超时值
  • 对令牌刷新实施速率限制
  • 定期进行安全审计

API 参考

OAuthProvider

表示 OAuth 提供商配置。

属性 类型 描述
name String OAuth 提供商的名称
discoveryUrl String 提供商的 OpenID Connect 发现文档的 URL
clientId String OAuth 应用程序的客户端 ID
redirectUrl String OAuth 应用程序的重定向 URL
scopes List 认证过程中请求的范围列表

AuthService

提供执行 OAuth 认证流程的方法。

方法 描述
performOAuthFlow 在 webview 中启动 OAuth 认证流程
logout 执行已认证用户的登出过程
getAuthorizationUrl 获取 OAuth 提供商的授权 URL
handleRedirect 在成功认证后的重定向 URL 处理

AuthorizationTokenResponse

表示包含授权令牌的响应。

属性 类型 描述
accessToken String? 用于进行身份验证请求的访问令牌
refreshToken String? 用于获取新访问令牌的刷新令牌
accessTokenExpirationDateTime DateTime? 访问令牌的过期日期和时间
idToken String? 包含用户信息的 ID 令牌
tokenType String? 访问令牌的类型(例如,Bearer)
scopes List 授权令牌授予的范围列表
authorizationAdditionalParameters Map<String, dynamic>? 与令牌一起返回的其他参数

错误处理

该插件通过自定义异常和 performOAuthFlow 方法中的 onError 回调提供错误处理:

try {
  final result = await OAuthService.performOAuthFlow(
    context,
    provider,
    onError: (error) {
      print('Authentication error: $error');
    },
  );
} catch (e) {
  if (e is OAuthException) {
    print('OAuth Error: ${e.message}');
    print('Error Code: ${e.code}');
  }
}

示例

一个完整的示例应用程序展示了如何使用此插件,可以在 这里 查看。

贡献

欢迎贡献!如果你发现了一个错误或想要添加一个功能,请创建一个问题或提交一个拉取请求。

许可证

该项目根据 MIT 许可证授权,详情请参阅 许可证文件


下面是完整的示例代码:

import 'package:authwebview/authwebview.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'dart:convert';

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

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

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'AuthWebView Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

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

  [@override](/user/override)
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  AuthorizationTokenResponse? _tokenResponse;
  bool _isLoading = true;
  final storage = const FlutterSecureStorage();

  final List<OAuthProvider> _providers = [
    // Example Google provider
    const OAuthProvider(
      name: 'Google',
      discoveryUrl:
          'https://accounts.google.com/.well-known/openid-configuration',
      clientId: 'your-client-id',
      redirectUrl: 'com.example.app:/oauth2callback',
      scopes: ['openid', 'profile', 'email'],
    ),
    // Example Microsoft provider
    const OAuthProvider(
      name: 'Microsoft',
      discoveryUrl:
          'https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration',
      clientId: 'your-client-id',
      redirectUrl: 'com.example.app:/oauth2callback',
      scopes: ['openid', 'profile', 'email', 'offline_access'],
    ),
  ];

  [@override](/user/override)
  void initState() {
    super.initState();
    _loadSavedTokens();
  }

  Future<void> _loadSavedTokens() async {
    try {
      final tokenJson = await storage.read(key: 'oauth_tokens');
      if (tokenJson != null) {
        setState(() {
          _tokenResponse = AuthorizationTokenResponse.fromJson(
            json.decode(tokenJson),
          );
        });
      }
    } catch (e) {
      debugPrint('Error loading saved tokens: $e');
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _saveTokens(AuthorizationTokenResponse tokens) async {
    try {
      await storage.write(
        key: 'oauth_tokens',
        value: json.encode(tokens.toJson()),
      );
    } catch (e) {
      debugPrint('Error saving tokens: $e');
    }
  }

  Future<void> _clearTokens() async {
    try {
      await storage.delete(key: 'oauth_tokens');
    } catch (e) {
      debugPrint('Error clearing tokens: $e');
    }
  }

  Future<void> _login(OAuthProvider provider) async {
    try {
      final result = await OAuthService.performOAuthFlow(
        context,
        provider,
        loadingWidget: const Center(
          child: CircularProgressIndicator(),
        ),
        backgroundColor:
            Theme.of(context).colorScheme.surface, // background -> surface
      );

      if (result != null) {
        setState(() {
          _tokenResponse = result;
        });
        await _saveTokens(result);
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Login failed: ${e.toString()}'),
            backgroundColor: Theme.of(context).colorScheme.error,
          ),
        );
      }
    }
  }

  Future<void> _logout(OAuthProvider provider) async {
    if (_tokenResponse?.idToken == null) {
      setState(() {
        _tokenResponse = null;
      });
      await _clearTokens();
      return;
    }

    try {
      final success = await OAuthService.logout(
        provider,
        _tokenResponse!.idToken!,
      );

      if (success) {
        setState(() {
          _tokenResponse = null;
        });
        await _clearTokens();
      } else {
        throw Exception('Logout failed');
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Logout failed: ${e.toString()}'),
            backgroundColor: Theme.of(context).colorScheme.error,
          ),
        );
      }
    }
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    if (_isLoading) {
      return const Scaffold(
        body: Center(
          child: CircularProgressIndicator(),
        ),
      );
    }

    return Scaffold(
      appBar: AppBar(
        title: const Text('AuthWebView Example'),
        backgroundColor: Theme.of(context).colorScheme.primaryContainer,
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            if (_tokenResponse == null) ...[
              const Text(
                'Choose a provider to login:',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              ),
              const SizedBox(height: 20),
              ..._providers.map(
                (provider) => Padding(
                  padding: const EdgeInsets.only(bottom: 10),
                  child: ElevatedButton(
                    onPressed: () => _login(provider),
                    child: Text('Login with ${provider.name}'),
                  ),
                ),
              ),
            ] else ...[
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const Text(
                        'Authentication Successful!',
                        style: TextStyle(
                          fontSize: 20,
                          fontWeight: FontWeight.bold,
                          color: Colors.green,
                        ),
                      ),
                      const SizedBox(height: 20),
                      Text('Access Token: ${_tokenResponse!.accessToken}'),
                      if (_tokenResponse!.refreshToken != null) ...[
                        const SizedBox(height: 10),
                        Text('Refresh Token: ${_tokenResponse!.refreshToken}'),
                      ],
                      if (_tokenResponse!.accessTokenExpirationDateTime !=
                          null) ...[
                        const SizedBox(height: 10),
                        Text(
                          'Expires: ${_tokenResponse!.accessTokenExpirationDateTime}',
                        ),
                      ],
                      const SizedBox(height: 20),
                      ElevatedButton(
                        onPressed: () => _logout(_providers.first),
                        style: ElevatedButton.styleFrom(
                          backgroundColor: Theme.of(context).colorScheme.error,
                          foregroundColor: Theme.of(context).colorScheme.onError,
                        ),
                        child: const Text('Logout'),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ],
        ),
      ),
    );
  }
}

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

1 回复

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


authwebview 是 Flutter 中一个用于处理网页认证的插件,通常用于 OAuth、OpenID Connect 或其他基于 Web 的认证流程。它允许你在 Flutter 应用中嵌入一个 WebView,用户可以在其中进行登录或授权操作,然后将认证结果返回给 Flutter 应用。

以下是如何在 Flutter 项目中使用 authwebview 插件的基本步骤:

1. 添加依赖

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

dependencies:
  flutter:
    sdk: flutter
  authwebview: ^1.0.0  # 请使用最新版本

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

2. 导入插件

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

import 'package:authwebview/authwebview.dart';

3. 使用 AuthWebView

你可以使用 AuthWebView 类来创建一个 WebView,并处理认证流程。以下是一个简单的示例:

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

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

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

class AuthWebViewExample extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AuthWebView Example'),
      ),
      body: AuthWebView(
        initialUrl: 'https://your-authentication-url.com',
        redirectUri: 'https://your-redirect-uri.com',
        onCallback: (Uri uri) {
          // 处理认证回调
          print('Callback Uri: $uri');
          // 你可以在这里解析 Uri 并提取 token 或其他信息
        },
        onError: (String error) {
          // 处理错误
          print('Error: $error');
        },
      ),
    );
  }
}
回到顶部