在本教程的前几节中,你为MJ Coffee这个Flutter应用添加了认证,然后通过添加刷新令牌轮换和社交登录来增强认证。你还研究了用你自己的品牌定制登录页面,以及Auth0中的用户管理。
在本节中,我们将重点讨论授权请记住:
- 认证关注的是回答 "你是谁 "的问题。
- 授权--本节的主题--是关于回答 "你被允许做什么?"的问题。
我们将通过向MJ Coffee添加实时聊天来涵盖授权,这样我们以后就可以看到聊天如何与Auth0整合,根据角色和权限限制其功能和UI。
如果你想在关注构建和执行步骤的同时略过这些内容,请寻找🛠表情符号。
在Flutter应用程序中添加实时聊天功能
在MJ Coffee应用程序中加入实时聊天服务,可以显著提高用户与我们的客户服务或员工沟通的速度。在四处考察后,我发现了Stream,这是一个令人兴奋的服务,有一个坚实的Flutter SDK,你可以快速而轻松地集成和定制。
安装依赖性
Stream带有几个SDK,可用于Dart或Flutter应用程序中。然而,其中一个附带了所有的功能,一个漂亮的主题,以及足够的API,这样你就可以随心所欲地定制它。我们将使用那一个。
🛠 打开 /pubspec.yaml文件,将stream_chat_flutter 添加到你的依赖项中。文件中的dependencies 部分最后应该是这样的。
// /pubspec.yaml
dependencies:
flutter:
sdk: flutter
font_awesome_flutter: ^9.1.0
flutter_svg: ^0.22.0
google_fonts: ^2.1.0
json_annotation: ^4.0.1
http: ^0.13.3
flutter_appauth: ^1.1.0
flutter_secure_storage: ^4.2.0
stream_chat_flutter: ^2.0.0
我建议使用晚于2.0的版本,它与Flutter 2及其对空安全的支持完全兼容。
如果你只针对Android,你可以忽略下一节。
如果你的目标是iOS
🛠如果你的目标是iOS,你需要采取一些额外的步骤:
- Stream库使用flutter文件选取器插件来为用户提供一个选择文件的界面。你需要按照本页面iOS部分的步骤来使用它。
- 该库还使用video_player插件来显示内联视频。按照这个指南来安装和启用它。
- 最后,该库使用image_picker插件来提供一个选择图片的用户界面。按照[这些说明]来使用这个插件。
🛠 不要忘记实际安装这些依赖项在终端或PowerShell中输入flutter pub get ,或在你的IDE中输入Pub get 。
创建一个Stream账户和应用程序
在你将Stream与你的应用程序集成之前,你需要一个Stream账户,你将用它来注册应用程序。
🛠如果你还没有账户,请创建你的账户,然后登录并转到Stream仪表板。

🛠 点击创建应用程序按钮,然后按照以下步骤操作:
- 在 "应用程序名称"栏中为你的应用程序输入一个名称。在这个例子中,我们将使用
MJCoffee。 - 在Feeds服务器位置菜单中,选择离你最近的位置。
- 不要从克隆现有应用程序菜单中选择任何东西--只需将其设置为
---. - 选择你的环境。通常,为生产和开发创建单独的应用程序是个好主意。在本教程中,我们将专注于生产,所以选择生产。
- 最后,单击 "创建应用程序"按钮。
你应该看到这个。

