Skip to content

Latest commit

 

History

History
1033 lines (758 loc) · 45.4 KB

File metadata and controls

1033 lines (758 loc) · 45.4 KB

六、构建人工智能认证系统

认证是任何应用中最突出的功能之一,无论它是本机移动软件还是网站,并且自从保护数据的需求以及与机密有关的隐私需求开始以来,认证一直是一个活跃的领域。 在互联网上共享的数据。 在本章中,我们将从基于 Firebase 的简单登录到应用开始,然后逐步改进以包括基于人工智能(AI)的认证置信度指标和 Google 的 ReCaptcha。 所有这些认证方法均以深度学习为核心,并提供了一种在移动应用中实现安全性的最新方法。

在本章中,我们将介绍以下主题:

  • 一个简单的登录应用
  • 添加 Firebase 认证
  • 了解用于认证的异常检测
  • 用于认证用户的自定义模型
  • 实现 ReCaptcha 来避免垃圾邮件
  • 在 Flutter 中部署模型

技术要求

对于移动应用,需要具有 Flutter 的 Visual Studio Code 和 Dart 插件以及 Firebase Console

GitHub 网址

一个简单的登录应用

我们将首先创建一个简单的认证应用,该应用使用 Firebase 认证对用户进行认证,然后再允许他们进入主屏幕。 该应用将允许用户输入其电子邮件和密码来创建一个帐户,然后使他们随后可以使用此电子邮件和密码登录。

以下屏幕快照显示了应用的完整流程:

该应用的小部件树如下:

现在让我们详细讨论每个小部件的实现。

创建 UI

让我们从创建应用的登录屏幕开始。 用户界面UI)将包含两个TextFormField来获取用户的电子邮件 ID 和密码,RaisedButton进行注册/登录,以及FlatButton进行注册和登录操作之间的切换。

以下屏幕快照标记了将用于应用的第一个屏幕的小部件:

现在让我们创建应用的 UI,如下所示:

  1. 我们首先创建一个名为signup_signin_screen.dart的新 dart 文件。 该文件包含一个有状态的小部件– SignupSigninScreen
  2. 第一个屏幕中最上面的窗口小部件是TextField,用于获取用户的邮件 ID。 _createUserMailInput()方法可帮助我们构建窗口小部件:
 Widget _createUserMailInput() {
  return Padding(
     padding: const EdgeInsets.fromLTRB(0.0, 100.0, 0.0, 0.0),
     child: new TextFormField(
       maxLines: 1,
       keyboardType: TextInputType.emailAddress,
       autofocus: false,
       decoration: new InputDecoration(
           hintText: 'Email',
           icon: new Icon(
             Icons.mail,
             color: Colors.grey,
           )),
       validator: (value) => value.isEmpty ? 'Email can\'t be empty' : null,
       onSaved: (value) => _usermail = value.trim(),
     ),
   );
 }

首先,我们使用EdgeInsets.fromLTRB()为小部件提供了填充。 这有助于我们在四个基本方向的每个方向(即左,上,右和下)上创建具有不同值的偏移量。 接下来,我们使用maxLines(输入的最大行数)创建了TextFormField,其值为1作为子级,它接收用户的电子邮件地址。 另外,根据输入类型TextInputType.emailAddress,我们指定了将在属性keyboardType中使用的键盘类型。 然后,将autoFocus设置为false。 然后,我们在装饰属性中使用InputDecoration提供hintText "Email"和图标Icons.mail。 为了确保用户在没有输入电子邮件地址或密码的情况下不要尝试登录,我们添加了一个验证器。 当尝试使用空字段登录时,将显示警告“电子邮件不能为空”。 最后,我们通过使用trim()删除所有尾随空格来修剪输入的值,然后将输入的值存储在_usermail字符串变量中。

  1. 与“步骤 2”中的TextField相似,我们定义了下一个方法_createPasswordInput(),以创建用于输入密码的TextFormField()
Widget _createPasswordInput() {
   return Padding(
     padding: const EdgeInsets.fromLTRB(0.0, 15.0, 0.0, 0.0),
     child: new TextFormField(
       maxLines: 1,
       obscureText: true,
       autofocus: false,
       decoration: new InputDecoration(
           hintText: 'Password',
           icon: new Icon(
             Icons.lock,
             color: Colors.grey,
           )),
       validator: (value) => value.isEmpty ? 'Password can\'t be empty' : null,
       onSaved: (value) => _userpassword = value.trim(),
     ),
   );
 }

我们首先使用EdgeInsets.fromLTRB()在所有四个基本方向上提供填充,以在顶部提供15.0的偏移量。 接下来,我们创建一个TextFormField,其中maxLines1,并将obscureText设置为true,将autofocus设置为falseobscureText用于隐藏正在键入的文本。 我们使用InputDecoration提供hintText密码和一个灰色图标Icons.lock。 为确保文本字段不为空,使用了一个验证器,当传递空值时,该警告器会发出警告Password can't be empty,即用户尝试在不输入密码的情况下登录/注册。 最后,trim()用于删除所有尾随空格,并将密码存储在_userpassword字符串变量中。

  1. 接下来,我们在_SignupSigninScreenState外部声明FormMode枚举,该枚举在两种模式SIGNINSIGNUP之间运行,如以下代码片段所示:
enum FormMode { SIGNIN, SIGNUP }

