Firebase Data Connect 提供强大的客户端安全性,具有以下特点:
- 移动和 Web 客户端授权
- 个别查询和更改级授权控制
- 使用 Firebase App Check 进行应用认证。
Data Connect 通过以下方式扩展了此安全功能:
- 服务器端授权
- 使用 IAM 实现 Firebase 项目和 Cloud SQL 用户安全。
授权客户端查询和变更
Data Connect 与 Firebase Authentication 完全集成,因此您可以在设计中使用有关访问您数据的用户的丰富数据(身份验证),以确定这些用户可以访问哪些数据(授权)。
Data Connect 为查询和更改提供了 @auth
指令,可让您设置授权操作所需的身份验证级别。本指南介绍了 @auth
指令并提供了示例。
此外,Data Connect 支持执行嵌入在更改中的查询,因此您可以检索存储在数据库中的其他授权条件,并在 @check
指令中使用这些条件来确定封闭更改是否已获授权。对于这种授权情形,@redact
指令可让您控制是否将查询结果返回给线协议中的客户端,以及是否在生成的 SDK 中省略嵌入式查询。查看这些指令的简介(包含示例)。
了解 @auth
指令
您可以对 @auth
指令进行参数化,以遵循多种预设访问权限级别之一,这些级别涵盖许多常见的访问场景。这些级别从 PUBLIC
(允许所有客户端在不进行任何身份验证的情况下执行查询和更改)到 NO_ACCESS
(禁止在使用 Firebase Admin SDK 的特权服务器环境之外执行查询和更改)不等。这些级别各与 Firebase Authentication 提供的身份验证流程相关联。
级别 | 定义 |
---|---|
PUBLIC |
任何人(无论是否经过身份验证)都可以执行此操作。 |
PUBLIC |
任何人(无论是否经过身份验证)都可以执行此操作。 |
USER_ANON |
任何已识别身份的用户(包括使用 Firebase Authentication 匿名登录的用户)都有权执行查询或更改。 |
USER |
除匿名登录用户外,使用 Firebase Authentication 登录的任何用户都有权执行查询或更改。 |
USER_EMAIL_VERIFIED |
使用已验证电子邮件地址通过 Firebase Authentication 登录的任何用户都有权执行查询或更改。 |
NO_ACCESS |
此操作无法在 Admin SDK 上下文之外执行。 |
以这些预设的访问权限级别为起点,您可以在 @auth
指令中使用 where
过滤器和在服务器上评估的通用表达式语言 (CEL) 表达式定义复杂且稳健的授权检查。
使用 @auth
指令实现常见的授权场景
预设的访问权限级别是授权的起点。
USER
访问权限级别是最常用的起始级别。
完全安全的访问权限将基于 USER
级别,以及用于检查用户属性、资源属性、角色和其他检查的过滤器和表达式。USER_ANON
和 USER_EMAIL_VERIFIED
级别是 USER
情形的变体。
借助表达式语法,您可以使用表示通过操作传递的身份验证数据的 auth
对象来评估数据,包括身份验证令牌中的标准数据和令牌中的自定义数据。如需查看 auth
对象中可用字段的列表,请参阅参考部分。
当然,在某些用例中,PUBLIC
是开始时正确的访问权限级别。再次强调,访问权限级别始终是起点,需要额外的过滤条件和表达式才能实现强大的安全保障。
本指南现在提供了有关如何在 USER
和 PUBLIC
上构建的示例。
一个激励性的示例
以下最佳实践示例参考了博客平台的以下架构,其中某些内容需要付费才能访问。
这样的平台可能会对 Users
和 Posts
进行建模。
type User @table(key: "uid") {
uid: String!
name: String
birthday: Date
createdAt: Timestamp! @default(expr: "request.time")
}
type Post @table {
author: User!
text: String!
# "one of 'draft', 'public', or 'pro'"
visibility: String! @default(value: "draft")
# "the time at which the post should be considered published. defaults to
# immediately"
publishedAt: Timestamp! @default(expr: "request.time")
createdAt: Timestamp! @default(expr: "request.time")
updatedAt: Timestamp! @default(expr: "request.time")
}
归用户所有的资源
Firebase 建议您编写用于测试用户对资源的所有权的过滤条件和表达式,在以下情况下,即测试对 Posts
的所有权。
在以下示例中,系统会使用表达式读取和比较来自身份验证令牌的数据。典型模式是使用 where: {authorUid:
{eq_expr: "auth.uid"}}
等表达式将存储的 authorUid
与身份验证令牌中传递的 auth.uid
(用户 ID)进行比较。
创建
此授权实践首先将授权令牌中的 auth.uid
添加到每个新 Post
中作为 authorUid
字段,以便在后续授权测试中进行比较。
# Create a new post as the current user
mutation CreatePost($text: String!, $visibility: String) @auth(level: USER) {
post_insert(data: {
# set the author's uid to the current user uid
authorUid_expr: "auth.uid"
text: $text
visibility: $visibility
})
}
更新
当客户端尝试更新 Post
时,您可以针对存储的 authorUid
测试传递的 auth.uid
。
# Update one of the current user's posts
mutation UpdatePost($id: UUID!, $text: String, $visibility: String) @auth(level:USER) {
post_update(
# only update posts whose author is the current user
first: { where: {
id: {eq: $id}
authorUid: {eq_expr: "auth.uid"}
}}
data: {
text: $text
visibility: $visibility
# insert the current server time for updatedAt
updatedAt_expr: "request.time"
}
)
}
删除
同样的方法也用于授权删除操作。
# Delete one of the current user's posts
mutation DeletePost($id: UUID!) @auth(level: USER) {
post_delete(
# only delete posts whose author is the current user
first: { where: {
id: {eq: $id}
authorUid: {eq_expr: "auth.uid"}
}}
)
}
# Common display information for a post
fragment DisplayPost on Post {
id, text, createdAt, updatedAt
author { uid, name }
}
列表
# List all posts belonging to the current user
query ListMyPosts @auth(level: USER) {
posts(where: {
userUid: {eq_expr: "auth.uid"}
}) {
# See the fragment above
...DisplayPost
# also show visibility since it is user-controlled
visibility
}
}
获取
# Get a post only if it belongs to the current user
query GetMyPost($id: UUID!) @auth(level: USER) {
post(key: {id: $id},
first: {where: {
id: {eq: $id}
authorUid: {eq_expr: "auth.uid"}}
}}, {
# See the fragment above
...DisplayPost
# also show visibility since it is user-controlled
visibility
}
}
过滤数据
借助 Data Connect 的授权系统,您可以编写复杂的过滤条件,并将其与 PUBLIC
等预设访问权限级别以及身份验证令牌中的数据结合使用。
授权系统还允许您仅使用表达式,而无需使用基本访问权限级别,如以下部分示例所示。
按资源属性过滤
在这里,授权不是基于身份验证令牌,因为基本安全级别设置为 PUBLIC
。不过,我们可以明确将数据库中的记录设置为适合公开访问;假设我们的数据库中有 Post
条记录,并且 visibility
已设置为“public”。
# List all posts marked as 'public' visibility
query ListPublicPosts @auth(level: PUBLIC) {
posts(where: {
# Test that visibility is "public"
visibility: {eq: "public"}
# Only display articles that are already published
publishedAt: {lt_expr: "request.time"}
}) {
# see the fragment above
...DisplayPost
}
}
按用户声明过滤
在此示例中,假设您设置了自定义用户声明,用于传入身份验证令牌,以便识别应用的“专业版”方案中的用户(在身份验证令牌中标记了 auth.token.plan
字段)。您的表达式可以针对此字段进行测试。
# List all public or pro posts, only permitted if user has "pro" plan claim
query ProListPosts @auth(expr: "auth.token.plan == 'pro'") {
posts(where: {
# display both public posts and "pro" posts
visibility: {in: ['public', 'pro']},
# only display articles that are already published
publishedAt: {lt_expr: "request.time"},
}) {
# see the fragment above
...DisplayPost
# show visibility so pro users can see which posts are pro\
visibility
}
}
按订单 + 限制过滤
或者,您也可以在 Post
记录中设置 visibility
,以标识这些记录是面向“专业”用户提供的内容,但对于数据的预览或预告片列表,进一步限制返回的记录数量。
# Show 2 oldest Pro post as a preview
query ProTeaser @auth(level: USER) {
posts(
where: {
# show only pro posts
visibility: {eq: "pro"}
# that have already been published more than 30 days ago
publishedAt: {lt_time: {now: true, sub: {days: 30}}}
},
# order by publish time
orderBy: [{publishedAt: DESC}],
# only return two posts
limit: 2
) {
# See the fragment above
...DisplayPost
}
}
按角色过滤
如果您的自定义声明定义了 admin
角色,您可以相应地测试和授权操作。
# List all posts unconditionally iff the current user has an admin claim
query AdminListPosts @auth(expr: "auth.token.admin == true") {
posts { ...DisplayPost }
}
了解 @check
和 @redact
指令
@check
指令用于验证查询结果中是否存在指定字段。通用表达式语言 (CEL) 表达式用于测试字段值。该指令的默认行为是检查并拒绝 null
值的节点。
@redact
指令会隐去来自客户端的部分响应。系统仍会对隐去的数据进行评估,以确定是否会产生副作用(包括数据更改和 @check
),并且结果仍可供 CEL 表达式中的后续步骤使用。
在 Data Connect 中,@check
和 @redact
指令最常用于授权检查上下文;请参阅授权数据查询的讨论。
添加 @check
和 @redact
指令以查询授权数据
常见的授权用例包括在数据库(例如特殊权限表)中存储自定义授权角色,并使用这些角色授权变更以创建、更新或删除数据。
使用授权数据查询功能,您可以根据用户 ID 查询角色,并使用 CEL 表达式来确定是否有权进行更改。例如,您可能需要编写一个 UpdateMovieTitle
更改,以允许已获授权的客户端更新电影名。
在本讨论的其余部分中,假设电影评论应用数据库在 MoviePermission
表中存储授权角色。
# MoviePermission
# Suppose a user has an authorization role with respect to records in the Movie table
type MoviePermission @table(key: ["doc", "userId"]) {
movie: Movie! # implies another field: movieId: UUID!
userId: String! # Can also be a reference to a User table, doesn't matter
role: String!
}
在以下示例实现中,UpdateMovieTitle
更改包含一个 query
字段,用于从 MoviePermission
检索数据,以及以下指令,以确保操作安全可靠:
- 一个
@transaction
指令,用于确保所有授权查询和检查以原子方式完成或失败。 @redact
指令,用于从响应中省略查询结果,这意味着我们的授权检查是在 Data Connect 服务器上执行的,但敏感数据不会向客户端公开。一对
@check
指令,用于对查询结果评估授权逻辑,例如测试给定用户 ID 是否具有进行修改的适当角色。
mutation UpdateMovieTitle($movieId: UUID!, $newTitle: String!) @auth(level: USER) @transaction {
# Step 1: Query and check
query @redact {
moviePermission( # Look up a join table called MoviePermission with a compound key.
key: {movieId: $movieId, userId_expr: "auth.uid"}
# Step 1a: Use @check to test if the user has any role associated with the movie
# Here the `this` binding refers the lookup result, i.e. a MoviePermission object or null
# The `this != null` expression could be omitted since rejecting on null is default behavior
) @check(expr: "this != null", message: "You do not have access to this movie") {
# Step 1b: Check if the user has the editor role for the movie
# Next we execute another @check; now `this` refers to the contents of the `role` field
role @check(expr: "this == 'editor'", message: "You must be an editor of this movie to update title")
}
}
# Step 2: Act
movie_update(id: $movieId, data: {
title: $newTitle
})
}
授权中应避免的反模式
上一节介绍了使用 @auth
指令时应遵循的模式。
您还应了解一些重要的反模式,以免犯错。
避免在查询和更改参数中传递用户属性 ID 和身份验证令牌参数
Firebase Authentication 是一款强大的工具,可用于呈现身份验证流程并安全地捕获身份验证数据,例如已注册的用户 ID 和存储在身份验证令牌中的众多字段。
建议不要在查询和更改参数中传递用户 ID 和身份验证令牌数据。
# Antipattern!
# This incorrectly allows any user to view any other user's posts
query AllMyPosts($userId: String!) @auth(level: USER) {
posts(where: {authorUid: {eq: $userId}}) {
id, text, createdAt
}
}
避免使用不带任何过滤条件的 USER
访问权限级别
正如本指南中多次提到的那样,USER
、USER_ANON
、USER_EMAIL_VERIFIED
等核心访问权限级别是授权检查的基准和起点,可通过过滤条件和表达式进行增强。如果使用这些级别,但没有相应的过滤条件或表达式来检查执行请求的用户是谁,则本质上等同于使用 PUBLIC
级别。
# Antipattern!
# This incorrectly allows any user to view all documents
query ListDocuments @auth(level: USER) {
documents {
id
title
text
}
}
避免使用 PUBLIC
或 USER
访问权限级别进行原型设计
为了加快开发速度,您可能会很想将所有操作都设置为 PUBLIC
访问权限级别或 USER
访问权限级别,而无需进一步增强功能来授权所有操作,并让您快速测试代码。
通过这种方式完成初始原型设计后,开始从 NO_ACCESS
切换到使用 PUBLIC
和 USER
级别的正式版授权。不过,请勿在未添加额外逻辑的情况下将其部署为 PUBLIC
或 USER
,如本指南所示。
# Antipattern!
# This incorrectly allows anyone to delete any post
mutation DeletePost($id: UUID!) @auth(level: PUBLIC) {
post: post_delete(
id: $id,
)
}
使用 Firebase App Check 进行应用认证
身份验证和授权是 Data Connect 安全性的关键组成部分。身份验证和授权与应用证明相结合,可构成非常强大的安全解决方案。
通过 Firebase App Check 进行证明时,运行您的应用的设备将使用应用或设备证明提供程序,证明 Data Connect 操作来自您的正版应用,以及请求来自真实的、未经篡改的设备。此证明会附加到您的应用向 Data Connect 发出的每个请求。
如需了解如何为 Data Connect 启用 App Check 并在应用中添加其客户端 SDK,请查看 App Check 概览。
@auth(level)
指令的身份验证级别
下表列出了所有标准访问权限级别及其 CEL 等效级别。身份验证级别的列表从广到窄排列,每个级别涵盖与以下级别匹配的所有用户。
级别 | 定义 |
---|---|
PUBLIC |
任何人(无论是否经过身份验证)都可以执行此操作。
注意事项:任何用户都可以读取或修改数据。 Firebase 建议为可公开浏览的数据(例如商品或媒体详情)使用此级别的授权。请参阅最佳实践示例和替代方案。 等同于 @auth(expr: "true")
@auth 过滤条件和表达式不能与此访问权限级别结合使用。任何此类表达式都将失败并显示 400 错误“Bad Request”。
|
USER_ANON |
任何已识别身份的用户(包括使用 Firebase Authentication 匿名登录的用户)都有权执行查询或更改。
注意: USER_ANON 是 USER 的超集。
注意事项:请注意,您必须针对此级别的授权仔细设计查询和更改。此级别允许用户使用 Authentication 匿名登录(自动登录仅与用户设备相关联),但不会自行执行其他检查,例如数据是否属于用户。请参阅最佳实践示例和替代方案。 由于 Authentication 匿名登录流程会发出 uid ,因此 USER_ANON 级别等同于 @auth(expr: "auth.uid != nil")
|
USER |
除匿名登录用户外,使用 Firebase Authentication 登录的任何用户都有权执行查询或更改。
注意事项:请注意,您必须针对此级别的授权仔细设计查询和更改。此级别仅检查用户是否已使用 Authentication 登录,不会自行执行其他检查(例如数据是否属于用户)。请参阅最佳实践示例和替代方案。 等同于 @auth(expr: "auth.uid != nil &&
auth.token.firebase.sign_in_provider != 'anonymous'")"
|
USER_EMAIL_VERIFIED |
使用已验证电子邮件地址通过 Firebase Authentication 登录的任何用户都有权执行查询或更改。 注意事项:由于电子邮件验证是使用 Authentication 执行的,因此它基于更为稳健的 Authentication 方法,因此与 USER 或 USER_ANON 相比,此级别可提供额外的安全保障。此级别仅检查用户是否已使用经过验证的电子邮件地址登录 Authentication,而不会自行执行其他检查(例如数据是否属于用户)。请参阅最佳实践示例和替代方案。
等同于 @auth(expr: "auth.uid != nil &&
auth.token.email_verified")" |
NO_ACCESS |
此操作无法在 Admin SDK 上下文之外执行。
等同于 @auth(expr: "false") |
@auth(expr)
和 @check(expr)
的 CEL 参考文档
如本指南其他部分的示例所示,您可以并应使用通用表达式语言 (CEL) 中定义的表达式,通过 @auth(expr:)
和 @check
指令控制对 Data Connect 的授权。
本部分介绍了与为这些指令创建表达式相关的 CEL 语法。
如需查看有关 CEL 的完整参考信息,请参阅 CEL 规范。
在查询和更改中传入的测试变量
借助 @auth(expr)
语法,您可以访问和测试查询和更改中的变量。
例如,您可以使用 vars.status
添加操作变量(例如 $status
)。
mutation Update($id: UUID!, $status: Any) @auth(expr: "has(vars.status)")
表达式可用的数据
@auth(expr:)
和 @check(expr:)
CEL 表达式都可以对以下内容进行求值:
request.operationName
vars
(request.variables
的别名)auth
(request.auth
的别名)
此外,@check(expr:)
表达式还可以评估:
this
(当前字段的值)
request.operationName 对象
request.operarationName
对象存储操作类型(查询或更改)。
vars
对象
借助 vars
对象,表达式可以访问在查询或更改中传递的所有变量。
您可以在表达式中使用 vars.<variablename>
作为完全限定的 request.variables.<variablename>
的别名:
# The following are equivalent
mutation StringType($v: String!) @auth(expr: "vars.v == 'hello'")
mutation StringType($v: String!) @auth(expr: "request.variables.v == 'hello'")
auth
对象
Authentication 可识别请求访问您数据的用户,并以对象的形式(您可在表达式中构建)提供信息。
在过滤条件和表达式中,您可以将 auth
用作 request.auth
的别名。
auth 对象包含以下信息:
uid
:分配给请求用户的唯一身份用户 ID。token
:Authentication 收集的值映射。
如需详细了解 auth.token
的内容,请参阅身份验证令牌中的数据
this
绑定
绑定 this
的计算结果为 @check
指令附加到的字段。在基本情况下,您可以评估单值查询结果。
mutation UpdateMovieTitle($movieId: UUID!, $newTitle: String!) @auth(level: USER) @transaction {
# Step 1: Query and check
query @redact {
moviePermission( # Look up a join table called MoviePermission with a compound key.
key: {movieId: $movieId, userId_expr: "auth.uid"}
) {
# Check if the user has the editor role for the movie. `this` is the string value of `role`.
# If the parent moviePermission is null, the @check will also fail automatically.
role @check(expr: "this == 'editor'", message: "You must be an editor of this movie to update title")
}
}
# Step 2: Act
movie_update(id: $movieId, data: {
title: $newTitle
})
}
如果返回的字段因任何祖先都是列表而出现多次,则系统会使用绑定到每个值的 this
对每个出现情况进行测试。
对于任何给定路径,如果祖先为 null
或 []
,则不会到达该字段,并且系统会跳过对该路径的 CEL 评估。换句话说,只有当 this
为 null
或非 null
时才会进行评估,而不会是 undefined
。
如果字段本身是列表或对象,this
会遵循相同的结构(包括在对象的情况下选择的所有后代),如以下示例所示。
mutation UpdateMovieTitle2($movieId: UUID!, $newTitle: String!) @auth(level: USER) @transaction {
# Step 1: Query and check
query {
moviePermissions( # Now we query for a list of all matching MoviePermissions.
where: {movieId: {eq: $movieId}, userId: {eq_expr: "auth.uid"}}
# This time we execute the @check on the list, so `this` is the list of objects.
# We can use the `.exists` macro to check if there is at least one matching entry.
) @check(expr: "this.exists(p, p.role == 'editor')", message: "You must be an editor of this movie to update title") {
role
}
}
# Step 2: Act
movie_update(id: $movieId, data: {
title: $newTitle
})
}
复杂表达式语法
您可以通过与 &&
和 ||
运算符组合来编写更复杂的表达式。
mutation UpsertUser($username: String!) @auth(expr: "(auth != null) && (vars.username == 'joe')")
以下部分介绍了所有可用的运算符。
运算符和运算符优先级
请使用下表作为运算符及其相应优先级的参考。
给定任意表达式 a
和 b
、字段 f
和索引 i
。
运算符 | 说明 | 关联度 |
---|---|---|
a[i] a() a.f |
索引、调用、字段访问 | 从左到右 |
!a -a |
一元否定 | 从右到左 |
a/b a%b a*b |
乘法运算符 | 从左到右 |
a+b a-b |
加法运算符 | 从左到右 |
a>b a>=b a<b a<=b |
关系运算符 | 从左到右 |
a in b |
存在于列表或映射中 | 从左到右 |
type(a) == t |
类型比较,其中 t 可以是 bool、int、float、number、string、list、map、timestamp 或 duration |
从左到右 |
a==b a!=b |
比较运算符 | 从左到右 |
a && b |
条件“与” | 从左到右 |
a || b |
条件“或” | 从左到右 |
a ? true_value : false_value |
三元表达式 | 从左到右 |
身份验证令牌中的数据
auth.token
对象可能包含以下值:
字段 | 说明 |
---|---|
email |
与账号关联的电子邮件地址(如果存在)。 |
email_verified |
如果用户已验证他们可以访问 email 地址,则为 true 。某些提供方会自动验证他们拥有的电子邮件地址。 |
phone_number |
与账号关联的电话号码(如果有)。 |
name |
用户的显示名(如果已设置)。 |
sub |
用户的 Firebase UID。此 UID 在项目中是唯一的。 |
firebase.identities |
与此用户账号关联的所有身份的字典。字典的键可以是以下任一值:email 、phone 、google.com 、facebook.com 、github.com 、twitter.com 。字典的值是与账号关联的每个身份提供方的唯一标识符的数组。例如,auth.token.firebase.identities["google.com"][0] 包含与该账号关联的第一个 Google 用户 ID。 |
firebase.sign_in_provider |
用于获取此令牌的登录服务提供方。可以是以下任一字符串:custom 、password 、phone 、anonymous 、google.com 、facebook.com 、github.com 、twitter.com 。 |
firebase.tenant |
与账号关联的租户 ID(如果有)。例如 tenant2-m6tyz 。 |
JWT ID 令牌中的其他字段
您还可以访问以下 auth.token
字段:
自定义令牌声明 | ||
---|---|---|
alg |
算法 | "RS256" |
iss |
颁发者 | 您项目的服务账号电子邮件地址 |
sub |
主题 | 您项目的服务账号电子邮件地址 |
aud |
受众 | "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit" |
iat |
颁发时间 | 当前时间(与 UNIX 计时原点之间相隔的秒数) |
exp |
到期时间 |
令牌到期的时间(与 UNIX 计时原点之间相隔的秒数),该时间可能比 iat 晚最多 3600 秒。
注意:这仅会控制自定义令牌本身的过期时间。但是,一旦您使用 signInWithCustomToken() 让用户登录,他们将一直在设备上保持登录状态,直到其会话失效或用户退出账号为止。 |
<claims> (可选) |
要包含在令牌中的可选自定义声明,可通过表达式中的 auth.token (或 request.auth.token )访问。例如,如果您创建自定义声明 adminClaim ,则可以使用 auth.token.adminClaim 访问该声明。
|
后续步骤
- Firebase Data Connect 提供了 Admin SDK,可让您从特权环境执行查询和更改。
- 如需了解 IAM 安全,请参阅管理服务和数据库的指南。