🛠 一旦你的应用程序被创建,记下你的Stream密钥。
注意,你将需要一个秘钥来签署用户ID,并获得一个生产用户令牌来连接用户。你将学习如何用Auth0 Actions做到这一点。
我建议通过命令行传递敏感数据和密钥 --dart-define通过命令行或添加到编辑器或IDE的运行命令中,而不是存储在应用程序的代码中。
你在为应用程序实现基本认证时采取了这种方法。你在启动应用程序时,将两个敏感值--你的Auth0域和Auth0客户端ID--作为命令行参数传递。你只需将Stream密钥作为第三个参数加入,像这样。
flutter run -d all --dart-define=AUTH0_DOMAIN=[YOUR DOMAIN] --dart-define=AUTH0_CLIENT_ID=[YOUR CLIENT ID] --dart-define=STREAM_API_KEY=[YOUR STREAM KEY]
为了让你的应用程序使用Stream key参数,你需要定义一个新的常量。
🛠 添加以下内容到你的 constants.dart文件中,该文件位于 /lib/helpers/目录中。
// /lib/helpers/constants.dart
const STREAM_API_KEY = String.fromEnvironment('STREAM_API_KEY');
将聊天整合到Flutter应用程序中
让我们首先在ChatService 单元类中创建一个新的StreamChatClient 。在这样做的过程中,有三件基本的事情需要考虑到。
- 用你的API密钥初始化Dart API客户端
- 设置当前用户
- 将客户端传递给顶层的StreamChat小组件
🛠 打开 chat_service.dart(位于目录中的 /lib/services/目录),并通过更新ChatService 类来初始化StreamChatClient ,使其显示如下。
// /lib/services/chat_service.dart
class ChatService {
static final ChatService instance = ChatService._internal();
factory ChatService() => instance;
ChatService._internal();
final StreamChatClient client = StreamChatClient(
STREAM_API_KEY,
logLevel: isInDebugMode ? Level.INFO : Level.OFF,
);
}
的唯一需要的位置参数是 StreamChatClient()唯一需要的位置参数是 STREAM_API_KEY,但你有更多的选项来配置你的客户端。例如,在调试期间看到所有的日志可能是相当有帮助的。这就是为什么我们用可选的logLevel 参数来设置将发生的日志量,基于应用程序是否处于调试模式。
现在你已经创建了客户端,你需要确保你的当前用户被适当地连接。
🛠 将 connectUser()到ChatService 类中的 chat_service.dart:
// /lib/services/chat_service.dart
class ChatService {
...
Future<Auth0User> connectUser(Auth0User? user) async {
if (user == null) {
throw Exception('User was not received');
}
await client.connectUser(
User(
id: user.id,
extraData: {
'image': user.picture,
'name': user.name,
},
),
// To be replaced with PRODUCTION TOKEN for user
client.devToken(user.id).rawValue,
);
return user;
}
...
}
ChatService 类的这个新方法。 connectUser(),将处理连接当前用户的逻辑。它接受一个Auth0User 对象。如果它没有收到,就意味着认证可能已经失败,用户不应该被连接到聊天中。
🛠打开 /lib/screens/home.dart,寻找_HomeScreenState 类,并在该类中寻找 setSuccessAuthState()方法。更新 setSuccessAuthState()到以下内容。
// /lib/screens/home.dart
setSuccessAuthState() {
setState(() {
isProgressing = false;
isLoggedIn = true;
name = AuthService.instance.idToken?.name;
});
ChatService.instance.connectUser(AuthService.instance.profile);
CoffeeRouter.instance.push(MenuScreen.route());
}
你需要传递两个必要的位置参数。
- 带有定义的用户ID的
User对象,以及 - 用户令牌,一个经过签名和加密的哈希字符串。
通常情况下,用户令牌应该在后端服务器上生成,以存储上述的秘钥,并签署和获取令牌。你很快就会知道Auth0如何作为你的后端来处理这个问题。
为了继续,你需要
- 获得一个开发令牌。
- 通过
user.id到devToken()的方法,以及 - 获取
rawValue,这是令牌的字符串。
🛠 为了确保devToken 工作,你需要禁用 auth 检查。在GetStream 应用程序的聊天仪表板上,打开禁用Auth0检查选项。