我们将对该按钮使用此枚举,该按钮将使用户既可以登录又可以注册。 这将帮助我们轻松地在两种模式之间切换。 枚举是一组用于表示常量值的标识符。

使用enum关键字声明枚举类型。 在enum内部声明的每个标识符都代表一个整数值; 例如,第一标识符具有值0,第二标识符具有值1。 默认情况下,第一个标识符的值为0

  1. 让我们定义一个_createSigninButton()方法,该方法返回按钮小部件以使用户注册并登录:
 Widget _createSigninButton() {
   return new Padding(
       padding: EdgeInsets.fromLTRB(0.0, 45.0, 0.0, 0.0),
       child: SizedBox(
         height: 40.0,
         child: new RaisedButton(
           elevation: 5.0,
           shape: new RoundedRectangleBorder(borderRadius: new BorderRadius.circular(30.0)),
           color: Colors.blue,
           child: _formMode == FormMode.SIGNIN
               ? new Text('SignIn',
                   style: new TextStyle(fontSize: 20.0, color: Colors.white))
               : new Text('Create account',
                   style: new TextStyle(fontSize: 20.0, color: Colors.white)),
           onPressed: _signinSignup,
         ),
       ));
 }

我们从Padding开始,将45.0的按钮offset置于顶部,然后将SizedBox40.0height作为子项,并将RaisedButton作为其子项。 使用RoundedRectangleBorder()为凸起的按钮赋予圆角矩形形状,其边框半径为30.0,颜色为blue。 作为子项添加的按钮的文本取决于_formMode的当前值。 如果_formMode的值(FormMode枚举的一个实例)为FormMode.SIGNIN,则按钮显示SignIn,否则创建帐户。 按下按钮时将调用_signinSignup方法,该方法将在后面的部分中介绍。

  1. 现在,我们将第四个按钮添加到屏幕上,以使用户在SIGNINSIGNUP表单模式之间切换。 我们定义返回FlatButton_createSigninSwitchButton()方法,如下所示:
 Widget _createSigninSwitchButton() {
   return new FlatButton(
     child: _formMode == FormMode.SIGNIN
         ? new Text('Create an account',
             style: new TextStyle(fontSize: 18.0, fontWeight: FontWeight.w300))
         : new Text('Have an account? Sign in',
             style:
                 new TextStyle(fontSize: 18.0, fontWeight: FontWeight.w300)),
     onPressed: _formMode == FormMode.SIGNIN
         ? _switchFormToSignUp
         : _switchFormToSignin,
   );
 }

如果_formMode的当前值为SIGNIN并按下按钮,则应更改为SIGNUP并显示Create an account。 否则,如果_formModeSIGNUP作为其当前值,并且按下按钮,则该值应切换为由文本Have an account? Sign in表示的SIGNIN。 使用三元运算符创建RaisedButtonText子级时,添加了在文本之间切换的逻辑。 onPressed属性使用非常相似的逻辑,该逻辑再次检查_formMode的值以在模式之间切换并使用_switchFormToSignUp_switchFormToSignin方法更新_formMode的值。 我们将在“步骤 7”和 8 中定义_switchFormToSignUp_switchFormToSignin方法。

  1. 现在,我们定义_switchFormToSignUp()如下:
 void _switchFormToSignUp() {
   _formKey.currentState.reset();
   setState(() {
     _formMode = FormMode.SIGNUP;
   });
 }

此方法重置_formMode的值并将其更新为FormMode.SIGNUP。 更改setState()内部的值有助于通知框架该对象的内部状态已更改,并且 UI 可能需要更新。

  1. 我们以与_switchFormToSignUp()非常相似的方式定义_switchFormToSignin()
 void _switchFormToSignin() {
   _formKey.currentState.reset();
   setState(() {
     _formMode = FormMode.SIGNIN;
   });
 }

此方法重置_formMode的值并将其更新为FormMode.SIGNIN。 更改setState()内部的值有助于通知框架该对象的内部状态已更改,并且 UI 可能需要更新。

  1. 现在,让我们将所有屏幕小部件Email TextFieldPassword TextFiedSignIn ButtonFlatButton切换为在单个容器中进行注册和登录。 为此,我们定义了一种方法createBody(),如下所示:
 Widget _createBody(){
   return new Container(
       padding: EdgeInsets.all(16.0),
       child: new Form(
         key: _formKey,
         child: new ListView(
           shrinkWrap: true,
           children: <Widget>[
             _createUserMailInput(),
             _createPasswordInput(),
             _createSigninButton(),
             _createSigninSwitchButton(),
             _createErrorMessage(),
           ],
         ),
       )
    );
 }

此方法返回一个以Form作为子元素的新Container并为其填充16.0。 表单使用_formKey作为其键,并添加ListView作为其子级。 ListView的元素是我们在前述方法中创建的用于添加TextFormFieldsButtons的小部件。 shrinkWrap设置为true,以确保ListView仅占用必要的空间,并且不会尝试扩展和填充整个屏幕

Form类用于将多个FormFields一起分组和验证。 在这里,我们使用Form将两个TextFormFields,一个RaisedButton和一个FlatButton包装在一起。

  1. 这里要注意的一件事是,由于进行认证,因此用户最终将成为网络操作,因此可能需要一些时间来发出网络请求。 在此处添加进度条可防止在进行网络操作时 UI 的死锁。 我们声明boolean标志_loading,当网络操作开始时将其设置为true。 现在,我们定义一种_createCircularProgress()方法,如下所示:
 Widget _createCircularProgress(){
   if (_loading) {
     return Center(child: CircularProgressIndicator());
   } return Container(height: 0.0, width: 0.0,);
 }

