Flutter静默授权插件silentauth_sdk_flutter的使用

发布于 1周前 作者 ionicwang 来自 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和/redirectAPI文档
                 },
  "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> 添加到您的应用程序项目中:

  1. 依赖它。打开位于应用程序文件夹内的 <code>pubspec.yaml</code> 文件,并在dependencies部分下添加以下内容:
silentauth_sdk_flutter: ^x.y.z
  1. 安装它
    • 从终端运行:
    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

1 回复

更多关于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');
}
回到顶部
AI 助手
你好,我是IT营的 AI 助手
您可以尝试点击下方的快捷入口开启体验!