当你把应用移到生产中,并且你有适当的方法来接收生产令牌时,确保你关闭禁用Auth0检查选项。
流媒体聊天用户界面组件将接受您在连接用户时提供的额外数据。例如,您可以创建一个包含用户图像和姓名的Map ,这样它们就会自动、漂亮地出现在整个聊天小部件中。
在写作时,Stream不接受Auth0的用户ID格式,其中包括 |在Auth源和用户哈希ID之间包含了 。你需要对Auth0User 模型中的id 箭头函数做一个修改,以考虑到这一点。
🛠 打开 /lib/models/auth0_user.dart并对id 箭头函数做此修改。
// /lib/models/auth0_user.dart
String get id => sub.split('|').join('');
下一步也是最后一步是创建StreamChat ,即应用程序的根部件。
🛠 打开文件。 /lib/main.dart文件,找到对MaterialApp 的调用,并添加builder 参数,使其返回一个StreamChat 对象。
// /lib/main.dart
...
MaterialApp(
debugShowCheckedModeBanner: false,
themeMode: ThemeMode.system,
home: HomeScreen(),
navigatorKey: CoffeeRouter.instance.navigatorKey,
theme: getTheme(),
builder: (context, child) {
return StreamChat(
child: child,
client: ChatService.instance.client,
);
},
),
...
由builder 返回的StreamChat 对象是一个继承的部件,目的是为高级定制提供API。它需要一个子程序和一个客户端,你已经在ChatService 类中初始化了。
就这样了--你的聊天准备好了重新启动应用程序,让我们继续使用预先构建的StreamChat UI部件添加支持屏幕和社区屏幕,以利用您添加的聊天服务。
实现支持聊天屏幕
通常情况下,支持聊天包括用户和代理在为两者创建的渠道中进行互动。因此,支持聊天屏幕将做以下工作。
- 为当前用户和一个可用的代理创建一个私人频道
- 听取该通道的更新
- 载入现有的聊天内容(如果有
- 聊天结束后,存档聊天历史
让我们创建一个 createSupportChat()让我们在ChatService 类中创建一个方法。
🛠 首先,在ChatService 类的开头添加以下实例变量。
// /lib/services/chat_service.dart
String? _currentChannelId;
🛠 然后将该方法添加到 createSupportChat()方法添加到ChatService 类中。
// /lib/services/chat_service.dart
Future<Channel> createSupportChat() async {
// To be replaced with EmployeeRole via Auth0
final String employeeId = 'rootEmployeeId';
final channel = client.channel(
'support',
id: _currentChannelId,
extraData: {
'name': 'MJCoffee Support',
'members': [
employeeId,
client.state.user!.id,
]
},
);
await channel.watch();
_currentChannelId = channel.id;
return channel;
}
这里有很多事情要做。让我们一步步来回顾一下。
首先,要为当前用户创建一个支持聊天频道,您需要知道一个可用代理的ID。在本教程中,您最终将学习如何创建一个API,根据角色通过Auth0获得一个可用的代理。不过,我们现在先跳过这一部分。
第二,你需要创建一个具有特定类型的通道。在这种情况下,该类型将是support 。Stream为频道提供了默认的类型;但是,你可以根据需要定义你的类型。要创建一个频道类型,请导航到Stream仪表板,在聊天概览标签中进入你的应用程序,并将你的频道类型添加到列表中。


接下来,你可以通过一个现有的频道ID来重新连接到一个频道,或者离开它 null来创建一个新的频道。Stream会自动给新频道分配一个ID。由于这个支持聊天是私人的,只对一个代理和一个当前用户开放,您可以在extraData 地图中添加ID到members 。
然后,调用 watch()方法来创建和监听该频道的事件。该 watch()方法是一个Future ,并将异步地执行其任务。你可以把通道的ID分配给_currentChannelId 私有的实例变量,这样你就可以在需要时重新连接到该通道。
以后,把频道ID存储在外部数据库或应用程序中的本地安全存储中可能是一个好主意,这样它就能在用户会话之间持续存在。
最后,该方法返回新创建的频道。
有了这个 createSupportChat()方法的实现,你现在可以实现聊天用户界面。
🛠 打开 /lib/screens/support.dart文件,在那里你可以找到SupportChatScreen 类。将该文件的内容更新为以下内容。
// /lib/screens/support.dart
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class SupportChatScreen extends StatefulWidget {
@override
_SupportChatScreenState createState() => _SupportChatScreenState();
}
class _SupportChatScreenState extends State<SupportChatScreen> {
Auth0User? profile = AuthService.instance.profile;
Channel? channel;
@override
void initState() {
super.initState();
createChannel();
}
createChannel() async {
final _channel = await ChatService.instance.createSupportChat();
setState(() {
channel = _channel;
});
}
@override
Widget build(BuildContext context) {
return channel == null
? Center(
child: Text('You are in the queue!, please wait...'),
)
: Scaffold(
body: SafeArea(
child: StreamChannel(
channel: channel!,
child: Column(
children: <Widget>[
Expanded(
child: MessageListView(),
),
MessageInput(
disableAttachments: true,
sendButtonLocation: SendButtonLocation.inside,
actionsLocation: ActionsLocation.leftInside,
showCommandsButton: true,
),
],
),
),
),
);
}
}
下面是这个UI实现中发生的事情。
- 这个支持聊天屏幕,
SupportChatScreen,是一个StatefulWidget。 profile变量是你在上一节中创建的AuthService的用户配置文件。channel变量用于检测是否已经创建了一个支持频道。- 该
createChannel()方法调用ChatService'screateSupportChat()方法,这是你最近定义的。 - 一旦频道准备好了。
setState()被调用,从而呈现出聊天用户界面。 - 在该
build()方法中,你可以在创建频道时显示一条信息,或者返回一个StreamChannel对象,该对象向widget树提供关于频道的信息,并传递给channel的引用。 - 通常情况下,
SupportChatScreen的一个子节点应该是一个Column,包括MessageListView()并被Expanded包裹,以确保它占用所有的可用空间。 - 你可以通过多种方式高度定制
MessageInput()的许多方式,例如禁用文件附件或命令按钮。稍后你将通过使用Auth0的权限和角色来确定哪些按钮应该被启用或禁用,所以请继续关注。
恭喜你--你已经实现了支持聊天屏幕让我们继续讨论社区视图。
现在是时候制作一个屏幕,让代理看到他们通过支持收到的所有消息。让我们把这个屏幕称为CommunityScreen 。
🛠 打开文件,并更新 类。 /lib/screens/community.dart文件,并将其中的CommunityScreen 类更新为以下内容。
// /lib/screens/community.dart
class CommunityScreen extends StatelessWidget {
final userId = ChatService.instance.client.state.user?.id as Object;
@override
Widget build(BuildContext context) {
return Scaffold(
body: ChannelsBloc(
child: ChannelListView(
filter: Filter.in_(
'members',
[userId],// current user is employee (role/permission)
),
sort: [SortOption('last_message_at')],
pagination: PaginationParams(
limit: 30,
),
channelWidget: ChannelPage(),
),
),
);
}
}
class ChannelPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: ChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: MessageListView(),
),
MessageInput(),
],
),
);
}
}
这个页面背后的逻辑相对简单。你想加载当前用户看到的所有渠道,在这种情况下是一个代理,然后打开每个渠道来回答各自的客户。
ChannelsBloc 管理一个带有分页、重新排序、查询和其他与频道相关的操作的频道列表,并加上 ChannelListView().Bloc 与Bloc 包或模式没有关系,这只是一个巧合。
你需要根据当前用户的成员资格来过滤所有的频道。如果有必要,也可以做自定义排序和分页。
最后,你需要确保 ChannelPage()也被正确地传递给,因为它使通道可路由。该 ChannelPage()没有什么花哨的东西--只是对你在SupportScreen 中已经做的事情的直接实现。
很完美你已经创建了应用程序的所有屏幕。然而,我们仍然需要实现权限和角色,以便根据我们可以定义并通过Auth0令牌接收的内容加载每个屏幕和功能。但如果你好奇,你可以简单地在MenuScreen ,并加载这些页面(尽管你可能还看不到任何通道)。
🛠 在文件中的 /lib/screens/menu.dart中,找到 final List<Widget> tabs在MenuScreenState ,并将其更新为以下内容。
// /lib/screens/menu.dart
...
final List<Widget> tabs = [
MenuList(coffees: coffees),
SupportChatScreen(),
ProfileScreen(),
];
...
🛠 在同一文件中,通过将items 的参数替换为以下内容,为bottomNavigationBar 添加一个新的图标。
// /lib/screens/menu.dart
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.list_alt),
label: "Menu",
),
BottomNavigationBarItem(
icon: Icon(Icons.support),
label: "Support",
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: "Profile",
),
],
🛠 在测试聊天之前,您应该在Stream仪表盘中临时添加您在createSupportChat 中定义的rootEmployeeId 用户ID。