仅当_loadingtrue并且正在进行网络操作时,该方法才返回CircularProgressIndicator()

  1. 最后,让我们在build()方法内添加所有小部件:
 @override
 Widget build(BuildContext context) {
   return new Scaffold(
       appBar: new AppBar(
         title: new Text('Firebase Authentication'),
       ),
       body: Stack(
         children: <Widget>[
           _createBody(),
           _createCircularProgress(),
         ],
       ));
 }

build()内部,添加包含应用标题的AppBar变量后,我们返回一个支架。 支架的主体包含一个带有子项的栈,这些子项是_createBody()_createCircularProgress() 函数调用返回的小部件。

现在,我们已经准备好应用的主要 UI 结构。

可以在这个页面中找到SignupSigninScreen的完整代码。

在下一部分中,我们将介绍将 Firebase 认证添加到应用中涉及的步骤。

添加 Firebase 认证

如前所述,在“简单登录应用”部分中,我们将使用用户的电子邮件和密码通过 Firebase 集成认证。

要在 Firebase 控制台上创建和配置 Firebase 项目,请参考“附录”。

以下步骤详细讨论了如何在 Firebase Console 上设置项目:

  1. 我们首先在 Firebase 控制台上选择项目:

  1. 接下来,我们将在Develop菜单中单击Authentication选项:

这将带我们进入认证屏幕。

  1. 迁移到登录标签并启用登录提供者下的“电子邮件/密码”选项:

这是设置 Firebase 控制台所需的全部。

接下来,我们将 Firebase 集成到代码中。 这样做如下:

  1. 迁移到 Flutter SDK 中的项目,然后将firebase-auth添加到应用级别build.gradle文件中:
implementation 'com.google.firebase:firebase-auth:18.1.0'
  1. 为了使FirebaseAuthentication在应用中正常工作,我们将在此处使用firebase_auth插件。 在pubspec.yaml文件的依赖项中添加插件依赖项:
firebase_auth: 0.14.0+4

确保运行flutter pub get以安装依赖项。

现在,让我们编写一些代码以在应用内部提供 Firebase 认证功能。

创建auth.dart

现在,我们将创建一个 Dart 文件auth.dart。 该文件将作为访问firebase_auth插件提供的认证方法的集中点:

  1. 首先,导入firebase_auth插件:
import 'package:firebase_auth/firebase_auth.dart';
  1. 现在,创建一个抽象类BaseAuth,该类列出了所有认证方法,并充当 UI 组件和认证方法之间的中间层:
abstract class BaseAuth {
 Future<String> signIn(String email, String password);
 Future<String> signUp(String email, String password);
 Future<String> getCurrentUser();
 Future<void> signOut();
}

顾名思义,这些方法将使用认证的四个主要函数:

  • signIn():使用电子邮件和密码登录已经存在的用户
  • signUp():使用电子邮件和密码为新用户创建帐户
  • getCurrentUser():获取当前登录的用户
  • signOut():注销已登录的用户

这里要注意的重要一件事是,由于这是网络操作,因此所有方法都异步操作,并在执行完成后返回Future值。

  1. 创建一个实现BaseAuthAuth类:
class Auth implements BaseAuth {
    //. . . . . 
}

在接下来的步骤中,我们将定义BaseAuth中声明的所有方法。

  1. 创建FirebaseAuth的实例:
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
  1. signIn()方法实现如下:
 Future<String> signIn(String email, String password) async {
     AuthResult result = await _firebaseAuth.signInWithEmailAndPassword(email: email, password: password);
    FirebaseUser user = result.user;
    return user.uid;
}

此方法接收用户的电子邮件和密码,然后调用signInWithEmailAndPassword(),并传递电子邮件和密码以登录已经存在的用户。 登录操作完成后,将返回AuthResult实例。 我们将其存储在result中,还使用result.user,它返回FirebaseUser.。它可用于获取与用户有关的信息,例如他们的uidphoneNumberphotoUrl。 在这里,我们返回user.uid,它是每个现有用户的唯一标识。 如前所述,由于这是网络操作,因此它异步运行,并在执行完成后返回Future

  1. 接下来,我们将定义signUp()方法以添加新用户:
Future<String> signUp(String email, String password) async {
    AuthResult result = await _firebaseAuth.createUserWithEmailAndPassword(email: email, password: password);
    FirebaseUser user = result.user;
    return user.uid;
 }

前面的方法接收在注册过程中使用的电子邮件和密码,并将其值传递给createUserWithEmailAndPassword。 类似于上一步中定义的对象,此调用还返回AuthResult对象,该对象还用于提取FirebaseUser。 最后,signUp方法返回新创建的用户的uid

  1. 现在,我们将定义getCurrentUser()
 Future<String> getCurrentUser() async {
   FirebaseUser user = await _firebaseAuth.currentUser();
   return user.uid;
 }

在先前定义的函数中,我们使用_firebaseAuth.currentUser()提取当前登录用户的信息。 此方法返回包装在FirebaseUser对象中的完整信息。 我们将其存储在user变量中。 最后,我们使用user.uid返回用户的uid

  1. 接下来,我们执行signOut()
