发布于 2026-01-06 1 阅读
0

Flutter 中的基于角色的访问控制

Flutter 中的基于角色的访问控制

当我在推特上发起#30DaysOfFlutter挑战时(不好意思用了这个广告 xD),我从没想过自己能坚持超过4天。但今天是第8天,我觉得进展非常顺利。

虽然我经常在 Twitter 上分享我的 Flutter 学习心得,但今天的主题实在太精彩了,值得单独写一篇博文。我正在用 Flutter 开发一个包含不同用户角色(管理员、经理和普通用户)的副项目。这些角色需要不同的用户界面、功能和数据访问权限!所以我决定不在每个组件里都堆砌 if-else 语句,而是构建一个强大的系统来无缝控制访问权限。而这正是我们今天要学习的内容——如何在 Flutter 中实现基于角色的访问控制 (RBAC)。

起点

我们先从简单的开始!为了探索角色管理,我们将构建一个只有两个文件的基本应用程序app.dartmain.dart把它想象成一张空白画布。暂时忽略花哨的用户界面,我们会逐步增加复杂性。这样,我们就可以一步一步地专注于访问控制的核心概念。

app.dart

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'RBAC Demo',
      home: const MyHomePage(),
    );
  }
}

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('User model app'),
      ),
      body: Container(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

main.dart

import 'package:flutter/material.dart';
import 'package:userrole/app.dart';

void main() {
  runApp(const MyApp());
}
Enter fullscreen mode Exit fullscreen mode

现在我们来介绍一些功能。首先,我们介绍一个简单的按钮,它能创建一个带有消息的提示栏:

import 'package:flutter/material.dart';

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

  @override
  State<MessageButton> createState() => _MessageButtonState();
}

class _MessageButtonState extends State<MessageButton> {
  @override
  Widget build(BuildContext context) {
    return TextButton(
      onPressed: _showMessage,
      child: const Text('Show me'),
    );
  }

  _showMessage() {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('I am role based'),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

处理角色的最佳实践不是通过用户界面进行管理,而是应该在服务器端检查每个功能调用中的角色和权限。用户界面角色管理更有利于用户体验。

为了模拟真实世界的身份验证,假设我们在用户成功登录后获取用户数据(包括角色)。虽然我们可以使用安全的 JWT 令牌来实现这一点,但为了简化操作,我们定义用于将此数据存储在本地存储中的纯 JSON 格式。

{
  "username": "Sparsh",
  "email": "sparsh@gmail.com",
  "role": {
    "name": "admin",
    "level": 3
  }
}
Enter fullscreen mode Exit fullscreen mode

我们需要在 中创建一个合适的用户数据容器model.dart

import 'package:flutter/foundation.dart';

@immutable
class UserData {
  final String username;
  final String email;
  final UserRole role;

  const UserData({
    required this.username,
    required this.email,
    required this.role,
  });

  factory UserData.fromJson(Map<String, dynamic> json) {
    return UserData(
      username: json['username']!,
      email: json['email'],
      role: UserRole.fromJson(json['role']),
    );
  }
}

@immutable
class UserRole {
  final String name;
  final int level;

  const UserRole({
    required this.name,
    required this.level,
  });

  factory UserRole.fromJson(Map<String, dynamic> json) {
    return UserRole(
      name: json['name'],
      level: json['level'] as int,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

为了让用户能够轻松访问数据,我们将创建一个专门的文件来存放数据,用户可以从应用程序中的任何位置轻松访问该文件。

core.dart

import 'dart:convert' show jsonDecode;
import 'package:userrole/mdoel.dart';
import 'package:flutter/services.dart' show rootBundle;

class Core {
  static UserData? _user;

  UserData? get user => _user;

  Future<void> setUserData() async {
    // loads user data from shared preferences and sets it to
    // [user]
  }
}
Enter fullscreen mode Exit fullscreen mode

main.dart将改为以下内容:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  Core core = Core();
  await core.setUserData();
  runApp(const MyApp());
}
Enter fullscreen mode Exit fullscreen mode

为了避免在每个组件中进行硬编码的角色检查,我们将定义一个有状态的WidgetWithRolerole_handlers.dart。该类与核心类交互以获取数据并集中进行角色验证,从而简化我们的代码。

import 'package:flutter/material.dart';
import 'package:userrole/core.dart';

class WidgetWithRole extends StatefulWidget {
  const WidgetWithRole({Key? key, required this.child}) : super(key: key);

  final Widget child;

  @override
  State<WidgetWithRole> createState() => _WidgetWithRoleState();
}

class _WidgetWithRoleState extends State<WidgetWithRole> {
  late Core core;

  @override
  void initState() {
    core = Core();

    super.initState();
  }

  bool get isAdmin => core.user?.role.name == "admin";

  @override
  Widget build(BuildContext context) {
    if (isAdmin) {
      return widget.child;
    }

    return Container();
  }
}
Enter fullscreen mode Exit fullscreen mode

现在,app.dart可以通过将 MessageButton 包装起来来利用此功能WidgetWithRole,从而无缝实现访问控制。

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('User model app'),
    ),
    body: const Center(
      child: WidgetWithRole(
        child: MessageButton(),
      ),
    ),
  );
}
Enter fullscreen mode Exit fullscreen mode

我们有一个非常棒的角色检查封装器,但是硬编码的“admin”角色感觉有些局限。让我们通过添加allowedRole参数来让它变得更好!你可以将任何角色作为字符串传入,但是枚举类型岂不是更简洁?我们会专门创建一个enums.dart文件来存放这些角色,将它们转换UserRole为枚举类型,并WidgetWithRole相应地更新调用。

model.dart

import 'package:flutter/foundation.dart';
import 'package:userrole/enums.dart';

@immutable
class UserData {
  final String username;
  final String email;
  final UserRole role;

