Flutter静默授权插件silentauth_sdk_flutter的使用
Flutter静默授权插件silentauth_sdk_flutter的使用
[![License][license-image]][license-url]
该SDK的唯一目的是在调用公共URL之前强制设备具备数据蜂窝连接,并将返回以下JSON响应:
- 成功 当数据连接已建立并且从URL端点接收到响应时。
{
"http_status": string, // 与URL相关的HTTP状态
"response_body" : { // 取决于HTTP状态是否为可选
... // 打开的URL的响应体
... // 有关/device_ip和/redirect的API文档
},
"debug" : {
"device_info": string,
"url_trace" : string
}
}
- 错误 当数据连接不可用或内部SDK错误发生时。
{
"error" : string,
"error_description": string
"debug": {
"device_info": string,
"url_trace" : string
}
}
潜在的错误代码有:sdk_no_data_connectivity
, sdk_connection_error
, sdk_redirect_error
, sdk_error
。
安装
要将包 <code>silentauth_sdk_flutter</code>
添加到您的应用程序项目中:
- 依赖它。打开位于应用程序文件夹内的
<code>pubspec.yaml</code>
文件,并在dependencies部分下添加以下内容:
silentauth_sdk_flutter: ^x.y.z
- 安装它
- 从终端运行:
flutter pub get
- 在Android Studio/IntelliJ中:点击顶部操作栏中的
<strong>Packages get</strong>
。 - 在VS Code中:点击顶部操作栏右侧的
<strong>Get Packages</strong>
。
兼容性
使用
设备是否适合静默认证?
import 'package:silentauth_sdk_flutter/silentauth_sdk_flutter.dart';
// ...
// 从后端获取带有覆盖范围的访问令牌
var token = ...
// 打开公共API端点/device_ip
SilentauthSdkFlutter sdk = SilentauthSdkFlutter();
try {
Map reach = await sdk.openWithDataCellularAndAccessToken(
"https://eu.api.silentauth.com/coverage/v0.1/device_ip", token, true);
if (reach.containsKey("error")) {
// 网络连接错误
} else if (reach.containsKey("http_status")) {
if (reach["http_status"] == 200 && reach["response_body"] != null) {
Map<dynamic, dynamic> body = reach["response_body"];
Coverage cv = Coverage.fromJson(body);
} else if (reach["status"] == 400 && reach["response_body"] != null) {
// 不支持的MNO,请参见${body.detail}
} else if (reach["status"] == 412 && reach["response_body"] != null) {
// 非移动IP,请参见${body.detail}
} else if (reach["response_body"] != null) {
// 其他错误,请参见${body.detail}
}
}
} catch (e) {
print(e);
}
如何打开检查URL
import 'package:silentauth_sdk_flutter/silentauth_sdk_flutter.dart';
// ...
SilentauthSdkFlutter sdk = SilentauthSdkFlutter();
Map result = await sdk.openWithDataCellular(checkUrl, false);
if (result.containsKey("error")) {
// 错误
} else if (result["http_status"] == 200 && result["response_body"] != null) {
if (result["response_body"].containsKey("error")) {
CheckErrorBody errorBody = CheckErrorBody.fromJson(body);
// 错误,请参见${body.error_description}
} else {
CheckSuccessBody successBody = CheckSuccessBody.fromJson(body);
// 将code、check_id和reference_id发送到后端以触发PATCH /checks/{check_id}
}
} else {
// 其他错误,请参见${body.detail}
}
示例Demo
嵌入式示例Demo位于 <code>example</code>
目录中,详见README。
以下是完整的示例代码:
/*
* MIT License
* Copyright (C) 2020 4Auth Limited. All rights reserved
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import 'dart:convert';
import 'dart:core';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:silentauth_sdk_flutter/silentauth_sdk_flutter.dart';
import 'src/http/mock_client.dart';
// 设置本地隧道的基本URL。
final String baseURL = "YOUR_LOCAL_TUNNEL_URL";
void main() {
runApp(PhoneCheckApp());
}
class PhoneCheckApp extends StatelessWidget {
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
title: 'SilentAuth Phone Check Sample',
theme: ThemeData(),
home: PhoneCheckHome(title: 'SilentAuth Flutter Sample App'),
);
}
}
class PhoneCheckHome extends StatefulWidget {
PhoneCheckHome({Key? key, required this.title}) : super(key: key);
final String title;
[@override](/user/override)
_PhoneCheckAppState createState() => _PhoneCheckAppState();
}
class _PhoneCheckAppState extends State<PhoneCheckHome> {
Future<CheckStatus>? _futurePhoneCheck;
String? _result = null;
final _formKey = GlobalKey<FormState>();
String? phoneNumber;
bool agreedToTerms = false;
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
title: 'SilentAuth Sample App',
theme: ThemeData(
scaffoldBackgroundColor: Colors.white,
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text('SilentAuth Flutter Sample App'),
),
body: bodyContainer(),
),
);
}
Container bodyContainer() {
return Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(8.0),
child: (_futurePhoneCheck == null) ? bodyForm() : buildFutureBuilder(),
);
}
Form bodyForm() {
return Form(
key: _formKey,
child: Scrollbar(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Text('SilentAuth', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 30),),
const SizedBox(height: 24),
validatingTextField(),
const SizedBox(height: 24),
validatingFormField(),
const SizedBox(height: 24),
verifyButton(),
Text((_result == null) ? "" : "Results $_result")
],
),
),
),
);
}
FutureBuilder<CheckStatus> buildFutureBuilder() {
return FutureBuilder<CheckStatus>(
future: _futurePhoneCheck,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
_result = 'Match status: ${snapshot.data!.match}';
} else if (snapshot.hasError) {
_result = 'Error:${snapshot.error}';
}
return bodyForm();
} else if (snapshot.connectionState == ConnectionState.active ||
snapshot.connectionState == ConnectionState.waiting ||
snapshot.connectionState == ConnectionState.none) {
print("-->");
}
return CircularProgressIndicator();
},
);
}
// 一个文本字段,验证文本是否为电话号码。
TextFormField validatingTextField() {
return TextFormField(
// autofocus: true,
initialValue: (phoneNumber == null) ? null : phoneNumber,
keyboardType: TextInputType.phone,
textInputAction: TextInputAction.next,
validator: (value) {
if (value!.isEmpty) {
return 'Please enter a phone number.';
}
RegExp exp = RegExp(r"^(?:[+0][1-9])?[0-9]{10,12}$");
if (exp.hasMatch(value)) {
return null;
}
return 'Not a valid phone number';
},
decoration: const InputDecoration(
filled: true,
hintText: 'e.g. +447830305594',
labelText: 'Enter phone number',
),
onChanged: (value) {
phoneNumber = value;
},
);
}
// 一个自定义表单字段,要求用户勾选复选框。
FormField<bool> validatingFormField() {
return FormField<bool>(
initialValue: agreedToTerms,
validator: (value) {
if (value == false) {
return 'You must agree to the terms of service.';
}
return null;
},
builder: (formFieldState) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Checkbox(
value: agreedToTerms,
onChanged: (value) {
// 当复选框的值更改时,
// 更新FormFieldState以便重新验证表单。
formFieldState.didChange(value);
setState(() {
agreedToTerms = value!;
});
},
),
Text(
'I agree to the terms of service.',
style: Theme.of(context).textTheme.subtitle1,
),
],
),
if (!formFieldState.isValid)
Text(
formFieldState.errorText ?? "",
style: Theme.of(context)
.textTheme
.caption!
.copyWith(color: Theme.of(context).errorColor),
),
],
);
},
);
}
Widget verifyButton() {
return TextButton(
child: const Text('Verify my phone number'),
style: TextButton.styleFrom(
primary: Colors.white,
backgroundColor: Colors.blue,
),
onPressed: () {
// 验证表单通过获取GlobalKey的FormState并调用其validate()方法。
var valid = _formKey.currentState!.validate();
if (!valid) {
return;
}
if (phoneNumber != null) {
FocusScope.of(context).unfocus();
setState(() {
_futurePhoneCheck = executeFlow(phoneNumber!);
});
}
},
);
}
// 获取覆盖范围访问令牌
Future<TokenResponse> getCoverageAccessToken() async {
final response = await http.get(
Uri.parse('$baseURL/coverage-access-token'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
);
if (response.statusCode == 200) {
return TokenResponse.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to get coverage access token: No access token');
}
}
// 平台消息是异步的,因此我们在异步方法中初始化。
Future<CheckStatus> executeFlow(String phoneNumber) async {
print("[Reachability] - Start");
var canMoveToNextStep = false;
var tokenResponse = await getCoverageAccessToken();
var token = tokenResponse.token;
SilentauthSdkFlutter sdk = SilentauthSdkFlutter();
try {
Map reach = await sdk.openWithDataCellularAndAccessToken(
"https://eu.api.silentauth.com/coverage/v0.1/device_ip", token, true);
print("isReachable = $reach");
if (reach.containsKey("error")) {
throw Exception(
'Status = ${reach["error"]} - ${reach["error_description"]}');
} else if (reach.containsKey("http_status")) {
if (reach["http_status"] == 200 && reach["response_body"] != null) {
Map<dynamic, dynamic> body = reach["response_body"];
Coverage cv = Coverage.fromJson(body);
print("body ${cv.networkName}");
if (cv.products != null) print("product ${cv.products![0]}");
// everything is fine
canMoveToNextStep = true;
} else if (reach["http_status"] == 400 && reach["response_body"] != null) {
ApiError body = ApiError.fromJson(reach["response_body"]);
print("[Is Reachable 400] ${body.detail}");
} else if (reach["http_status"] == 412 && reach["response_body"] != null) {
ApiError body = ApiError.fromJson(reach["response_body"]);
print("[Is Reachable 412] ${body.detail}");
} else if (reach["response_body"] != null) {
ApiError body = ApiError.fromJson(reach["response_body"]);
print("[Is Reachable ] ${body.detail}");
}
}
if (canMoveToNextStep) {
print("Moving on with Creating PhoneCheck...");
final response = await http.post(
Uri.parse('$baseURL/v0.2/phone-check'),
headers: <String, String>{
'content-type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'phone_number': phoneNumber,
}),
);
print("[PhoneCheck] - Received response");
if (response.statusCode == 200) {
PhoneCheck checkDetails =
PhoneCheck.fromJson(jsonDecode(response.body));
Map result = await sdk.openWithDataCellular(checkDetails.url, false);
print("openWithDataCellular Results -> $result");
if (result.containsKey("error")) {
print(result);
throw Exception('Error in openWithDataCellular: $result');
} else if (result["http_status"] == 200 && result["response_body"] != null) {
if (result["response_body"].containsKey("error")) {
CheckErrorBody body = CheckErrorBody.fromJson(result["response_body"]);
print("CheckErrorBody: ${body.description}");
throw Exception('openWithDataCellular error ${body.description}');
} else {
CheckSuccessBody body = CheckSuccessBody.fromJson(result["response_body"]);
print('CheckSuccessBody $body');
try {
return exchangeCode(body.checkId, body.code, body.referenceId);
} catch (error) {
print("Error retrieving check result $error");
throw Exception("Error retrieving check result");
}
}
} else {
ApiError body = ApiError.fromJson(result["response_body"]);
print("ApiError ${body.detail}");
throw Exception("Error: ${body.detail}");
}
} else {
throw Exception('Failed to create phone check');
}
} else {
print("isReachable parsing error");
throw Exception('reachability failed');
}
} on PlatformException catch (e) {
print("isReachable Error: ${e.toString()}");
throw Exception('reachability failed');
}
}
}
Future<CheckStatus> exchangeCode(
String checkID, String code, String? referenceID) async {
var body = jsonEncode(<String, String>{
'code': code,
'check_id': checkID,
'reference_id': (referenceID != null) ? referenceID : ""
});
final response = await http.post(
Uri.parse('$baseURL/v0.2/phone-check/exchange-code'),
body: body,
headers: <String, String>{
'content-type': 'application/json; charset=UTF-8',
},
);
print("response request ${response.request}");
if (response.statusCode == 200) {
CheckStatus exchangeCheckRes =
CheckStatus.fromJson(jsonDecode(response.body));
print("Exchange Check Result $exchangeCheckRes");
if (exchangeCheckRes.match) {
print("✅ successful PhoneCheck match");
} else {
print("❌ failed PhoneCheck match");
}
return exchangeCheckRes;
} else {
throw Exception('Failed to exchange Code');
}
}
class TokenResponse {
final String token;
final String url;
TokenResponse({required this.token, required this.url});
factory TokenResponse.fromJson(Map<dynamic, dynamic>json) {
return TokenResponse(
token: json['token'],
url: json['url']
);
}
}
class PhoneCheck {
final String id;
final String url;
PhoneCheck({required this.id, required this.url});
factory PhoneCheck.fromJson(Map<dynamic, dynamic> json) {
return PhoneCheck(
id: json['check_id'],
url: json['check_url'],
);
}
}
class CheckStatus {
final String id;
bool match = false;
CheckStatus({required this.id, required this.match});
factory CheckStatus.fromJson(Map<dynamic, dynamic> json) {
return CheckStatus(
id: json['check_id'],
match: json['match'] == null ? false : json['match'],
);
}
}
// 设置一个模拟HTTP客户端。
final http.Client httpClient = MockClient();
Future<String> getMockData() {
return Future.delayed(Duration(seconds: 2), () {
return "PhoneCheck Mock data";
// throw Exception("Custom Error");
});
}
Future<String> asyncMockData() async {
await Future.delayed(Duration(seconds: 10));
return Future.value("PhoneCheck Mock data");
}
更多关于Flutter静默授权插件silentauth_sdk_flutter的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter静默授权插件silentauth_sdk_flutter的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
silentauth_sdk_flutter
是一个用于在 Flutter 应用中实现静默授权的插件。静默授权是一种在用户无感知的情况下进行身份验证的方式,通常用于后台任务或需要频繁访问用户数据的场景。
以下是如何在 Flutter 项目中使用 silentauth_sdk_flutter
插件的步骤:
1. 添加依赖
首先,在 pubspec.yaml
文件中添加 silentauth_sdk_flutter
插件的依赖:
dependencies:
flutter:
sdk: flutter
silentauth_sdk_flutter: ^1.0.0 # 请使用最新版本
然后运行 flutter pub get
来获取依赖。
2. 初始化 SDK
在应用启动时,初始化 silentauth_sdk_flutter
。通常可以在 main.dart
文件中进行初始化:
import 'package:silentauth_sdk_flutter/silentauth_sdk_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 初始化 SDK
await SilentAuthSdkFlutter.initialize(
clientId: 'YOUR_CLIENT_ID', // 替换为你的客户端ID
clientSecret: 'YOUR_CLIENT_SECRET', // 替换为你的客户端密钥
redirectUri: 'YOUR_REDIRECT_URI', // 替换为你的重定向URI
);
runApp(MyApp());
}
3. 请求静默授权
在需要静默授权的地方,调用 requestSilentAuth
方法:
import 'package:silentauth_sdk_flutter/silentauth_sdk_flutter.dart';
Future<void> performSilentAuth() async {
try {
final authResponse = await SilentAuthSdkFlutter.requestSilentAuth();
print('Access Token: ${authResponse.accessToken}');
print('Refresh Token: ${authResponse.refreshToken}');
print('Expires In: ${authResponse.expiresIn}');
} catch (e) {
print('Failed to perform silent auth: $e');
}
}
4. 处理授权结果
requestSilentAuth
方法会返回一个包含访问令牌、刷新令牌和过期时间等信息的 AuthResponse
对象。你可以根据这些信息来进行后续的 API 调用或其他操作。
5. 错误处理
在静默授权过程中可能会出现各种错误,例如网络问题、授权失败等。确保在 try-catch
块中捕获并处理这些错误。
6. 刷新令牌
如果访问令牌过期,可以使用刷新令牌来获取新的访问令牌:
Future<void> refreshToken() async {
try {
final authResponse = await SilentAuthSdkFlutter.refreshToken(refreshToken: 'YOUR_REFRESH_TOKEN');
print('New Access Token: ${authResponse.accessToken}');
print('New Refresh Token: ${authResponse.refreshToken}');
print('New Expires In: ${authResponse.expiresIn}');
} catch (e) {
print('Failed to refresh token: $e');
}
}
7. 注销
在用户注销时,可以调用 logout
方法来清除所有的授权信息:
Future<void> logout() async {
await SilentAuthSdkFlutter.logout();
print('User logged out');
}