如何使用Flask API进行Flutter认证

723 阅读8分钟

使用Flask API进行Flutter认证

软件或应用程序大多持有不是每个人都能访问的数据。这使得认证成为软件或应用开发的一个核心方面。

认证是证明试图访问一个系统的用户是否有权限这样做的过程。

了解Flutter框架

Flutter是一个由谷歌创建的UI工具包。它被用来构建Android和IoS的跨平台移动应用以及Windows、Mac和Linux的桌面应用。

用Flutter构建的UI总是依赖于后端技术栈的核心功能,如认证和访问控制。这些后端技术之一是Flask框架。Flask是一个用于构建网络应用的Python网络框架。

前提条件

在本指南中,Flutter 2.5版本将被用于构建Flutter项目,Android studio是用于编写代码的IDE,但也可以使用visual studio代码。为了与本文一起编码,推荐使用前面提到的工具。本文假设读者有Flutter和Python flask的背景知识。

目标

本指南将帮助读者了解Flutter应用程序如何与后台互动,以及当Flask被用作后台技术时如何进行身份验证。在这个过程中,我们将建立一个Flutter应用程序,使用Flask API对用户进行认证,其工作原理如下所示。

demo

了解应用程序

Web应用程序主要由两部分组成。有前端后端。这两部分非常微妙,它们大多以不同的方式构建,有时由不同的开发人员来完成。

应用程序的前端部分也被称为应用程序的客户端。它侧重于应用程序的外观,前端开发人员确保应用程序的外观和响应性(即,它在所有类型的屏幕上看起来很好)。

应用程序的后端部分侧重于应用程序的工作方式。应用程序的后端也被称为应用程序的服务器端。后台开发人员确保从用户那里收到的数据得到适当的存储,并以速度和效率迅速呈现给用户。

应用程序编程接口(API)

API就像后端和前端之间的一个中间人。它使用端点来连接计算机或计算机程序。端点是指两个或多个系统之间的通信,其中一个请求从前端发送到一个网络应用程序或网络服务器。

从应用程序的客户端和服务器端传输数据是通过API实现的。数据也可以通过API从一个后端服务器传输到另一个。例如,下面是使用Python的Flask为认证而建立的主要登录和注册端点的代码片断。

from flask import Blueprint, request, json, jsonify
from .models import Student
from software import db

views = Blueprint('views', __name__)

@views.route('/register', methods=["GET", "POST"])
def register():
    d={}
    if request.method =="POST":
        mail = request.form["email"]
        password = request.form["password"]

        email = Student.query.filter_by(email=mail).first()

        if email is None:
            register = Student(email=mail, password=password)

            db.session.add(register)
            db.session.commit()
           
            return jsonify(["Register success"])
        else:
            # already exist
            
            return jsonify(["user alredy exist"])


@views.route('/login', methods=["GET", "POST"])
def login():
    d = {}
    if request.method == "POST":
        mail = request.form["email"]
        password = request.form["password"]

        login = Student.query.filter_by(email=mail, password=password).first()

        if login is None:
            # acount not found
            
            return jsonify(["Wrong Credentials"]) 
        else:
            # acount found
            
            return jsonify([ "success"])

上面的注册端点的代码片段接受来自表单的数据,并对数据库中已经存在的数据进行过滤,以确保没有数据不一致的情况。然后,过滤器的结果决定了结果。

登录端点的代码片断对数据进行过滤,以确保用户在注册的情况下被注册。然后,用户就可以访问系统中的信息。如果没有,用户就会收到一条信息,说明凭证是错误的。

Flutter应用程序设计

安装

要建立一个Flutter应用程序,必须在计算机上安装Flutter。

flutter create name-of-app

安装后,位于项目目录内的lib文件夹内的main.dart 文件被清除,并替换为下面的代码片段。

import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:loginwithapi/views/login.dart';
import 'package:loginwithapi/views/register.dart';
import 'package:loginwithapi/views/welcome.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "LoginApp",
      home: WelcomePage(),
      builder: EasyLoading.init(),
    );
  }
}

从上面的代码片断中,import 'package:flutter/material.dart'; ,导入MaterialApp ,这个小部件包裹了多个小部件,这些小部件大多是材料设计应用中需要的。

如果你粘贴上面的代码片断,你会注意到红线,表明导入的东西有问题。一个将从import 'package:flutter_easyloading/flutter_easyloading.dart'; 来修复,使用命令添加flutter_easyloading到包中。

flutter pub add flutter_easyloading

上面的命令将flutter_easyloading 添加到项目中的包中。对于import 'package:loginwithapi/views/welcome.dart';.错误的出现是因为这行代码从一个不存在的页面导入了一个类。