  const UserData({
    required this.username,
    required this.email,
    required this.role,
  });

  factory UserData.fromJson(Map<String, dynamic> json) {
    return UserData(
      username: json['username']!,
      email: json['email'],
      role: UserRole.admin,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

enums.dart

enum UserRole {
  admin('admin', 3);

  const UserRole(this.name, this.level);

  final String name;
  final int level;

  @override
  String toString() => name;
}
Enter fullscreen mode Exit fullscreen mode

现在,在MyHomePage小部件中,我们将传递allowedRole参数给WidgetWIthRole

app.dart

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('User model app'),
    ),
    body: const Center(
      child: WidgetWithRole(
        allowedRole: UserRole.admin,
        child: MessageButton(),
      ),
    ),
  );
}
Enter fullscreen mode Exit fullscreen mode

现在我们将修改检入role_handlers.dart文件:

role_handlers.dart

class WidgetWithRole extends StatefulWidget {
  const WidgetWithRole({
    Key? key,
    required this.child,
    required this.allowedRole,
  }) : super(key: key);

  final Widget child;
  final UserRole allowedRole;

  @override
  State<WidgetWithRole> createState() => _WidgetWithRoleState();
}

class _WidgetWithRoleState extends State<WidgetWithRole> {
  late Core core;

  @override
  void initState() {
    core = Core();

    super.initState();
  }

  bool get isAllowed => core.user?.role == widget.allowedRole;

  @override
  Widget build(BuildContext context) {
    if (isAllowed) {
      return widget.child;
    }

    return Container();
  }
}
Enter fullscreen mode Exit fullscreen mode

现在,我们已经重构并完善了角色处理程序的版本,我认为是时候引入多角色及其相关的一切了。

首先,让我们更新model.dartenum.dart支持多个角色及其反序列化,

enum.dart

enum UserRole {
  admin('admin', 3),
  manager('manager', 2),
  user('user', 1);

  const UserRole(this.name, this.level);

  final String name;
  final int level;

  @override
  String toString() => name;

  factory UserRole.fromJson(String? role) {
    switch (role) {
      case "admin":
        return UserRole.admin;
      case "manager":
        return UserRole.manager;
      default:
        return UserRole.user;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

role_handlers.dart

final List<UserRole> allowedRoles;

// ...

bool get isAllowed => widget.allowedRoles.contains(core.user?.role);
Enter fullscreen mode Exit fullscreen mode

现在app.dart,我们将修改代码,使其能够访问多个角色:

child: WidgetWithRole(
  allowedRoles: [
    UserRole.admin,
    UserRole.manager,
    UserRole.user,
  ],
  child: MessageButton(),
),
Enter fullscreen mode Exit fullscreen mode

结论

这样你就可以在 Flutter 应用中引入基于角色的访问控制 (RBAC) 了。还有其他一些方法,例如添加角色层级结构,并仅指定最低级别的角色,这样所有高于该级别的角色都可以访问组件。你可以根据自身需求和应用复杂度选择最合适的方案。

文章来源:https://dev.to/sparshmalhotra/role-based-access-control-in-flutter-4m6c