引言
上一篇文章中,我们学习了本地数据存储相关的知识点。今天我们探讨一下几乎所有现代应用都无法绕开的核心模块——用户认证与授权。
想象一下这个场景:用户打开你的App,输入账号密码登录。之后,无论他是重启App、刷新页面,他的登录状态都依然保持。他可以进行一些操作,比如发布内容、查看个人资料,但无法访问和管理他人的数据。
这一整套流畅、安全体验的背后,就是由认证 和授权 两大技术在支撑。
- 认证:解决“你是谁?”的问题。验证用户身份,比如通过密码、指纹、面部识别或第三方令牌。最典型的实际业务场景就是登录。
- 授权:解决“你能做什么?”的问题。验证用户是否有权限执行某项操作或访问某些资源。比如,普通用户无法进入管理员后台。
以上这些都是JTW经典使用场景,下面开始一步步带你从零构建一套完整得可扩展的Flutter认证授权体系。我们会用到 JWT、SharedPreferences、Provider 等核心技术。
一、 JWT
在开始写代码之前,我们必须先理解下我们将要使用的核心安全令牌——JWT。
1.1 什么是JWT?
JWT,全称 JSON Web Token,是一种开放标准。它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为JSON对象。
可以把它理解成一个数字身份证。这个身份证里包含了你的基本信息,比如:姓名、身份证号,并且有防伪标识。
1.2 JWT的组成结构
一个JWT通常长这样:xxxxx.yyyyy.zzzzz,由三部分组成,用点号分隔。
- Header
- Payload
- Signature
下面我们画一下JWT结构图,加深理解:
-
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生命周期中是如何工作的。下图清晰地展示了一个完整的认证流程:
这个流程非常重要,后续内容都是围绕它展开的,记住这个流程也就学会了JWT。
二、 搭建Flutter认证体系
下面我们将按照步骤一步步构建Flutter端认证架构:
- Model定义:创建User和Auth相关的数据模型。
- Service层:封装登录、注册等API请求。
- Token管理:安全地存储和获取JWT。
- State管理:使用Provider管理全局的认证状态。
- 路由守卫:实现权限控制,保护需要登录的页面。
- 第三方登录:集成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认证授权体系:
核心知识点:
- JWT原理: 是一种无状态、自包含的令牌,由Header、Payload、Signature三部分组成,用于客户端和服务器之间安全传递认证信息。
- 令牌管理: 如何使用
shared_preferences在本地存储和管理JWT令牌,实现登录状态持久化。 - 状态管理: 使用
Provider和ChangeNotifier创建全局的的认证状态,动态更新UI。 - 路由守卫: 使用声明式方式实现了
AuthGuard组件,保护需要登录才能访问的页面。 - 服务封装: 将所有的网络请求逻辑封装在
AuthService中,易维护和测试。 - 第三方登录: 集成了Google登录的基本流程,即客户端获取第三方令牌,然后交由自己的后端服务器验证并换发自家JWT的OAuth2流程。
四、 写在最后
至此Flutter用户认证与授权知识就完全讲完了,从理解JWT的原理,到令牌的存储管理,再到全局状态的响应式更新和精细的页面权限控制,最后到第三方登录的集成,其中每一个步骤都是至关重要的。希望这篇文章对你有所帮助!我们下期见!
5

被折叠的 条评论
为什么被折叠?