您可以跳过这一步,但有一个根支持用户是个好主意,以防特定代理无法使用。这样的话,没有代理的用户仍然可以得到支持。

🛠 在模拟器中运行你的应用程序,进入支持聊天屏幕,并尝试一下吧!
如果你在安卓模拟器上遇到一个关于过时的Kotlin版本的错误,打开
android/app/build.gradle,并搜索ext.kotlin_version.将该行改为ext.kotlin_version = '1.5.10'.
Auth0行动和生成流用户生产令牌
到目前为止,对于每个用户,你一直在使用为开发环境生成的令牌。为了发布你的应用,该应用将不得不请求生产令牌。
正如我前面提到的,检索生产令牌通常由你的后端服务器完成。Auth0可以充当这一服务,并为你检索这些令牌,这要感谢一个名为Auth0 Actions的强大工具。
你可以使用Auth0 Actions来处理你的应用逻辑,并将其纳入一个特定的流程。它们是用Node.js编写的安全的、针对租户的、有版本的函数,在Auth0工作时的特定点上执行。这使得你可以通过自定义逻辑来定制和扩展Auth0的功能。把Actions想象成无服务器函数,如AWS Lambda或谷歌云函数。
你可以决定何时何地在Auth0运行环境中执行一个Action。你的流程可能是。
- **登录。**在用户登录后和刷新令牌发出时执行。
- **机器到机器。**在用户登录后,在客户端凭证钩中执行。
- **用户注册前。**在用户被添加到数据库或无密码连接之前执行。
- **用户注册后。**在一个用户被添加到数据库或无密码连接后执行。该执行是异步的,不会影响交易。
- **修改密码后:**在数据库连接用户的密码被修改后执行。
- **发送电话信息。**在使用自定义MFA提供者时执行。

