使用 firebase_admin 或 google.cloud.firestore 在 python 中的 Firestore 客户端(作为用户)

Posted

技术标签:

【中文标题】使用 firebase_admin 或 google.cloud.firestore 在 python 中的 Firestore 客户端(作为用户)【英文标题】:Firestore client in python (as user) using firebase_admin or google.cloud.firestore 【发布时间】:2021-05-15 02:34:04 【问题描述】:

我正在构建一个使用 Firestore 的 python 客户端应用程序。我已成功使用 Google Identity Platform 注册并登录 Firebase 项目,并使用经过身份验证的用户身份验证的 google.cloud.firestore.Client 创建了一个有效的 Firestore 客户端:

import json
import requests
import google.oauth2.credentials
from google.cloud import firestore

request_url = f"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=self.__api_key"
headers = "Content-Type": "application/json; charset=UTF-8"
data = json.dumps("email": self.__email, "password": self.__password, "returnSecureToken": True)
response = requests.post(request_url, headers=headers, data=data)
try:
    response.raise_for_status()
except (HTTPError, Exception):
    content = response.json()
    error = f"error: content['error']['message']"
    raise AuthError(error)

json_response = response.json()
self.__token = json_response["idToken"]
self.__refresh_token = json_response["refreshToken"]

credentials = google.oauth2.credentials.Credentials(self.__token,
                                                    self.__refresh_token,
                                                    client_id="",
                                                    client_secret="",
                                                    token_uri=f"https://securetoken.googleapis.com/v1/token?key=self.__api_key"
                                                    )

self.__db = firestore.Client(self.__project_id, credentials)

但是,我遇到的问题是,当令牌过期时,我收到以下错误:

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/grpc_helpers.py", line 57, in error_remapped_callable
    return callable_(*args, **kwargs)
  File "/usr/local/lib/python3.7/dist-packages/grpc/_channel.py", line 826, in __call__
    return _end_unary_response_blocking(state, call, False, None)
  File "/usr/local/lib/python3.7/dist-packages/grpc/_channel.py", line 729, in _end_unary_response_blocking
    raise _InactiveRpcError(state)
grpc._channel._InactiveRpcError: <_InactiveRpcError of RPC that terminated with:
    status = StatusCode.UNAUTHENTICATED
    details = "Missing or invalid authentication."
    debug_error_string = ""created":"@1613043524.699081937","description":"Error received from peer ipv4:172.217.16.74:443","file":"src/core/lib/surface/call.cc","file_line":1055,"grpc_message":"Missing or invalid authentication.","grpc_status":16"
>

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/lib/python3.7/threading.py", line 917, in _bootstrap_inner
    self.run()
  File "/home/my_app/src/controllers/im_alive.py", line 20, in run
    self.__device_api.set_last_updated(utils.device_id())
  File "/home/my_app/src/api/firestore/firestore_device_api.py", line 21, in set_last_updated
    "lastUpdatedTime": self.__firestore.SERVER_TIMESTAMP
  File "/home/my_app/src/api/firestore/firestore.py", line 100, in update
    ref.update(data)
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/firestore_v1/document.py", line 382, in update
    write_results = batch.commit()
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/firestore_v1/batch.py", line 147, in commit
    metadata=self._client._rpc_metadata,
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/firestore_v1/gapic/firestore_client.py", line 1121, in commit
    request, retry=retry, timeout=timeout, metadata=metadata
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/gapic_v1/method.py", line 145, in __call__
    return wrapped_func(*args, **kwargs)
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/retry.py", line 286, in retry_wrapped_func
    on_error=on_error,
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/retry.py", line 184, in retry_target
    return target()
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/timeout.py", line 214, in func_with_timeout
    return func(*args, **kwargs)
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/grpc_helpers.py", line 59, in error_remapped_callable
    six.raise_from(exceptions.from_grpc_error(exc), exc)
  File "<string>", line 3, in raise_from
google.api_core.exceptions.Unauthenticated: 401 Missing or invalid authentication.