在构建部件内返回了一个MaterialApp 。它有一个标题和首页子部件。标题主要是应用程序的名称,而在主页中,一个WelcomPage 的类被调用。

为了修复红线错误,添加welcomePage 类,在lib文件夹内创建两个新文件夹。命名为views,service 在views文件夹内创建三个文件,命名为login.dart,register.dart, 和welcome.dart 并添加下面的代码片段。

在欢迎页面上工作

import 'package:flutter/material.dart';
import 'package:loginwithapi/views/login.dart';
import 'package:loginwithapi/views/register.dart';

class WelcomePage extends StatefulWidget {
  const WelcomePage({Key? key}) : super(key: key);

  @override
  _WelcomePageState createState() => _WelcomePageState();
}

class _WelcomePageState extends State<WelcomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Container(
        margin: const EdgeInsets.symmetric(horizontal: 30),
        decoration: BoxDecoration(borderRadius: BorderRadius.circular(20)),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            InkWell(
                onTap: () {
                  Navigator.push(context,
                      MaterialPageRoute(builder: (context) => LoginPage()));
                },
                child: Container(
                  margin:
                  const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
                  child: const Center(
                    child: Text(
                      "Login",
                      style: TextStyle(
                          fontWeight: FontWeight.bold, color: Colors.white),
                    ),
                  ),
                  height: 50,
                  width: double.infinity,
                  decoration: BoxDecoration(
                      color: Colors.red,
                      borderRadius: BorderRadius.circular(25)),
                )),
            InkWell(
                onTap: () {
                  Navigator.push(context,
                      MaterialPageRoute(builder: (context) => RegisterPage()));
                },
                child: Container(
                  margin:
                  const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
                  child: const Center(
                    child: Text(
                      "Register",
                      style: TextStyle(
                          fontWeight: FontWeight.bold, color: Colors.white),
                    ),
                  ),
                  height: 50,
                  width: double.infinity,
                  decoration: BoxDecoration(
                      color: Colors.green,
                      borderRadius: BorderRadius.circular(25)),
                ))
          ],
        ),
      ),
    );
  }
}

登录页面设计

import 'package:flutter/material.dart';
import 'package:loginwithapi/service/http_service.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';

class LoginPage extends StatefulWidget {
  const LoginPage({Key? key}) : super(key: key);

  @override
  _LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  late String email;
  late String password;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: const Text('Login Page')),
        body: Container(
          margin: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
          child: Column(
            children: [
              TextField(
                obscureText: false,
                decoration: InputDecoration(hintText: 'email'),
                onChanged: (value) {
                  setState(() {
                    email = value;
                  });
                },
              ),
              TextField(
                obscureText: true,
                decoration: InputDecoration(hintText: 'password'),
                onChanged: (value) {
                  setState(() {
                    password = value;
                  });
                },
              ),
              InkWell(
                  onTap: () async {
                    print(password);
                    print(email);
                    await HttpService.login(email, password, context);
                  },
                  child: Container(
                    margin: const EdgeInsets.symmetric(
                        horizontal: 20, vertical: 10),
                    child: const Center(
                      child: Text(
                        "Login",
                        style: TextStyle(
                            fontWeight: FontWeight.bold, color: Colors.white),
                      ),
                    ),
                    height: 50,
                    width: double.infinity,
                    decoration: BoxDecoration(
                        color: Colors.red,
                        borderRadius: BorderRadius.circular(25)),
                  ))
            ],
          ),
        )
      // ignore: avoid_unnecessary_containers
    );
  }
}

注册页面代码

import 'package:flutter/material.dart';
import 'package:loginwithapi/service/http_service.dart';

class RegisterPage extends StatefulWidget {
  const RegisterPage({Key? key}) : super(key: key);

  @override
  _RegisterPageState createState() => _RegisterPageState();
}

class _RegisterPageState extends State<RegisterPage> {

  late String email;
  late String password;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: const Text('Register Page')),
        body: Container(
          margin: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
          child: Column(
            children: [
              TextField(
                obscureText: false,
                decoration: InputDecoration(hintText: 'email'),
                onChanged: (value) {
                  setState(() {
                    email = value;
                  });
                },
              ),
              TextField(
                obscureText: true,
                decoration: InputDecoration(hintText: 'password'),
                onChanged: (value) {
                  setState(() {
                    password = value;
                  });
                },
              ),
              InkWell(
                  onTap: () async {
                    await HttpService.register(email, password, context);
                  },
                  child: Container(
                    margin: const EdgeInsets.symmetric(
                        horizontal: 20, vertical: 10),
                    child: const Center(
                      child: Text(
                        "Register",
                        style: TextStyle(
                            fontWeight: FontWeight.bold, color: Colors.white),
                      ),
                    ),
                    height: 50,
                    width: double.infinity,
                    decoration: BoxDecoration(
                        color: Colors.red,
                        borderRadius: BorderRadius.circular(25)),
                  ))
            ],
          ),
        )
      // ignore: avoid_unnecessary_containers
    );
  }
}

