使用 Laravel API 和 Passport 验证 Flutter 应用程序

115 阅读8分钟

安装 Flutter 包

http包,消耗HTTP资源。

shared_preferences包,为简单数据提供持久存储。

将包添加到您的pubspec.yaml文件

dependencies:  
**shared_preferences: ^0.5.6+1  
****http: ^0.12.0+4**

运行命令flutter pub get

注意:您还可以选择使用SQLiteHive包处理数据存储,特别是对于大型数据结构。

像这样创建文件夹结构:

lib目录中,创建两个文件夹:

network_utilsscreen 。

在 screen 目录,创建 login.dart, home.dart, register.dart 并创造 api.dart 文件输入 network_utils 文件夹。

api.dart文件中,编写一个管理所有API方法的Network类


import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';

class Network{
  final String _url = 'http://localhost:8000/api/v1';
  //if you are using android studio emulator, change localhost to 10.0.2.2
  var token;

  _getToken() async {
    SharedPreferences localStorage = await SharedPreferences.getInstance();
    token = jsonDecode(localStorage.getString('token'))['token'];
  }
  
  authData(data, apiUrl) async {
    var fullUrl = _url + apiUrl;
    return await http.post(
        fullUrl,
        body: jsonEncode(data),
        headers: _setHeaders()
    );
  }

  getData(apiUrl) async {
    var fullUrl = _url + apiUrl;
    await _getToken();
    return await http.get(
        fullUrl,
        headers: _setHeaders()
    );
  }

  _setHeaders() => {
    'Content-type' : 'application/json',
    'Accept' : 'application/json',
    'Authorization' : 'Bearer $token'
  };

}

Network类中,定义 String _url = 'http://localhost:8000/api/v1 这基本上是我们Laravel API 的 BASE_URL。

如果您使用的是 android 模拟器,请将localhost:8000更改10.0.2.2:8000

另外,定义一个变量token,这将存储用于身份验证的 API 令牌,并将附加到每个需要身份验证的请求。

接下来我们写一个异步函数 _getToken

_getToken()async{  
SharedPreferences localStorage = await SharedPreferences.*getInstance*();  
token = jsonDecode(localStorage.getString('token'))['token'];  
}

该函数检查用户设备中存储的令牌并将其分配给最初定义的String token;

我们使用SharedPreferences来获取设备的localStorage实例,在其中使用_getString方法检索登录或注册期间存储的密钥token  使用jsonDecode包装此方法,将检索到的键/值对转换为数组。这样,我们就可以访问值['token'] 。

authData()函数是一个异步函数,用于处理对我们的登录注册API 端点的所有post请求,它需要两个参数: dataapiUrl 。

我们通过附加 baseUrl _url和传递给函数的apiUrl来构建fullUrl , http.post()方法采用fullUrl 、 bodyheaders来发出POST请求。 authData函数返回此请求的响应。

您可能已经观察到,对于headers参数,我们调用_setHeaders()来传递标头,如下所示:

_setHeaders() => {  
'Content-type' : 'application/json',  
'Accept' : 'application/json',  
'Authorization' : 'Bearer $token'  
};

这使得它可重用且更干净,因为我们在发出请求时必须到处调用它。

接下来是getData()函数,这也是一个带有参数apiUrlasync函数,如果您的查询需要传递一组参数您可以添加更多参数,例如data 。就本文而言,我们的GET请求不带额外参数。

getData()函数中,我们构建类似于postData fullUrl并调用_getToken()以确保设置了token ,否则它将为空,并且我们的端点将返回unauthorized我们没有为authData()这样做方法,因为注册登录路由不需要身份验证。

http.get()方法帮助发送我们的GET请求,这里没有传递body参数。

这是我们应用程序的主要入口点,在这里我们将检查用户是否经过身份验证并返回登录屏幕或主屏幕。

import 'package:flutter/material.dart';
import 'package:tutorial_app/screen/login.dart';
import 'package:tutorial_app/screen/home.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Test App',
      debugShowCheckedModeBanner: false,
      home: CheckAuth(),
    );
  }
}

class CheckAuth extends StatefulWidget {
  @override
  _CheckAuthState createState() => _CheckAuthState();
}

class _CheckAuthState extends State<CheckAuth> {
  bool isAuth = false;
  @override
  void initState() {
    _checkIfLoggedIn();
    super.initState();
  }