Future<void> signOut() async {
   return _firebaseAuth.signOut();
 }

此函数仅在当前FirebaseAuth实例上调用signOut()并注销已登录的用户。

至此,我们已经完成了用于实现 Firebase 认证的所有基本编码。

可以在这个页面中查看auth.dart中的整个代码。

现在让我们看看如何在应用内部使认证生效。

SignupSigninScreen中添加认证

在本节中,我们将在SignupSigninScreen中添加 Firebase 认证。

我们在signup_signin_screen.dart文件中定义了_signinSignup()方法。 当按下登录按钮时,将调用该方法。 该方法的主体如下所示:

 void _signinSignup() async {
   setState(() {
     _loading = true;
   });
     String userId = "";     
       if (_formMode == FormMode.SIGNIN) {
         userId = await widget.auth.signIn(_usermail, _userpassword);
       } else {
         userId = await widget.auth.signUp(_usermail, _userpassword);
       }
       setState(() {
         _loading = false;
       });
       if (userId.length > 0 && userId != null && _formMode == FormMode.SIGNIN) {
         widget.onSignedIn();
       }
}

在上述方法中,我们首先将_loading的值设置为true,以便进度条显示在屏幕上,直到登录过程完成。 接下来,我们创建一个userId字符串,一旦登录/登录操作完成,该字符串将存储userId的值。 现在,我们检查_formMode的当前值。 如果等于FormMode.SIGNIN,则用户希望登录到现有帐户。 因此,我们使用传递到SignupSigninScreen构造器中的实例来调用Auth类内部定义的signIn()方法。

这将在后面的部分中详细讨论。 否则,如果_formMode的值等于FormMode.SIGNUP,则将调用Auth类的signUp()方法,并传递用户的邮件和密码以创建新帐户。 一旦成功完成登录/注册,userId变量将用于存储用户的 ID。 整个过程完成后,将_loading设置为false,以从屏幕上删除循环进度指示器。 另外,如果在用户登录到现有帐户时userId具有有效值,则将调用onSignedIn(),这会将用户定向到应用的主屏幕。

此方法也传递给SignupSigninScreen的构造器,并将在后面的部分中进行讨论。 最后,我们将整个主体包裹在try-catch块中,以便在登录过程中发生的任何异常都可以捕获而不会导致应用崩溃,并可以在屏幕上显示。

创建主屏幕

我们还需要确定认证状态,即用户在启动应用时是否已登录,如果已经登录,则将其定向到主屏幕。如果尚未登录,则应显示SignInSignupScreen 首先,在完成该过程之后,将启动主屏幕。 为了实现这一点,我们在新的 dart 文件main_screen.dart中创建一个有状态的小部件MainScreen,然后执行以下步骤:

  1. 我们将从定义枚举AuthStatus开始,该枚举表示用户的当前认证状态,可以登录或不登录:
enum AuthStatus {
 NOT_SIGNED_IN,
 SIGNED_IN,
}
  1. 现在,我们创建enum类型的变量来存储当前认证状态,其初始值设置为NOT_SIGNED_IN
AuthStatus authStatus = AuthStatus.NOT_SIGNED_IN;
  1. 初始化小部件后,我们将通过覆盖initState()方法来确定用户是否已登录:
 @override
 void initState() {
   super.initState();
   widget.auth.getCurrentUser().then((user) {
     setState(() {
       if (user != null) {
         _userId = user;
       }
       authStatus =
           user == null ? AuthStatus.NOT_SIGNED_IN : AuthStatus.SIGNED_IN;
     });
   });
 }

使用在构造器中传递的类的实例调用Auth类的getCurrentUser()。 如果该方法返回的值不为null,则意味着用户已经登录。因此,_userId字符串变量的值设置为返回的值。 另外,将authStatus设置为AuthStatus.SIGNED_IN.,否则,如果返回的值为null,则意味着没有用户登录,因此authStatus的值设置为AuthStatus.NOT_SIGNED_IN

  1. 现在,我们将定义另外两个方法onSignIn()onSignOut(),以确保将认证状态正确存储在变量中,并相应地更新用户界面:
void _onSignedIn() {
   widget.auth.getCurrentUser().then((user){
     setState(() {
       _userId = user;
     });
   });
   setState(() {
     authStatus = AuthStatus.SIGNED_IN;
   });
 }
 void _onSignedOut() {
   setState(() {
     authStatus = AuthStatus.NOT_SIGNED_IN;
     _userId = "";
   });
 }

_onSignedIn()方法检查用户是否已经登录,并将authStatus设置为AuthStatus.SIGNED_IN._onSignedOut()方法检查用户是否已注销,并将authStatus设置为AuthStatus.SIGNED_OUT

  1. 最后,我们重写build方法将用户定向到正确的屏幕:
 @override
 Widget build(BuildContext context) {
   if(authStatus == AuthStatus.SIGNED_OUT) {
     return new SignupSigninScreen(
       auth: widget.auth,
       onSignedIn: _onSignedIn,
     );
   } else {
     return new HomeScreen(
       userId: _userId,
       auth: widget.auth,
       onSignedOut: _onSignedOut,
       );
   }
 }

