《Flutter全栈开发实战指南:从零到高级》- 16 -用户认证与授权

引言

上一篇文章中,我们学习了本地数据存储相关的知识点。今天我们探讨一下几乎所有现代应用都无法绕开的核心模块——用户认证与授权

想象一下这个场景:用户打开你的App,输入账号密码登录。之后,无论他是重启App、刷新页面,他的登录状态都依然保持。他可以进行一些操作,比如发布内容、查看个人资料,但无法访问和管理他人的数据。

这一整套流畅、安全体验的背后,就是由认证授权 两大技术在支撑。

  • 认证:解决“你是谁?”的问题。验证用户身份,比如通过密码、指纹、面部识别或第三方令牌。最典型的实际业务场景就是登录
  • 授权:解决“你能做什么?”的问题。验证用户是否有权限执行某项操作或访问某些资源。比如,普通用户无法进入管理员后台。

以上这些都是JTW经典使用场景,下面开始一步步带你从零构建一套完整得可扩展的Flutter认证授权体系。我们会用到 JWTSharedPreferencesProvider 等核心技术。


一、 JWT

在开始写代码之前,我们必须先理解下我们将要使用的核心安全令牌——JWT

1.1 什么是JWT?

JWT,全称 JSON Web Token,是一种开放标准。它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为JSON对象。

可以把它理解成一个数字身份证。这个身份证里包含了你的基本信息,比如:姓名、身份证号,并且有防伪标识。

1.2 JWT的组成结构

一个JWT通常长这样:xxxxx.yyyyy.zzzzz,由三部分组成,用点号分隔。

  1. Header
  2. Payload
  3. Signature

下面我们画一下JWT结构图,加深理解:

JWT: header.payload.signature
Header
Payload
Signature
声明类型/算法
e.g. HS256
存放实际数据
e.g. 用户ID, 过期时间
用于验证发送者
防篡改
  • Header: 通常由两部分组成,令牌的类型(即JWT)和所使用的签名算法(如HMAC SHA256或RSA)。

    {
      "alg": "HS256",
      "typ": "JWT"
    }
    

    然后,这个JSON会被Base64Url编码,形成JWT的第一部分。

  • Payload: 这里是令牌的主要内容,并包含了一些声明。声明是关于用户实体和其他数据的描述。有三种类型的声明:注册声明公共声明私有声明

    • 注册声明: 预定义的一些声明,建议但不强制使用。如 iss(签发者),exp(过期时间),sub(主题)等。
    • 公共声明: 可以随意定义,但为避免冲突,应在IANA JSON Web Token Registry中定义或使用包含防冲突命名空间的URI。
    • 私有声明: 在提供者和消费者之间共享的自定义声明。

    一个典型的Payload可能如下:

    {
      "sub": "123456789", // 用户ID
      "name": "马保国",
      "admin": true,      // 用户角色/权限
      "iat": 1516239022   // 签发时间
    }
    

    同样,这个JSON也会被Base64Url编码,形成JWT的第二部分。

  • Signature: 这是最核心的部分,用于防止令牌被篡改。生成签名需要一个秘钥(注:只有服务器知道)以及前两部分(Header和Payload)的Base64Url编码后的字符串。

    比如,使用HMAC SHA256算法的签名如下:

    HMACSHA256(
      base64UrlEncode(header) + "." + base64UrlEncode(payload),
      secret)
    

    签名用于验证消息在传递过程中没有被更改。对于使用私钥签名的令牌,它还可以验证JWT的发送方是否为它所称的发送方。

1.3 为什么选择JWT?
  • 无状态: 服务器不需要在服务端存储会话信息,令牌自身包含了所有用户信息;
  • 可扩展性: Payload可以存放自定义信息,方便传递用户角色、权限等;
  • 跨域友好: 基于JSON,非常适合RESTful API和跨域场景;
1.4 JWT的工作流程

理解了JWT是什么之后,我们来看它在整个App生命周期中是如何工作的。下图清晰地展示了一个完整的认证流程:

用户Flutter App后端服务器1. 输入账号密码登录2. 发送登录请求 (POST /login)3. 验证账号密码4. 生成JWT (含用户ID/角色)5. 返回JWT令牌6. 安全存储JWT (如本地存储)7. 在请求头携带JWT(Authorization: Bearer <token>)8. 验证JWT签名和有效期9. 返回请求的数据loop[后续请求]Token过期后...10. 请求携带过期Token11. 返回 401 Unauthorized12. 使用Refresh Token请求新AT13. 返回新的Access Token14. 更新存储的Token用户Flutter App后端服务器

这个流程非常重要,后续内容都是围绕它展开的,记住这个流程也就学会了JWT。


二、 搭建Flutter认证体系

下面我们将按照步骤一步步构建Flutter端认证架构:

  1. Model定义:创建User和Auth相关的数据模型。
  2. Service层:封装登录、注册等API请求。
  3. Token管理:安全地存储和获取JWT。
  4. State管理:使用Provider管理全局的认证状态。
  5. 路由守卫:实现权限控制,保护需要登录的页面。
  6. 第三方登录:集成Apple等快捷登录。
2.1 第一步:定义数据模型

首先,我们需要创建一些模型类来表示用户数据和认证响应。

用户模型 :user_model.dart

/// User Model:服务端返回的用户信息
class User {
  final String id;
  final String email;
  final String? name; 
  final String? avatarUrl; // 头像URL

  User({
    required this.id,
    required this.email,
    this.name,
    this.avatarUrl,
  });

  /// JSON Map反序列化
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] ?? json['_id'], 
      email: json['email'],
      name: json['name'],
      avatarUrl: json['avatarUrl'],
    );
  }

  /// 序列化为JSON Map
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'email': email,
      'name': name,
      'avatarUrl': avatarUrl,
    };
  }
}

认证响应模型 :auth_response.dart

/// 登录/注册API的响应模型
class AuthResponse {
  final bool success;
  final String message;
  final String? accessToken;  // 访问令牌
  final String? refreshToken; // 刷新令牌
  final User? user; // 用户信息

  AuthResponse({
    required this.success,
    required this.message,
    this.accessToken,
    this.refreshToken,
    this.user,
  });

  factory AuthResponse.fromJson(Map<String, dynamic> json) {
    return AuthResponse(
      success: json['success'],
      message: json['message'],
      accessToken: json['data']?['accessToken'], 
      refreshToken: json['data']?['refreshToken'],
      user: json['data']?['user'] != null
          ? User.fromJson(json['data']['user'])
          : null,
    );
  }
}
2.2 第二步:创建认证服务

接下来,我们创建一个AuthService类,它负责所有与认证相关的网络请求。

认证服务 :auth_service.dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'models/auth_response.dart';

/// 认证服务类:封装所有与后端认证相关的API调用
class AuthService {
  static const String _baseUrl = 'https://xxxx-api.com/api/v1'; 

  final http.Client client;

  // 依赖注入http.Client
  AuthService({required this.client});

  /// 用户登录
  Future<AuthResponse> login(String email, String password) async {
    try {
      final response = await client.post(
        Uri.parse('$_baseUrl/auth/login'),
        headers: {'Content-Type': 'application/json'},
        body: jsonEncode({
          'email': email,
          'password': password,
        }),
      );

      if (response.statusCode == 200) {
        // 请求成功
        final jsonResponse = jsonDecode(response.body);
        return AuthResponse.fromJson(jsonResponse);
      } else {
        // 请求失败
        final errorData = jsonDecode(response.body);
        return AuthResponse(
          success: false,
          message: errorData['message'] ?? '登录失败,请重试',
        );
      }
    } catch (e) {
      // 异常处理
      return AuthResponse(
        success: false,
        message: '网络连接异常: $e',
      );
    }
  }

  /// 用户注册
  Future<AuthResponse> register(String email, String password, String? name) async {
    // 实现逻辑与login类似,此处省略......
  }