你可能已经想到了这些场景中的几个用例。
让我们创建一个自定义动作,在用户登录后为他们交换Stream生产用户令牌,并将其与Flutter应用程序收到的ID令牌和用户元数据挂钩。
首先,你需要创建一个新的动作。进入Auth0 Dashboard中Actions下的Custom Actions菜单,点击 "Create "按钮。

🛠您应该为该动作提供一个名称,从列表中选择一个触发器,然后创建它。为了生成一个聊天令牌并将其附加到idToken ,选择登录/发布登录触发器,这允许您在登录流程中使用该动作。

你会看到一个编辑器,在那里你可以写你的逻辑。你可以运行代码,在部署之前进行尝试,将你的秘密添加到环境中,而不是放在应用程序代码中,甚至可以添加几乎所有的公共NPM包!

🛠 开始添加getstream npm 包。在撰写本文时,该包的当前版本是 7.2.10.
🛠 回到Stream仪表板,复制你的秘密和客户端密钥...

🛠 ......然后添加它们,名称为 GET_STREAM_CHAT_SECRET_KEY和 GET_STREAM_CHAT_CLIENT_KEY,分别添加到Auth0的这个函数中。

🛠 最后,你可以导入getStream ,并绕过user_id ,创建一个用户令牌。
// Javascript
const stream = require('getstream');
exports.onExecutePostLogin = async (event, api) => {
const getStreamClient = stream.connect(
event.secrets.GET_STREAM_CHAT_CLIENT_KEY,
event.secrets.GET_STREAM_CHAT_SECRET_KEY
);
const getStreamToken = getStreamClient.createUserToken(
`${event.user.user_id.split('|').join('')}` // getstream does not support `|` in the ID yet, so we have to omit it.
);
// api.user.setAppMetadata("stream_chat_token", getStreamToken);
// api.user.setUserMetadata("stream_chat_token", getStreamToken);
const namespace = 'https://getstream.mjcoffee.app';
if (event.authorization) {
api.idToken.setCustomClaim(`${namespace}/user_token`, getStreamToken);
api.accessToken.setCustomClaim(`${namespace}/user_token`, getStreamToken);
}
};
让我们探讨一下上面的实现。
一旦你得到令牌,你可以用令牌的值设置一个名为stream_chat_token 的用户或应用程序元数据。
另外,你可以在idToken 和accessToken 上设置一个自定义的索赔。自定义声明必须采取URI的形式,这意味着 https://getstream.mjcoffee.app/user_token是一个可接受的索赔名称,而user_token 则不是。
**重要提示:**在Stream中,只有字符
a到z。0到9,@,_, 和-在用户ID中是允许的。因此,我们不应该使用|为Auth0中的user_id。
让我们在部署前运行并测试这个功能。点击播放图标,用自动为你生成的示例事件运行。

