使用授權和認證功能保護 Data Connect

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_ANONUSER_EMAIL_VERIFIED 等級是 USER 情況的變化版本。

您可以使用運算式語法,透過代表透過作業傳遞的驗證資料的 auth 物件來評估資料,包括驗證權杖中的標準資料和權杖中的自訂資料。如需 auth 物件中可用的欄位清單,請參閱參考資料章節

當然,在某些用途中,PUBLIC 是正確的存取層級。再次強調,存取層級始終是起點,您需要額外的篩選器和運算式,才能確保安全性。

本指南現在提供範例,說明如何在 USERPUBLIC 上進行建構。

動力來源示例

以下最佳做法範例是指部落格平台的結構定義,其中特定內容必須透過付費方案才能存取。

這類平台可能會模擬 UsersPosts

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 存取層級

如指南中多次提到的,核心存取層級 (例如 USERUSER_ANONUSER_EMAIL_VERIFIED) 是授權檢查的基準和起點,可搭配篩選器和運算式加以強化。使用這些層級時,如果沒有對應的篩選器或運算式來檢查「哪位」使用者執行要求,基本上就等同於使用 PUBLIC 層級。

# Antipattern!
# This incorrectly allows any user to view all documents
query ListDocuments @auth(level: USER) {
  documents {
    id
    title
    text
  }
}

避免使用 PUBLICUSER 存取層級進行原型設計

為了加快開發速度,您可能會將所有作業設為 PUBLIC 存取層級或 USER 存取層級,而無須進一步強化授權所有作業,讓您快速測試程式碼。

完成初步的原型設計後,請開始從 NO_ACCESS 切換至可正式發布的授權,並使用 PUBLICUSER 層級。不過,如果未按照本指南所示新增額外邏輯,請勿將其部署為 PUBLICUSER

# 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_ANONUSER 的超集。

注意事項:請注意,您必須針對這層級的授權,仔細設計查詢和突變。這個層級可讓使用者透過 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 方法,因此與 USERUSER_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 評估。換句話說,只有在 thisnull 或非 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')")

下節將說明所有可用的運算子。

運算子和運算子優先順序

請參考下表,瞭解運算子及其相應的優先順序。

給定任意運算式 ab、欄位 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 與此使用者帳戶相關聯的所有身分的字典。字典的鍵可以是下列任一值:emailphonegoogle.comfacebook.comgithub.comtwitter.com。字典的值是與帳戶相關聯的每個 ID 提供者的專屬 ID 陣列。舉例來說,auth.token.firebase.identities["google.com"][0] 包含與帳戶相關聯的第一個 Google 使用者 ID。
firebase.sign_in_provider 用來取得此權杖的登入資訊提供者。可以是下列字串之一:custompasswordphoneanonymousgoogle.comfacebook.comgithub.comtwitter.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 存取該宣告。

後續步驟