  /// 使用Refresh Token刷新Access Token
  Future<AuthResponse> refreshToken(String refreshToken) async {
    // 当Access Token过期时调用此方法......
  }

  /// 退出登录
  Future<bool> logout() async {
    // 通常需要调用后端API使令牌失效,同时App端清楚本地存储的Token......
  }
}
2.3 第三步:JWT令牌管理与持久化

用户登录成功后会收到JWT。我们不能每次都让用户重新登录,需要把令牌持久化地存储在设备上。在Flutter中,可以用 shared_preferences 插件来实现简单的本地存储。

令牌管理器 :token_manager.dart

import 'package:shared_preferences/shared_preferences.dart';

/// JWT令牌管理类:负责令牌的存储、获取和清除
class TokenManager {
  static const String _keyAccessToken = 'access_token';
  static const String _keyRefreshToken = 'refresh_token';
  static const String _keyUserInfo = 'user_info'; 

  static late final SharedPreferences _prefs;

  /// 初始化
  static Future<void> init() async {
    _prefs = await SharedPreferences.getInstance();
  }

  /// 保存Access Token
  static Future<void> saveAccessToken(String token) async {
    await _prefs.setString(_keyAccessToken, token);
  }

  /// 获取Access Token
  static String? getAccessToken() {
    return _prefs.getString(_keyAccessToken);
  }

  /// 保存Refresh Token
  static Future<void> saveRefreshToken(String token) async {
    await _prefs.setString(_keyRefreshToken, token);
  }

  /// 获取Refresh Token
  static String? getRefreshToken() {
    return _prefs.getString(_keyRefreshToken);
  }

  /// 保存用户基本信息
  static Future<void> saveUserInfo(String userJson) async {
    await _prefs.setString(_keyUserInfo, userJson);
  }

  /// 获取用户基本信息
  static String? getUserInfo() {
    return _prefs.getString(_keyUserInfo);
  }

  /// 退出登录后要清除所有令牌和用户信息
  static Future<void> clearAll() async {
    await _prefs.remove(_keyAccessToken);
    await _prefs.remove(_keyRefreshToken);
    await _prefs.remove(_keyUserInfo);
  }
}

重要提示shared_preferences 对于存储简单的Token信息是足够的,但它并不是绝对安全的存储方案。比如金融行业对于安全性要求极高的应用,应考虑使用 flutter_secure_storage 这类插件,它利用Keychain(iOS)和Keystore(Android)提供更高级别的安全保障。

2.4 第四步:全局认证状态管理

现在我们已经知道如何存储令牌了,还需要知道状态管理,让整个App都知道当前的登录状态。可以使用 provider 包来创建一个全局的认证状态管理器。

认证状态Model :auth_model.dart

import 'package:flutter/foundation.dart';
import 'user_model.dart';
import '../services/auth_service.dart';
import '../utils/token_manager.dart';

/// 认证状态模型,使用ChangeNotifier,当状态改变时通知所有监听者
class AuthModel with ChangeNotifier {
  User? _user; 
  String? _accessToken; 
  bool _isLoading = false; 

  // 获取当前状态
  User? get user => _user;
  String? get accessToken => _accessToken;
  bool get isLoading => _isLoading;
  bool get isAuth => _accessToken != null; 

  final AuthService _authService;

  AuthModel({required AuthService authService}) : _authService = authService {
    // 当AuthModel创建时,尝试从本地加载令牌和用户信息
    _loadStoredAuthInfo();
  }

  /// 从本地存储加载认证信息
  Future<void> _loadStoredAuthInfo() async {
    _accessToken = TokenManager.getAccessToken();
    String? userJson = TokenManager.getUserInfo();
    if (userJson != null) {
      // 注意:这里需要根据你的存储方式反序列化User对象
      // _user = User.fromJson(jsonDecode(userJson));
    }
    notifyListeners(); 
  }