如果authStatusAuthStatus.SIGNED_OUT,则返回SignupSigninScreen,并传递auth实例和_onSignedIn()方法。 否则,将直接返回HomeScreen,并传递已登录用户的userIdAuth实例类和_onSignedOut()方法。

可以在此处查看main_screen.dart的完整代码

在下一部分中,我们将为应用添加一个非常简单的主屏幕。

创建主屏幕

由于我们对认证部分更感兴趣,因此主屏幕(即成功登录后指向用户的屏幕)应该非常简单。 它仅包含一些文本和一个注销选项。 正如我们对所有先前的屏幕和小部件所做的一样,我们首先创建一个home_screen.dart文件和一个有状态的HomeScreen小部件。

主屏幕将显示如下:

此处的完整代码位于重写的build()方法内部:

 @override
 Widget build(BuildContext context) {
     return new Scaffold(
         appBar: new AppBar(
             title: new Text('Firebase Authentication'),
             actions: <Widget>[
                 new FlatButton(
                     child: new Text('Logout',
                     style: new TextStyle(fontSize: 16.0, color: Colors.white)),
                     onPressed: _signOut
                 )
             ],
         ),
         body: Center(child: new Text('Hello User', 
         style: new TextStyle(fontSize: 32.0))
         ),
     );
 }

我们在此处返回Scaffold,其中包含标题为Text Firebase AuthenticationAppBaractions属性的小部件列表。 actions用于在应用标题旁边添加小部件列表到应用栏中。 在这里,它仅包含FlatButtonLogout,在按下时将调用_signOut

_signOut()方法显示如下:

 _signOut() async {
   try {
     await widget.auth.signOut();
     widget.onSignedOut();
   } catch (e) {
     print(e);
   }
 }

该方法主要是调用Auth类中定义的signOut()方法,以将用户从应用中注销。 回忆传入HomeScreenMainScreen_onSignedOut()方法。 当用户退出时,该方法在此处用作widget.onSignedOut()来将authStatus更改为SIGNED_OUT。 同样,它包装在try-catch块中,以捕获并打印此处可能发生的任何异常。

可以在此处查看home_screen.dart的整个代码

至此,应用的主要组件已经准备就绪,现在让我们创建最终的材质应用。

创建main.dart

main.dart内部,我们创建Stateless WidgetApp,并覆盖build()方法,如下所示:

 @override
 Widget build(BuildContext context) {
   return new MaterialApp(
       title: 'Firebase Authentication',
       debugShowCheckedModeBanner: false,
       theme: new ThemeData(
         primarySwatch: Colors.blue,
       ),
       home: new MainScreen(auth: new Auth()));
 }

该方法从主屏幕返回MaterialApp,以提供标题,主题。

可以在此处查看main.dart文件

了解用于认证的异常检测

异常检测是机器学习的一个备受关注的分支。 该术语含义简单。 基本上,它是用于检测异常的方法的集合。 想象一袋苹果。 识别并挑选坏苹果将是异常检测的行为。

异常检测以几种方式执行:

  • 通过使用列的最小最大范围来识别数据集中与其余样本非常不同的数据样本
  • 通过将数据绘制为线形图并识别图中的突然尖峰
  • 通过围绕高斯曲线绘制数据并将最末端的点标记为离群值(异常)

一些常用的方法是支持向量机,贝叶斯网络和 K 最近邻。 在本节中,我们将重点介绍与安全性相关的异常检测。

假设您通常在家中登录应用上的帐户。 如果您突然从数千英里外的位置登录帐户,或者在另一种情况下,您以前从未使用过公共计算机登录帐户,那将是非常可疑的,但是突然有一天您这样做。 另一个可疑的情况可能是您尝试 10-20 次密码,每次在成功成功登录之前每次都输入错误密码。 当您的帐户遭到盗用时,所有这些情况都是可能的行为。 因此,重要的是要合并一个能够确定您的常规行为并对异常行为进行分类的系统。 换句话说,即使黑客使用了正确的密码,企图破坏您的帐户的尝试也应标记为异常。

这带给我们一个有趣的观点,即确定用户的常规行为。 我们如何做到这一点? 什么是正常行为? 它是针对每个用户的还是一个通用概念? 问题的答案是它是非常特定于用户的。 但是,行为的某些方面对于所有用户而言都可以相同。 一个应用可能会在多个屏幕上启动登录。 单个用户可能更喜欢其中一种或两种方法。 这将导致特定于该用户的特定于用户的行为。 但是,如果尝试从未由开发人员标记为登录屏幕的屏幕进行登录,则无论是哪个用户尝试登录,都肯定是异常的。

在我们的应用中,我们将集成一个这样的系统。 为此,我们将记录一段时间内我们应用的许多用户进行的所有登录尝试。 我们将特别注意他们尝试登录的屏幕以及它们传递给系统的数据类型。 一旦收集了很多这些样本,就可以根据用户执行的任何操作来确定系统对认证的信心。 如果系统在任何时候认为用户表现出的行为与他们的惯常行为相差很大,则该用户将未经认证并被要求验证其帐户详细信息。

让我们从创建预测模型开始,以确定用户认证是常规的还是异常的。

用于认证用户的自定义模型

我们将本节分为两个主要子节:

  • 构建用于认证有效性检查的模型
  • 托管自定义认证验证模型

让我们从第一部分开始。

构建用于认证有效性检查的模型

在本部分中,我们将构建模型来确定是否有任何用户正在执行常规登录或异常登录:

  1. 我们首先导入必要的模块,如下所示:
import sys
import os
import json
import pandas
import numpy
from keras.models import Sequential
from keras.layers import LSTM, Dense, Dropout
from keras.layers.embeddings import Embedding
from keras.preprocessing import sequence
from keras.preprocessing.text import Tokenizer
from collections import OrderedDict
  1. 现在,我们将数据集导入到项目中。 可以在这里中找到该数据集:
csv_file = 'data.csv'

dataframe = pandas.read_csv(csv_file, engine='python', quotechar='|', header=None)
count_frame = dataframe.groupby([1]).count()
print(count_frame)
total_req = count_frame[0][0] + count_frame[0][1]
num_malicious = count_frame[0][1]

print("Malicious request logs in dataset: {:0.2f}%".format(float(num_malicious) / total_req * 100))

前面的代码块将 CSV 数据集加载到项目中。 它还会打印一些与数据有关的统计信息,如下所示:

  1. 我们在上一步中加载的数据目前尚无法使用,无法进行深度学习。 在此步骤中,我们将其分为特征列和标签列,如下所示:
X = dataset[:,0]
Y = dataset[:,1]
  1. 接下来,我们将删除数据集中包含的某些列,因为我们不需要所有这些列来构建简单的模型:
for index, item in enumerate(X):
    reqJson = json.loads(item, object_pairs_hook=OrderedDict)
    del reqJson['timestamp']
    del reqJson['headers']
    del reqJson['source']
    del reqJson['route']
    del reqJson['responsePayload']
    X[index] = json.dumps(reqJson, separators=(',', ':'))
  1. 接下来,我们将在剩余的请求正文上执行分词。 分词是一种用于将大文本块分解为较小文本的方法,例如将段落分成句子,将句子分成单词。 我们这样做如下:
tokenizer = Tokenizer(filters='\t\n', char_level=True)
tokenizer.fit_on_texts(X)
  1. 分词之后,我们将请求正文中的文本转换为单词向量,如下一步所示。 我们将数据集和DataFrame标签分为两部分,即 75%-25%,以进行训练和测试:
num_words = len(tokenizer.word_index)+1
X = tokenizer.texts_to_sequences(X)

max_log_length = 1024
train_size = int(len(dataset) * .75)

X_processed = sequence.pad_sequences(X, maxlen=max_log_length)
X_train, X_test = X_processed[0:train_size], X_processed[train_size:len(X_processed)]
Y_train, Y_test = Y[0:train_size], Y[train_size:len(Y)]
  1. 接下来,我们基于长短期记忆LSTM)创建基于循环神经网络RNN)的学习方法,来识别常规用户行为。 将单词嵌入添加到层中,以帮助维持单词向量和单词之间的关系:
model = Sequential()
model.add(Embedding(num_words, 32, input_length=max_log_length))
model.add(Dropout(0.5))
model.add(LSTM(64, recurrent_dropout=0.5))
model.add(Dropout(0.5))
model.add(Dense(1, activation='sigmoid'))

我们的输出是单个神经元,在正常登录的情况下,该神经元保存0;在登录异常的情况下,则保存1

  1. 现在,我们以精度作为度量标准编译模型,而损失则作为二进制交叉熵来计算:
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
print(model.summary())
  1. 现在,我们准备进行模型的训练:
model.fit(X_train, Y_train, validation_split=0.25, epochs=3, batch_size=128)
  1. 我们将快速检查模型所达到的准确率。 当前模型的准确率超过 96%:
score, acc = model.evaluate(X_test, Y_test, verbose=1, batch_size=128)
print("Model Accuracy: {:0.2f}%".format(acc * 100))

下面的屏幕快照显示了前面代码块的输出:

  1. 现在,我们保存模型权重和模型定义。 我们稍后将它们加载到 API 脚本中,以验证用户的认证:
model.save_weights('lstm-weights.h5')
model.save('lstm-model.h5')

现在,我们可以将认证模型作为 API 进行托管,我们将在下一部分中进行演示。

托管自定义认证验证模型

在本节中,我们将创建一个 API,用于在用户向模型提交其登录请求时对其进行认证。 请求标头将被解析为字符串,并且模型将使用它来预测登录是否有效:

  1. 我们首先导入创建 API 服务器所需的模块:
from sklearn.externals import joblib
from flask import Flask, request, jsonify
from string import digits

import sys
import os
import json
import pandas
import numpy
import optparse
from keras.models import Sequential, load_model
from keras.preprocessing import sequence
from keras.preprocessing.text import Tokenizer
from collections import OrderedDict
  1. 现在,我们实例化一个Flask应用对象。 我们还将从上一节“构建用于认证有效性检查的模型”中加载保存的模型定义和模型权重。然后,我们重新编译模型,并使用_make_predict_function( )方法创建其预测方法,如以下步骤所示:
app = Flask(__name__)

model = load_model('lstm-model.h5')
model.load_weights('lstm-weights.h5')
model.compile(loss = 'binary_crossentropy', optimizer = 'adam', metrics = ['accuracy'])
model._make_predict_function()
  1. 然后,我们创建一个remove_digits()函数,该函数用于从提供给它的输入中去除所有数字。 这将用于在将请求正文文本放入模型之前清除它:
def remove_digits(s: str) -> str:
    remove_digits = str.maketrans('', '', digits)
    res = s.translate(remove_digits)
    return res
  1. 接下来,我们将在 API 服务器中创建/login路由。 该路由由login()方法处理,并响应GETPOST请求方法。 正如我们对训练输入所做的那样,我们删除了请求标头中的非必要部分。 这可以确保模型将对数据进行预测,类似于对其进行训练的数据:
@app.route('/login', methods=['GET, POST'])
def login():
    req = dict(request.headers)
    item = {}
    item["method"] = str(request.method)
    item["query"] = str(request.query_string)
    item["path"] = str(request.path)
    item["statusCode"] = 200
    item["requestPayload"] = []

    ## MORE CODE BELOW THIS LINE

    ## MORE CODE ABOVE THIS LINE

    response = {'result': float(prediction[0][0])}
    return jsonify(response)
  1. 现在,我们将代码添加到login()方法中,该方法将标记请求正文并将其传递给模型以执行有关登录请求有效性的预测,如下所示:
@app.route('/login', methods=['GET, POST'])
def login():
    ...
    ## MORE CODE BELOW THIS LINE
    X = numpy.array([json.dumps(item)])
    log_entry = "store"

    tokenizer = Tokenizer(filters='\t\n', char_level=True)
    tokenizer.fit_on_texts(X)
    seq = tokenizer.texts_to_sequences([log_entry])
    max_log_length = 1024
    log_entry_processed = sequence.pad_sequences(seq, maxlen=max_log_length)

    prediction = model.predict(log_entry_processed)
    ## MORE CODE ABOVE THIS LINE
    ...

最后,应用以 JSON 字符串的形式返回其对用户进行认证的信心。

  1. 最后,我们使用apprun()方法启动服务器脚本:
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)
  1. 将此文件另存为main.py。 要开始执行服务器,请打开一个新终端并使用以下命令:
python main.py

服务器监听其运行系统的所有传入 IP。 通过在0.0.0.0 IP 上运行它,可以实现这一点。 如果我们希望稍后在基于云的服务器上部署脚本,则需要这样做。 如果不指定0.0.0.0主机,则默认情况下会使它监听127.0.0.1,这不适合在公共服务器上进行部署。 您可以在此处详细了解这些地址之间的区别

在下一节中,我们将看到如何将 ReCaptcha 集成到迄今为止在该项目中构建的应用中。 之后,我们将把本节中构建的 API 集成到应用中。

实现 ReCaptcha 来保护垃圾邮件

为了为 Firebase 认证增加另一层安全性,我们将使用 ReCaptcha。 这是 Google 所支持的一项测试,可帮助我们保护数据免受垃圾邮件和滥用行为的自动 bot 攻击。 该测试很简单,很容易被人类解决,但是却阻碍了漫游器和恶意用户的使用。

要了解有关 ReCaptcha 及其用途的更多信息,请访问这里

ReCAPTCHA v2

在本节中,我们将把 ReCaptcha 版本 2 集成到我们的应用中。 在此版本中,向用户显示一个简单的复选框。 如果刻度变为绿色,则表明用户已通过验证。

另外,还可以向用户提出挑战,以区分人和机器人。 这个挑战很容易被人类解决。 他们要做的就是根据说明选择一堆图像。 使用 ReCaptcha 进行认证的传统流程如下所示:

一旦用户能够验证其身份,他们就可以成功登录。

获取 API 密钥

要在我们的应用内部使用 ReCaptcha,我们需要在reCAPTCHA管理控制台中注册该应用,并获取站点密钥和秘密密钥。 为此,请访问这里并注册该应用。 您将需要导航到“注册新站点”部分,如以下屏幕截图所示:

我们可以通过以下两个简单步骤来获取 API 密钥:

  1. 首先提供一个域名。 在这里,我们将在 reCAPTCHA v2 下选择 reCAPTCHA Android。
  2. 选择 Android 版本后,添加项目的包名称。 正确填写所有信息后,单击“注册”。

这将引导您到显示站点密钥和秘密密钥的屏幕,如以下屏幕快照所示:

站点密钥秘密密钥复制并保存到安全位置。 我们将在编码应用时使用它们。

代码整合

为了在我们的应用中包含 ReCaptcha v2,我们将使用 Flutter 包flutter_recaptcha_v2。 将flutter_recaptcha_v2:0.1.0依赖项添加到pubspec.yaml文件中,然后在终端中运行flutter packages get以获取所需的依赖项。 以下步骤详细讨论了集成:

  1. 我们将代码添加到signup_signin_screen.dart。 首先导入依赖项:
import 'package:flutter_recaptcha_v2/flutter_recaptcha_v2.dart';
  1. 接下来,创建一个RecaptchaV2Controller实例:
RecaptchaV2Controller recaptchaV2Controller = RecaptchaV2Controller();
  1. reCAPTCHA 复选框将添加为小部件。 首先,让我们定义一个返回小部件的_createRecaptcha()方法:
 Widget _createRecaptcha() {
   return RecaptchaV2(
     apiKey: "Your Site Key here", 
     apiSecret: "Your API Key here", 
     controller: recaptchaV2Controller,
     onVerifiedError: (err){
       print(err);
     },
     onVerifiedSuccessfully: (success) {
       setState(() {
       if (success) {
         _signinSignup();
       } else {
         print('Failed to verify');
       }
       });
     },
   );
 }