  void _checkIfLoggedIn() async{
    SharedPreferences localStorage = await SharedPreferences.getInstance();
    var token = localStorage.getString('token');
    if(token != null){
      setState(() {
        isAuth = true;
      });
    }
  }
  @override
  Widget build(BuildContext context) {
    Widget child;
    if (isAuth) {
      child = Home();
    } else {
      child = Login();
    }
    return Scaffold(
      body: child,
    );
  }
}

_CheckAuthState 我们定义一个布尔值 isAuth = false; 在 initState() 我们打电话 _checkIfLoggedIn 这个方法检查 localStorage 的可用性 token 和集 isAuth = true 像这样:

void _checkIfLoggedIn() async{  
SharedPreferences localStorage = await SharedPreferences.*getInstance*();  
var token = localStorage.getString('token');  
if(token != null){  
setState(() {  
isAuth = true;  
});  
}  
}

最后,在构建方法中,我们检查是否isAuth = true并返回Home() else Login()

我们就在这里,如果到目前为止您没有遇到任何错误,请 注册 登录

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:tutorial_app/network_utils/api.dart';
import 'package:tutorial_app/screen/home.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tutorial_app/screen/register.dart';
class Login extends StatefulWidget {
  @override
  _LoginState createState() => _LoginState();
}

class _LoginState extends State<Login> {
  bool _isLoading = false;
  final _formKey = GlobalKey<FormState>();
  var email;
  var password;
  final _scaffoldKey = GlobalKey<ScaffoldState>();
  _showMsg(msg) {
    final snackBar = SnackBar(
      content: Text(msg),
      action: SnackBarAction(
        label: 'Close',
        onPressed: () {
          // Some code to undo the change!
        },
      ),
    );
    _scaffoldKey.currentState.showSnackBar(snackBar);
  }
  @override
  Widget build(BuildContext context) {
    // Build a Form widget using the _formKey created above.
    return Scaffold(
      key: _scaffoldKey,
      body: Container(
          color: Colors.teal,
          child: Stack(
            children: <Widget>[
              Positioned(
                child: Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      Card(
                        elevation: 4.0,
                        color: Colors.white,
                        margin: EdgeInsets.only(left: 20, right: 20),
                        shape: RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(15)),
                        child: Padding(
                          padding: const EdgeInsets.all(10.0),
                          child: Form(
                            key: _formKey,
                            child: Column(
                              mainAxisAlignment: MainAxisAlignment.center,
                              children: <Widget>[

                                TextFormField(
                                  style: TextStyle(color: Color(0xFF000000)),
                                  cursorColor: Color(0xFF9b9b9b),
                                  keyboardType: TextInputType.text,
                                  decoration: InputDecoration(
                                    prefixIcon: Icon(
                                      Icons.email,
                                      color: Colors.grey,
                                    ),
                                    hintText: "Email",
                                    hintStyle: TextStyle(
                                        color: Color(0xFF9b9b9b),
                                        fontSize: 15,
                                        fontWeight: FontWeight.normal),
                                  ),
                                  validator: (emailValue) {
                                    if (emailValue.isEmpty) {
                                      return 'Please enter email';
                                    }
                                    email = emailValue;
                                    return null;
                                  },
                                ),
                                TextFormField(
                                  style: TextStyle(color: Color(0xFF000000)),
                                  cursorColor: Color(0xFF9b9b9b),
                                  keyboardType: TextInputType.text,
                                  obscureText: true,
                                  decoration: InputDecoration(
                                    prefixIcon: Icon(
                                      Icons.vpn_key,
                                      color: Colors.grey,
                                    ),
                                    hintText: "Password",
                                    hintStyle: TextStyle(
                                        color: Color(0xFF9b9b9b),
                                        fontSize: 15,
                                        fontWeight: FontWeight.normal),
                                  ),
                                  validator: (passwordValue) {
                                    if (passwordValue.isEmpty) {
                                      return 'Please enter some text';
                                    }
                                    password = passwordValue;
                                    return null;
                                  },
                                ),
                                Padding(
                                  padding: const EdgeInsets.all(10.0),
                                  child: FlatButton(
                                    child: Padding(
                                      padding: EdgeInsets.only(
                                          top: 8, bottom: 8, left: 10, right: 10),
                                      child: Text(
                                        _isLoading? 'Proccessing...' : 'Login',
                                        textDirection: TextDirection.ltr,
                                        style: TextStyle(
                                          color: Colors.white,
                                          fontSize: 15.0,
                                          decoration: TextDecoration.none,
                                          fontWeight: FontWeight.normal,
                                        ),
                                      ),
                                    ),
                                    color: Colors.teal,
                                    disabledColor: Colors.grey,
                                    shape: new RoundedRectangleBorder(
                                        borderRadius:
                                        new BorderRadius.circular(20.0)),
                                    onPressed: () {
                                      if (_formKey.currentState.validate()) {
                                        _login();
                                      }
                                    },
                                  ),
                                ),
                              ],
                            ),
                          ),
                        ),
                      ),

                      Padding(
                        padding: const EdgeInsets.only(top: 20),
                        child: InkWell(
                          onTap: () {
                            Navigator.push(
                                context,
                                new MaterialPageRoute(
                                    builder: (context) => Register()));
                          },
                          child: Text(
                            'Create new Account',
                            style: TextStyle(
                              color: Colors.white,
                              fontSize: 15.0,
                              decoration: TextDecoration.none,
                              fontWeight: FontWeight.normal,
                            ),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              )
            ],
          ),
        ),
    );
  }
  void _login() async{
    setState(() {
      _isLoading = true;
    });
    var data = {
      'email' : email,
      'password' : password
    };

    var res = await Network().authData(data, '/login');
    var body = json.decode(res.body);
    if(body['success']){
      SharedPreferences localStorage = await SharedPreferences.getInstance();
      localStorage.setString('token', json.encode(body['token']));
      localStorage.setString('user', json.encode(body['user']));
      Navigator.push(
          context,
          new MaterialPageRoute(
              builder: (context) => Home()
          ),
      );
    }else{
      _showMsg(body['message']);
    }

    setState(() {
      _isLoading = false;
    });

  }
}

在 Login 类中,我们首先使用Form Widget 构建一个带有输入验证的基本登录表单。 定义一个布尔值_isLoading = false;我们将使用它来将login按钮的状态更改为processing... 此外,定义emailpassword变量,在验证后从各自的表单字段设置它们的值。我们将定义_formKey_scaffoldKey如下

final _formKey = GlobalKey<FormState>();This uniquely identifies the Form , and allows validation of the form in a later step. final _scaffoldKey = GlobalKey<ScaffoldState>();Assign a GlobalKey to the Scaffold, then use the key.currentState property to obtain the ScaffoldState for a snackBar later. 

现在,在构建小部件下方,我们编写 async _login()方法,如下所示:

void _login() async{  
setState(() {  
_isLoading = true;  
});  
var data = {  
'email' : email,  
'password' : password  
};  
  
var res = await Network().authData(data, '/login');  
var body = json.decode(res.body);  
if(body['success']){  
SharedPreferences localStorage = await SharedPreferences.*getInstance*();  
localStorage.setString('token', json.encode(body['token']));  
localStorage.setString('user', json.encode(body['user']));  
Navigator.*push*(  
context,  
new MaterialPageRoute(  
builder: (context) => Home()  
),  
);  
}else{  
_showMsg(body['message']);  
}  
  
setState(() {  
_isLoading = false;  
});  
  
}

我们设置 _isLoading = truelogin按钮更改 processing... 收集表单值并构建一个data对象,该数据对象将传递给我们的 API 调用 var res = await Network().authData(data, '/login) 定义一个 var res = await Network().authData(data, '/login) 在这里,我们对authData()方法进行API 调用,该方法最初是在Network类中的api.dart文件中创建的,还记得它需要两个参数吗?因此,我们传入dataAPI 路由'/login' 

一旦我们的后端服务收到响应,它将存储在res变量中,然后我们将此 JSON res.body解码 var body = json.decode(res.body)

使用if 条件语句,我们检查body['success']这是来自我们 API 的布尔值,如果为 true ,那么我们已成功通过应用程序的身份验证,因此,我们使用SharedPreferencestokenuser设置localStorage值,如下所示在片段中。使用Navigator.push我们导航到Home()路线。

否则,如果我们的端点出现错误,我们将调用_showMsg()并传入消息body['message']这将触发包含错误消息的SnackBar通知。

最后,我们设置_isLoading = false 。

register.dartlogin.dart 类似, 显着的变化在于表单字段的数量以及 API 端点'/register' ,如下所示:

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:tutorial_app/network_utils/api.dart';
import 'package:tutorial_app/screen/home.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tutorial_app/screen/login.dart';

class Register extends StatefulWidget {
  @override
  _RegisterState createState() => _RegisterState();
}

class _RegisterState extends State<Register> {
  bool _isLoading = false;
  final _formKey = GlobalKey<FormState>();
  var email;
  var password;
  var fname;
  var lname;
  var phone;
  @override
  Widget build(BuildContext context) {
    return Material(
      child: Container(
        color: Colors.teal,
        child: Stack(
          children: <Widget>[
            Positioned(
              child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Card(
                      elevation: 4.0,
                      color: Colors.white,
                      margin: EdgeInsets.only(left: 20, right: 20),
                      shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(15)),
                      child: Padding(
                        padding: const EdgeInsets.all(10.0),
                        child: Form(
                          key: _formKey,
                          child: Column(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: <Widget>[

                              TextFormField(
                                style: TextStyle(color: Color(0xFF000000)),
                                cursorColor: Color(0xFF9b9b9b),
                                keyboardType: TextInputType.text,
                                decoration: InputDecoration(
                                  prefixIcon: Icon(
                                    Icons.email,
                                    color: Colors.grey,
                                  ),
                                  hintText: "Email",
                                  hintStyle: TextStyle(
                                      color: Color(0xFF9b9b9b),
                                      fontSize: 15,
                                      fontWeight: FontWeight.normal),
                                ),
                                validator: (emailValue) {
                                  if (emailValue.isEmpty) {
                                    return 'Please enter email';
                                  }
                                  email = emailValue;
                                  return null;
                                },
                              ),
                              TextFormField(
                                style: TextStyle(color: Color(0xFF000000)),
                                cursorColor: Color(0xFF9b9b9b),
                                keyboardType: TextInputType.text,
                                decoration: InputDecoration(
                                  prefixIcon: Icon(
                                    Icons.insert_emoticon,
                                    color: Colors.grey,
                                  ),
                                  hintText: "First Name",
                                  hintStyle: TextStyle(
                                      color: Color(0xFF9b9b9b),
                                      fontSize: 15,
                                      fontWeight: FontWeight.normal),
                                ),
                                validator: (firstname) {
                                  if (firstname.isEmpty) {
                                    return 'Please enter your first name';
                                  }
                                  fname = firstname;
                                  return null;
                                },
                              ),
                              TextFormField(
                                style: TextStyle(color: Color(0xFF000000)),
                                cursorColor: Color(0xFF9b9b9b),
                                keyboardType: TextInputType.text,
                                decoration: InputDecoration(
                                  prefixIcon: Icon(
                                    Icons.insert_emoticon,
                                    color: Colors.grey,
                                  ),
                                  hintText: "Last Name",
                                  hintStyle: TextStyle(
                                      color: Color(0xFF9b9b9b),
                                      fontSize: 15,
                                      fontWeight: FontWeight.normal),
                                ),
                                validator: (lastname) {
                                  if (lastname.isEmpty) {
                                    return 'Please enter your last name';
                                  }
                                  lname = lastname;
                                  return null;
                                },
                              ),
                              TextFormField(
                                style: TextStyle(color: Color(0xFF000000)),
                                cursorColor: Color(0xFF9b9b9b),
                                keyboardType: TextInputType.text,
                                decoration: InputDecoration(
                                  prefixIcon: Icon(
                                    Icons.phone,
                                    color: Colors.grey,
                                  ),
                                  hintText: "Phone",
                                  hintStyle: TextStyle(
                                      color: Color(0xFF9b9b9b),
                                      fontSize: 15,
                                      fontWeight: FontWeight.normal),
                                ),
                                validator: (phonenumber) {
                                  if (phonenumber.isEmpty) {
                                    return 'Please enter phone number';
                                  }
                                  phone = phonenumber;
                                  return null;
                                },
                              ),
                              TextFormField(
                                style: TextStyle(color: Color(0xFF000000)),
                                cursorColor: Color(0xFF9b9b9b),
                                keyboardType: TextInputType.text,
                                obscureText: true,
                                decoration: InputDecoration(
                                  prefixIcon: Icon(
                                    Icons.vpn_key,
                                    color: Colors.grey,
                                  ),
                                  hintText: "Password",
                                  hintStyle: TextStyle(
                                      color: Color(0xFF9b9b9b),
                                      fontSize: 15,
                                      fontWeight: FontWeight.normal),
                                ),
                                validator: (passwordValue) {
                                  if (passwordValue.isEmpty) {
                                    return 'Please enter some text';
                                  }
                                  password = passwordValue;
                                  return null;
                                },
                              ),
                              Padding(
                                padding: const EdgeInsets.all(10.0),
                                child: FlatButton(
                                  child: Padding(
                                    padding: EdgeInsets.only(
                                        top: 8, bottom: 8, left: 10, right: 10),
                                    child: Text(
                                      _isLoading? 'Proccessing...' : 'Register',
                                      textDirection: TextDirection.ltr,
                                      style: TextStyle(
                                        color: Colors.white,
                                        fontSize: 15.0,
                                        decoration: TextDecoration.none,
                                        fontWeight: FontWeight.normal,
                                      ),
                                    ),
                                  ),
                                  color: Colors.teal,
                                  disabledColor: Colors.grey,
                                  shape: new RoundedRectangleBorder(
                                      borderRadius:
                                      new BorderRadius.circular(20.0)),
                                  onPressed: () {
                                    if (_formKey.currentState.validate()) {
                                        _register();
                                    }
                                  },
                                ),
                              ),
                            ],
                          ),
                        ),
                      ),
                    ),

                    Padding(
                      padding: const EdgeInsets.only(top: 20),
                      child: InkWell(
                        onTap: () {
                          Navigator.push(
                              context,
                              new MaterialPageRoute(
                                  builder: (context) => Login()));
                        },
                        child: Text(
                          'Already Have an Account',
                          style: TextStyle(
                            color: Colors.white,
                            fontSize: 15.0,
                            decoration: TextDecoration.none,
                            fontWeight: FontWeight.normal,
                          ),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            )
          ],
        ),
      ),
    );
  }
  void _register()async{
    setState(() {
      _isLoading = true;
    });
    var data = {
      'email' : email,
      'password': password,
      'phone': phone,
      'fname': fname,
      'lname': lname
    };

    var res = await Network().authData(data, '/register');
    var body = json.decode(res.body);
    if(body['success']){
      SharedPreferences localStorage = await SharedPreferences.getInstance();
      localStorage.setString('token', json.encode(body['token']));
      localStorage.setString('user', json.encode(body['user']));
      Navigator.push(
        context,
        new MaterialPageRoute(
            builder: (context) => Home()
        ),
      );
    }

    setState(() {
      _isLoading = false;
    });
  }
}

lib/main.dart在这里,我们将检索存储在localStorage中的用户数据,并实现注销操作。

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:tutorial_app/screen/login.dart';
import 'package:tutorial_app/network_utils/api.dart';
import 'package:shared_preferences/shared_preferences.dart';
class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home>{
  String name;
  @override
  void initState(){
    _loadUserData();
    super.initState();
  }
  _loadUserData() async{
    SharedPreferences localStorage = await SharedPreferences.getInstance();
    var user = jsonDecode(localStorage.getString('user'));

    if(user != null) {
      setState(() {
        name = user['fname'];
      });
    }
  }
    @override
    Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Test App'),
            backgroundColor: Colors.teal,
          ),
          body: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Text('Hi, $name',
                  style: TextStyle(
                    fontWeight: FontWeight.bold
                    ),
                  ),
                  Center(
                    child: RaisedButton(
                      elevation: 10,
                      onPressed: (){
                        logout();
                      },
                      color: Colors.teal,
                      shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
                      child: Text('Logout'),
                    ),
                  ),
                ],
              ),
          ),
        );
      }

  void logout() async{
    var res = await Network().getData('/logout');
    var body = json.decode(res.body);
    if(body['success']){
      SharedPreferences localStorage = await SharedPreferences.getInstance();
      localStorage.remove('user');
      localStorage.remove('token');
      Navigator.push(
          context,
          MaterialPageRoute(builder: (context)=>Login()));
    }
  }
}

initState() 方法中,我们调用 _loadUserData返回注册/登录 期间存储在localStorage 中的用户数据

_loadUserData() async{  
SharedPreferences localStorage = await SharedPreferences.*getInstance*();  
var user = jsonDecode(localStorage.getString('user'));  
  
if(user != null) {  
setState(() {  
name = user['fname'];  
});  
}  
}

您可以将此用户数据设置为任何声明为保存用户数据的变量,我将user['fname'] 设置为 String name  ,这稍后将在 第 40 行的Text小部件中使用, 接下来,我们使用RaisedButton小部件创建一个注销按钮 ,并onPressed调用logout() 方法。

logout 方法调用Network类的getData方法,并在收到成功响应后传递apiUrl  /logout ,表明该令牌已被我们的后端护照服务失效,我们在localStorage上调用.remove()方法,这是一个SharedPreferences 的实例中, 我们传入token密钥并对user执行相同的操作并导航到Login()屏幕。