如果一切顺利,你会得到一个生成的JWT格式的令牌。它的有效载荷将包含user_id ,并且它将被Stream服务器用你的秘钥签名。
下面是你将得到的一个例子。
// response in Action run
[
{
name: 'https://getstream.mjcoffee.app/user_token',
target: 'idToken',
type: 'SetCustomClaim',
value:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYXV0aDA1ZjdjOGVjN2MzM2M2YzAwNGJiYWZlODIifQ.7ZIyr27skgrGm6REEz5o-WvoCArNblDnwiOdxXW4dp8',
},
{
name: 'https://getstream.mjcoffee.app/user_token',
target: 'accessToken',
type: 'SetCustomClaim',
value:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYXV0aDA1ZjdjOGVjN2MzM2M2YzAwNGJiYWZlODIifQ.7ZIyr27skgrGm6REEz5o-WvoCArNblDnwiOdxXW4dp8',
},
];

🛠 接下来,回到Flow,选择Login,并将你新创建的自定义动作拖到流程中,然后应用这些变化。

最后一步,Flutter应用程序需要读取这个令牌并将其添加到用户模型中。这样,当你把用户连接到Stream的聊天时,你就可以用Auth0收到的令牌代替你一直使用的开发令牌。
🛠 由于这是一个自定义的索赔,它将显示在idToken ;因此你需要修改你的Auth0IdToken 模型类(位于 /lib/models/auth0_id_token.dart)...
// /lib/models/auth0_id_token.dart
@JsonSerializable()
class Auth0IdToken {
Auth0IdToken({
....
required this.streamChatUserToken,
....
})
....
@JsonKey(name: 'https://getstream.mjcoffee.app/user_token')
final String streamChatUserToken;
....
}
🛠 然后对你的Auth0User 类做同样的修改(位于 /lib/models/auth0_user.dart),因为它将是用户详细信息的一部分。
// m/lib/models/auth0_user.dart
@JsonSerializable()
class Auth0User {
Auth0User({
....
required this.streamChatUserToken,
....
})
....
@JsonKey(name: 'https://getstream.mjcoffee.app/user_token')
final String streamChatUserToken;
....
}
🛠 一旦你完成了,运行build_runner 命令,再次生成模型。
flutter pub run build_runner build --delete-conflicting-outputs
🛠 找到ChatService 类中的connectUser (位于 /lib/services/chat_service.dart),并将 devToken()替换为用户对象上新收到的令牌。
// /lib/services/chat_service.dart
await client.connectUser(
User(
id: user.id,
extraData: {
'image': user.picture,
'name': user.name,
},
),
// client.devToken(user.id).rawValue,
user.streamChatUserToken,
);
做得很好!退出应用程序,重新启动它。然后再次登录。这一次,你会收到一个生产就绪的用户令牌。当你进入支持聊天屏幕时,一切都应该按预期工作。
展望未来
现在应用程序的实时聊天已经准备好了正确的认证流程,您可以进入下一节,关注授权、角色和权限。