在上述方法中,我们仅使用RecaptchaV2()构造器,即可为特定属性指定值。 添加您先前在apiKeyapiSecret属性中注册时保存的站点密钥和秘密密钥。 我们使用先前为属性控制器创建的recaptcha控制器recaptchaV2Controller的实例。 如果成功验证了用户,则将调用_signinSignup()方法以使用户登录。如果在验证期间发生错误,我们将打印错误。

  1. 现在,由于在用户尝试登录时应显示reCaptcha,因此我们将createSigninButton()中的登录凸起按钮的onPressed属性修改为recaptchaV2Controller
Widget _createSigninButton() {
    . . . . . . .
    return new Padding(
        . . . . . . .
        child: new RaisedButton(
            . . . . . . 
            //Modify the onPressed property
            onPressed: recaptchaV2Controller.show
        )
    )
}
  1. 最后,我们将_createRecaptcha()添加到build()内部的主体栈中:
 @override
 Widget build(BuildContext context) {
    . . . . . . .
    return new Scaffold(
        . . . . . . .
        body: Stack(
            children: <Widget>[
                _createBody(),
                _createCircularProgress(),

                //Add reCAPTCHA Widget
                _createRecaptcha()
                 ],
       ));
 }

这就是一切! 现在,我们具有比 Firebase 认证更高的安全级别,可以保护应用的数据免受自动机器人的攻击。 现在让我们看一下如何集成定制模型以检测恶意用户。

在 Flutter 中部署模型

至此,我们的 Firebase 认证应用与 ReCaptcha 保护一起运行。 现在,让我们添加最后的安全层,该层将不允许任何恶意用户进入应用。

我们已经知道该模型位于以下端点。 我们只需从应用内部进行 API 调用,传入用户提供的电子邮件和密码,并从模型中获取结果值。 该值将通过使用阈值结果值来帮助我们判断登录是否是恶意的。

如果该值小于 0.20,则认为该登录名是恶意的,并且屏幕上将显示以下消息:

现在,让我们看一下在 Flutter 应用中部署模型的步骤:

  1. 首先,由于我们正在获取数据并且将使用网络调用(即 HTTP 请求),因此我们需要向pubspec.yaml文件添加http依赖项,并按以下方式导入:
import 'package:http/http.dart' as http;
  1. 首先在auth.dart:内部定义的BaseAuth抽象类中添加以下函数声明
 Future<double> isValidUser(String email, String password);
  1. 现在,让我们在Auth类中定义isValidUser()函数:
 Future<double> isValidUser(String email, String password) async{
   final response = await http.Client()
       .get('http://34.67.160.232:8000/login?user=$email&password=$password');
     var jsonResponse = json.decode(response.body);
     var val = '${jsonResponse["result"]}';
     double result = double.parse(val);     
     return result;
   }

此函数将用户的电子邮件和密码作为参数,并将它们附加到请求 URL,以便为特定用户生成输出。 get request响应存储在变量响应中。 由于响应为 JSON 格式,因此我们使用json.decode()对其进行解码,并将解码后的响应存储在另一个变量响应中。 现在,我们使用‘${jsonResponse["result"]}'访问jsonResponse中的结果值,使用double.parse()将其转换为双精度类型整数,并将其存储在结果中。 最后,我们返回结果的值。

  1. 为了激活代码内部的恶意检测,我们从SigninSignupScreen调用了isValidUser()方法。 当具有现有帐户的用户选择从if-else块内部登录时,将调用此方法:
if (_formMode == FormMode.SIGNIN) {

    var val = await widget.auth.isValidUser(_usermail, _userpassword);

    . . . .
    } else {
      . . . .   
    }

isValidUser返回的值存储在val变量中。

  1. 如果该值小于 0.20,则表明登录活动是恶意的。 因此,我们将异常抛出并在 catch 块内抛出catch并在屏幕上显示错误消息。 这可以通过创建自定义异常类MalicousUserException来完成,该类在实例化时返回一条错误消息:
class MaliciousUserException implements Exception {
  String message() => 'Malicious login! Please try later.';
}
  1. 现在,我们将在调用isValidUser()之后添加if块,以检查是否需要抛出异常:
var val = await widget.auth.isValidUser(_usermail, _userpassword);
//Add the if block 
if(val < 0.20) {
    throw new MaliciousUserException();
}
  1. 现在,该异常已捕获在catch块内,并且不允许用户继续登录。此外,我们将_loading设置为false以表示不需要进一步的网络操作:
catch(MaliciousUserException) {
       setState(() {
         _loading = false;
           _errorMessage = 'Malicious user detected. Please try again later.';
       });

这就是一切! 我们之前基于 Firebase 认证创建的 Flutter 应用现在可以在后台运行智能模型的情况下找到恶意用户。

总结

在本章中,我们了解了如何使用 Flutter 和由 Firebase 支持的认证系统构建跨平台应用,同时结合了深度学习的优势。 然后,我们了解了如何将黑客攻击尝试归类为一般用户行为中的异常现象,并创建了一个模型来对这些异常现象进行分类以防止恶意用户登录。最后,我们使用了 Google 的 ReCaptcha 来消除对该应用的垃圾邮件使用,因此,使其在自动垃圾邮件或脚本化黑客攻击方面更具弹性。

在下一章中,我们将探索一个非常有趣的项目–使用移动应用上的深度学习生成音乐成绩单。