  /// 登录方法
  Future<bool> login(String email, String password) async {
    _isLoading = true;
    notifyListeners(); // 通知UI开始加载

    final response = await _authService.login(email, password);

    _isLoading = false;

    if (response.success && response.accessToken != null) {
      // 登录成功
      _user = response.user;
      _accessToken = response.accessToken;

      // 持久化令牌和用户信息
      await TokenManager.saveAccessToken(_accessToken!);
      if (response.refreshToken != null) {
        await TokenManager.saveRefreshToken(response.refreshToken!);
      }
      if (_user != null) {
        await TokenManager.saveUserInfo(jsonEncode(_user!.toJson()));
      }

      notifyListeners(); // 通知UI状态已改变(已登录)
      return true;
    } else {
      // 登录失败,可以在这里处理错误提示
      return false;
    }
  }

  /// 退出登录
  Future<void> logout() async {
    // 调用服务端的退出接口
    await _authService.logout();
    
    // 清除本地存储
    await TokenManager.clearAll();
    
    // 清除内存中的状态
    _user = null;
    _accessToken = null;
    
    notifyListeners(); // 通知UI状态已改变(已登出)
  }

  /// 设置加载状态
  void setLoading(bool loading) {
    _isLoading = loading;
    notifyListeners();
  }
}

现在,我们需要在App的顶层注入这个AuthModel

main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'models/auth_model.dart';
import 'services/auth_service.dart';
import 'utils/token_manager.dart';

void main() async {
  // 确保Flutter绑定已初始化
  WidgetsFlutterBinding.ensureInitialized();
  
  // 初始化令牌管理器
  await TokenManager.init();

  runApp(
    // 使用MultiProvider在根节点提供多个状态模型
    MultiProvider(
      providers: [
        ChangeNotifierProvider<AuthModel>(
          create: (ctx) => AuthModel(
            authService: AuthService(client: http.Client()),
          ),
        ),
        // 还可以添加其他Provider,如UserModel, CartModel等
      ],
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter全栈开发',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const SplashPage(), // 启动页
      routes: {
        // 定义路由......
      },
    );
  }
}
2.5 第五步:路由守卫与权限控制

现在,我们的App已经知道用户是否登录了。接下来,我们要保护那些需要登录才能访问的页面。这就是路由守卫

我们将创建一个 AuthGuard 组件,放在需要保护的页面外面。

认证守卫 :auth_guard.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/auth_model.dart';

/// 认证守卫组件
/// 已登录,则显示子组件;未登录,则调整登录页
class AuthGuard extends StatelessWidget {
  final Widget child;

  const AuthGuard({Key? key, required this.child}) : super(key: key);

  
  Widget build(BuildContext context) {
    final authModel = Provider.of<AuthModel>(context);

    return authModel.isAuth
        ? child // 已登录,放行!
        : const LoginPage(); // 未登录,跳转到登录页
        // ......
  }
}

使用案例:在需要保护的页面外包裹AuthGuard

// 在路由表中使用
// MaterialApp(
//   ...
//   routes: {
//     '/profile': (ctx) => AuthGuard(child: ProfilePage()),
//     '/settings': (ctx) => AuthGuard(child: SettingsPage()),
//   },
// )

// 或者使用Navigator.push时
Navigator.of(context).push(
  MaterialPageRoute(
    builder: (ctx) => AuthGuard(child: const ProfilePage()),
  ),
);
2.6 第六步:集成第三方登录

让用户注册新账号总是有成本的。集成第三方登录可以极大提升用户体验和转化率。这里我们以 Google登录 为例。

1. 添加依赖
pubspec.yaml 中添加:

dependencies:
  google_sign_in: ^6.1.1

2. 配置平台

  • Android: 需要在Firebase控制台创建项目,并配置SHA-1证书指纹,然后将 google-services.json 文件放到 android/app/ 目录下。
  • iOS: 需要在Apple Developer中心配置Bundle Identifier,并在Xcode中配置URL Schemes。

3. 实现Google登录

import 'package:google_sign_in/google_sign_in.dart';

class GoogleSignInService {
  static final _googleSignIn = GoogleSignIn(
    // 配置客户端ID
    // serverClientId: 'xxxxxx-client-id',
  );

