《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的原理,到令牌的存储管理,再到全局状态的响应式更新和精细的页面权限控制,最后到第三方登录的集成,其中每一个步骤都是至关重要的。希望这篇文章对你有所帮助!我们下期见!

posted on 2026-01-28 09:44  ljbguanli  阅读(2)  评论(0)    收藏  举报