我尝试省略令牌,只指定刷新令牌,然后调用credentials.refresh(),但来自https://securetoken.googleapis.com/v1/token 端点的响应中的expires_in 是字符串而不是数字(docs here),这使得google.oauth2._client.py:257 中的_parse_expiry(response_data) 引发异常。

有什么方法可以使用google.cloudfirebase_admin 中的firestore.Client 并让它自动处理刷新令牌,还是我需要切换到手动调用Firestore RPC API 并在正确的位置刷新令牌时间?

注意:没有用户与 python 应用程序交互,因此解决方案必须不需要用户交互。

【问题讨论】:

我仍在学习 Google Cloud 上的一些东西,但恕我直言,如果您希望在没有用户交互的情况下工作,那么您应该使用服务帐户作为客户端身份验证。我相信这些处理方式不同,应该对 IAM 处理的令牌到期进行一些调整 如果有办法让服务帐户完全像普通 Firebase 用户一样对待,那么这绝对是一种可能。但是,我还没有看到类似的东西 这听起来对我来说也超级有趣!我看到您为此使用了一些课程。你认为你可以把它包装在像 firestore-cloud-client 这样的 pipy 包中吗?并做一些与旧的 firestore python API 类似的事情?真的很有帮助! 该类主要是出于其他原因,但没有太多。上面代码的唯一补充是:from google.oauth2 import _client_client._parse_expiry = parse_expiry其中parse_expiry是一个函数,它在计算日期时间之前将expires_in从响应中转换为int:expires_in = response_data.get("expires_in", None)return datetime.datetime.utcnow() + datetime.timedelta(seconds=int(expires_in)) if expires_in else None 【参考方案1】:

你不能只将字符串转换为整数_parse_expiry(int(float(response_data))) 吗?

如果它不起作用,您可以在获取错误 401 后尝试调用并刷新令牌,请参阅我的 answer 了解有关如何处理令牌的总体思路。

【讨论】:

这实际上是我的临时技巧。因为 _parse_expiry 在 Google Oauth2 库中,所以我必须将函数覆盖如下:from google.oauth2 import _client_client._parse_expiry = my_parse_expiry。您能解释一下为什么要先将值转换为 float 吗? 我不确定 response.data 是什么。如果没有小数,你可以忽略它,只转换为 int。【参考方案2】:

正如@Marco 所说,如果要在没有用户的环境中使用,建议您使用service account。当您使用服务帐户时,您只需将 GOOGLE_APPLICATION_CREDENTIALS 环境变量设置为服务帐户 json 文件的位置,然后在没有任何凭据的情况下实例化 firestore 客户端(凭据将自动获取):

import firestore
client = firestore.Client()

并运行它(假设是 Linux):

$ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json
$ python file.py

不过,如果你真的想为脚本使用用户凭据,你可以安装Google Cloud SDK,然后:

$ gcloud auth application-default login

这将打开浏览器并供您选择帐户和登录。登录后,它会创建一个与您的用户帐户对应的“虚拟”服务帐户文件(客户端也会自动加载该文件)。在这里,您也不需要将任何参数传递给您的客户端。

另见:Difference between “gcloud auth application-default login” and “gcloud auth login”

【讨论】:

是否可以将服务帐户配置为普通 Firebase 用户?我不是在寻找服务器端解决方案,因此必须能够获取 Firebase 用户 ID,并且必须根据安全规则检查对 Firebase 和 Firestore 的读/写操作

以上是关于使用 firebase_admin 或 google.cloud.firestore 在 python 中的 Firestore 客户端(作为用户)的主要内容,如果未能解决你的问题,请参考以下文章

firebase_admin auth.verify_id_token 非常慢

孤荷凌寒自学python第五十二天初次尝试使用python读取Firebase数据库中记录

孤荷凌寒自学python第五十四天使用python来删除Firebase数据库中的文档

孤荷凌寒自学python第五十三天使用python写入和修改Firebase数据库中记录

Firestore - 嵌套查询

使用 Google 脚本创建和/或更新邮件合并 Google 文档