Firebase Data Connect 提供下列功能,確保用戶端安全性:
- 行動和網路用戶端授權
- 個別查詢和變異查詢層級授權控制項
- 使用 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
時,您可以測試傳入的 auth.uid
與儲存的 authorUid
是否相符。
# 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
# 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
}
}
依使用者宣告篩選
在此假設您已設定自訂使用者要求,以便在應用程式的「pro」方案中傳遞驗證權杖,並在驗證權杖中標示 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
指令時應遵循的模式。
您也應瞭解重要的反模式,並避免使用。
避免在查詢和 mutation 引數中傳遞使用者屬性 ID 和驗證權杖參數
Firebase Authentication 是一項強大的工具,可用於呈現驗證流程,並安全擷取驗證資料,例如已註冊的使用者 ID 和儲存在驗證權杖中的多個欄位。
我們不建議在查詢和 mutation 引數中傳遞使用者 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 錯誤要求錯誤。 |
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 可以是布林值、整數、浮點數、數字、字串、清單、地圖、時間戳記或時間長度 |
由左至右 |
a==b a!=b |
比較運算子 | 由左至右 |
a && b |
條件式 AND | 由左至右 |
a || b |
條件式 OR | 由左至右 |
a ? true_value : false_value |
三元運算式 | 由左至右 |
驗證權杖中的資料
auth.token
物件可能包含下列值:
欄位 | 說明 |
---|---|
email |
與帳戶相關聯的電子郵件地址 (如果有)。 |
email_verified |
true :如果使用者已驗證自己有權存取 email 地址。部分供應商會自動驗證他們擁有的電子郵件地址。 |
phone_number |
與帳戶相關聯的電話號碼 (如果有的話)。 |
name |
使用者的顯示名稱 (如有)。 |
sub |
使用者的 Firebase UID。在專案中不得重複。 |
firebase.identities |
與此使用者帳戶相關聯的所有身分的字典。字典的鍵可以是下列任一值:email 、phone 、google.com 、facebook.com 、github.com 、twitter.com 。字典的值是與帳戶相關聯的每個 ID 提供者的專屬 ID 陣列。舉例來說,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 |
與帳戶相關聯的 tenantId (如有)。例如 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 安全性,請參閱服務和資料庫管理指南。