欢迎文件中的代码片断包括两个inkwell widget,包裹在一个Containerinkwell widget对用户进行的触摸作出反应。例如,标有login的那个在点击后会带你到登录页面,而另一个标有register的会导航到注册页面。

登录和注册文件中的代码片段包括两个TextField widget和一个InkWell widget,都被包裹在一个ContainerInkwell widget在点击时提交从文本中得到的数据。

你会观察到import 'package:loginwithapi/service/http_service.dart'; 上有一条红线,这是因为你正在导入http_service.dart 。我们将在稍后修复;在此之前,你可以清除导入的错误,你的应用程序应该如下图所示。

Welcome Page

demo

Login Page

Register Page

登录和注册逻辑

要编写登录和注册逻辑,在我们之前创建的服务文件夹中创建一个文件,并粘贴下面的代码片段。

import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:loginwithapi/views/dashboard.dart';
import 'package:loginwithapi/views/welcome.dart';

class HttpService {
  static final _client = http.Client();

  static var _loginUrl = Uri.parse('https://flaskflutterlogin.herokuapp.com/login');

  static var _registerUrl = Uri.parse('https://flaskflutterlogin.herokuapp.com/register');

  static login(email, password, context) async {
    http.Response response = await _client.post(_loginUrl, body: {
      "email": email,
      "password": password,
    });

    if (response.statusCode == 200) {
      print(jsonDecode(response.body));
      var json = jsonDecode(response.body);

      if (json[0] == 'success') {
        await EasyLoading.showSuccess(json[0]);
        await Navigator.push(
            context, MaterialPageRoute(builder: (context) => Dashboard()));
      } else {
        EasyLoading.showError(json[0]);
      }
    } else {
      await EasyLoading.showError(
          "Error Code : ${response.statusCode.toString()}");
    }
  }

  static register(email, password, context) async {
    http.Response response = await _client.post(_registerUrl, body: {
      "email": email,
      "password": password,
    });

    if (response.statusCode == 200) {
      var json = jsonDecode(response.body);
      if (json[0] == 'username already exist') {
        await EasyLoading.showError(json[0]);

      } else {
        await EasyLoading.showSuccess(json[0]);
        Navigator.pushReplacement(
            context, MaterialPageRoute(builder: (context) => Dashboard()));
      }
    } else {
      await EasyLoading.showError(
          "Error Code : ${response.statusCode.toString()}");
    }
  }
}

代码import 'package:http/http.dart' as http; 正在导入http,这是一个flutter包,使flutter应用程序能够从API端点获取和发布数据。import 'dart:convert'; 帮助encodingdecoding 数据。

除了import 'package:loginwithapi/views/dashboard.dart'; ,其余的导入都很熟悉,我们稍后会看一下。

在所有导入之后,创建了HttpService 类,其中_loginUrl 被声明为持有登录URL端点的类的一个属性,_registerUrl 被声明为持有注册URL端点的一个属性。

创建了login 方法,该方法以email and password 为参数。asyncawait 被用来加速fetching,posting 数据,因为从服务器访问数据需要时间。

该方法中的条件语句是检查端点的response 。如果response200 ,这意味着success ,代码解码响应并使用fluttereasy_loading 。这是一个用于在flutter应用程序中发出消息提醒的包。它可以根据应用程序服务器端给出的条件,发送错误或成功消息。

在我们的例子中,如果解码后的响应是'success' ,那么就会弹出一个成功提示信息,告诉用户登录成功。然后用户将被导航到下一个页面,否则将弹出一个错误信息,告诉用户凭证是错误的,如下图所示。

wrong login credentials error message

Login success

注册方法的参数与登录方法的参数相同。它也像注册方法一样使用async and await 。唯一不同的是,错误信息被解码了。

Easy_loading 如果用户存在,将弹出消息 。如果用户不存在,将弹出一个成功信息 ,如下图所示。user already exists registered successfully

User already exist

Register success

总结

我们建立了一个可以使用API端点注册和登录用户的应用程序。flutter代码的GitHub仓库可以在这里找到,Flask API的仓库可以在这里找到。