  /// Google登录
  static Future<GoogleSignInAccount?> signIn() async {
    try {
      // 触发Google登录流程
      final account = await _googleSignIn.signIn();
      if (account == null) {
        // 用户取消了登录
        return null;
      }

      // 获取认证信息(包含idToken和accessToken)
      final authentication = await account.authentication;

      // 这里我们需要idToken,发送给服务器进行验证
      final String? idToken = authentication.idToken;

      print('Google ID Token: $idToken');
      
      return account;
    } catch (error) {
      print('Google登录失败: $error');
      return null;
    }
  }

  /// 退出Google登录
  static Future<void> signOut() async {
    await _googleSignIn.signOut();
  }
}

4. 将Google登录集成到AuthModel中
在我们的AuthModel中添加一个方法:

/// 在AuthModel中添加
Future<bool> loginWithGoogle() async {
  _isLoading = true;
  notifyListeners();

  // 1. 获取Google的idToken
  final googleAccount = await GoogleSignInService.signIn();
  final googleAuth = await googleAccount?.authentication;
  final String? idToken = googleAuth?.idToken;

  if (idToken == null) {
    _isLoading = false;
    notifyListeners();
    return false;
  }

  // 2. 将idToken发送给服务器
  // 服务器会验证idToken的有效性,并生成JWT
  try {
    final response = await http.post(
      Uri.parse('$_baseUrl/auth/google'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'idToken': idToken}),
    );

    _isLoading = false;

    if (response.statusCode == 200) {
      final jsonResponse = jsonDecode(response.body);
      final authResponse = AuthResponse.fromJson(jsonResponse);

      if (authResponse.success && authResponse.accessToken != null) {
        // 3. 登录成功后的处理逻辑
        _user = authResponse.user;
        _accessToken = authResponse.accessToken;
        await TokenManager.saveAccessToken(_accessToken!);
        if (authResponse.refreshToken != null) {
          await TokenManager.saveRefreshToken(authResponse.refreshToken!);
        }
        if (_user != null) {
          await TokenManager.saveUserInfo(jsonEncode(_user!.toJson()));
        }
        notifyListeners();
        return true;
      }
    }
    return false;
  } catch (e) {
    _isLoading = false;
    notifyListeners();
    print('请求失败: $e');
    return false;
  }
}

Apple登录的流程与此类似,需要使用 sign_in_with_apple 插件,并在iOS端进行相应配置。


三、 总结

下面我们用一张完整的架构脉络图来梳理一下我们今天构建的整个Flutter认证授权体系:

External Services
Flutter App
Backend API
服务器
Google Sign-In
AuthGuard
路由守卫
UI Components
登录页/主页/个人页
AuthModel
状态管理
AuthService
API调用
TokenManager
持久化存储
本地存储
SharedPreferences

核心知识点

  1. JWT原理: 是一种无状态、自包含的令牌,由Header、Payload、Signature三部分组成,用于客户端和服务器之间安全传递认证信息。
  2. 令牌管理: 如何使用 shared_preferences 在本地存储和管理JWT令牌,实现登录状态持久化。
  3. 状态管理: 使用 ProviderChangeNotifier 创建全局的的认证状态,动态更新UI。
  4. 路由守卫: 使用声明式方式实现了 AuthGuard 组件,保护需要登录才能访问的页面。
  5. 服务封装: 将所有的网络请求逻辑封装在 AuthService 中,易维护和测试。
  6. 第三方登录: 集成了Google登录的基本流程,即客户端获取第三方令牌,然后交由自己的后端服务器验证并换发自家JWT的OAuth2流程。

四、 写在最后

至此Flutter用户认证与授权知识就完全讲完了,从理解JWT的原理,到令牌的存储管理,再到全局状态的响应式更新和精细的页面权限控制,最后到第三方登录的集成,其中每一个步骤都是至关重要的。希望这篇文章对你有所帮助!我们下期见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

QuantumLeap丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值