您可以使用 App Check 来保护应用的非 Firebase 资源,例如自托管的后端。为此,您需要执行以下两项操作:
- 修改您的应用客户端,以将 App Check 令牌随每个请求一起发送到后端,如 iOS+、Android 和 Web 对应的页面所述。
- 按照本页所述,修改后端以要求将有效的 App Check 令牌随每个请求一起发送。
令牌验证
如需在后端验证 App Check 令牌,请向 API 端点的处理程序添加以下逻辑:
检查确认每个请求都包含 App Check 令牌。
使用 Admin SDK 验证 App Check 令牌。
如果验证成功,Admin SDK 会返回已解码的 App Check 令牌。成功的验证表示令牌来自属于您的 Firebase 项目的应用。
拒绝未通过任一检查的任何请求。例如:
Node.js
如果您尚未安装 Node.js Admin SDK,请先安装。
然后,以 Express.js 中间件为例:
import express from "express";
import { initializeApp } from "firebase-admin/app";
import { getAppCheck } from "firebase-admin/app-check";
const expressApp = express();
const firebaseApp = initializeApp();
const appCheckVerification = async (req, res, next) => {
const appCheckToken = req.header("X-Firebase-AppCheck");
if (!appCheckToken) {
res.status(401);
return next("Unauthorized");
}
try {
const appCheckClaims = await getAppCheck().verifyToken(appCheckToken);
// If verifyToken() succeeds, continue with the next middleware
// function in the stack.
return next();
} catch (err) {
res.status(401);
return next("Unauthorized");
}
}
expressApp.get("/yourApiEndpoint", [appCheckVerification], (req, res) => {
// Handle request.
});
Python
如果您尚未安装 Python Admin SDK,请先安装。
然后,在 API 端点处理程序中,调用 app_check.verify_token()
并在其失败时拒绝请求。在以下示例中,使用 @before_request
修饰的函数将针对所有请求执行此任务:
import firebase_admin
from firebase_admin import app_check
import flask
import jwt
firebase_app = firebase_admin.initialize_app()
flask_app = flask.Flask(__name__)
@flask_app.before_request
def verify_app_check() -> None:
app_check_token = flask.request.headers.get("X-Firebase-AppCheck", default="")
try:
app_check_claims = app_check.verify_token(app_check_token)
# If verify_token() succeeds, okay to continue to route handler.
except (ValueError, jwt.exceptions.DecodeError):
flask.abort(401)
@flask_app.route("/yourApiEndpoint")
def your_api_endpoint(request: flask.Request):
# Handle request.
...
Go
如果您尚未安装 Admin SDK for Go,请先安装。
然后,在 API 端点处理程序中,调用 appcheck.Client.VerifyToken()
并在其失败时拒绝请求。在以下示例中,封装容器函数会将此逻辑添加到端点处理程序:
package main
import (
"context"
"log"
"net/http"
firebaseAdmin "firebase.google.com/go/v4"
"firebase.google.com/go/v4/appcheck"
)
var (
appCheck *appcheck.Client
)
func main() {
app, err := firebaseAdmin.NewApp(context.Background(), nil)
if err != nil {
log.Fatalf("error initializing app: %v\n", err)
}
appCheck, err = app.AppCheck(context.Background())
if err != nil {
log.Fatalf("error initializing app: %v\n", err)
}
http.HandleFunc("/yourApiEndpoint", requireAppCheck(yourApiEndpointHandler))
log.Fatal(http.ListenAndServe(":8080", nil))
}
func requireAppCheck(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
wrappedHandler := func(w http.ResponseWriter, r *http.Request) {
appCheckToken, ok := r.Header[http.CanonicalHeaderKey("X-Firebase-AppCheck")]
if !ok {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized."))
return
}
_, err := appCheck.VerifyToken(appCheckToken[0])
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized."))
return
}
// If VerifyToken() succeeds, continue with the provided handler.
handler(w, r)
}
return wrappedHandler
}
func yourApiEndpointHandler(w http.ResponseWriter, r *http.Request) {
// Handle request.
}
其他
如果您的后端是用其他语言编写的,您可以使用通用 JWT 库(如 jwt.io 中的库)来验证 App Check 令牌。
您的令牌验证逻辑必须完成以下步骤:
- 从 App Check JWKS 端点 (
https://firebaseappcheck.googleapis.com/v1/jwks
) 获取 Firebase App Check 公共 JSON Web 密钥 (JWK) 集 - 验证 App Check 令牌的签名以确保其合法性。
- 确保令牌的标头使用 RS256 算法。
- 确保令牌的标头类型为 JWT。
- 确保令牌由 Firebase App Check 在您的项目下颁发。
- 确保令牌未过期。
- 确保令牌的目标设备与您的项目匹配。
- 可选:检查令牌的主题是否与应用的 ID 匹配。
JWT 库的功能可能有所不同;请务必手动完成所选库未处理的所有步骤。
以下示例使用 jwt
gem 作为 Rack 中间件层在 Ruby 中执行必要的步骤。
require 'json'
require 'jwt'
require 'net/http'
require 'uri'
class AppCheckVerification
def initialize(app, options = {})
@app = app
@project_number = options[:project_number]
end
def call(env)
app_id = verify(env['HTTP_X_FIREBASE_APPCHECK'])
return [401, { 'Content-Type' => 'text/plain' }, ['Unauthenticated']] unless app_id
env['firebase.app'] = app_id
@app.call(env)
end
def verify(token)
return unless token
# 1. Obtain the Firebase App Check Public Keys
# Note: It is not recommended to hard code these keys as they rotate,
# but you should cache them for up to 6 hours.
uri = URI('https://firebaseappcheck.googleapis.com/v1/jwks')
jwks = JSON(Net::HTTP.get(uri))
# 2. Verify the signature on the App Check token
payload, header = JWT.decode(token, nil, true, jwks: jwks, algorithms: 'RS256')
# 3. Ensure the token's header uses the algorithm RS256
return unless header['alg'] == 'RS256'
# 4. Ensure the token's header has type JWT
return unless header['typ'] == 'JWT'
# 5. Ensure the token is issued by App Check
return unless payload['iss'] == "https://firebaseappcheck.googleapis.com/#{@project_number}"
# 6. Ensure the token is not expired
return unless payload['exp'] > Time.new.to_i
# 7. Ensure the token's audience matches your project
return unless payload['aud'].include? "projects/#{@project_number}"
# 8. The token's subject will be the app ID, you may optionally filter against
# an allow list
payload['sub']
rescue
end
end
class Application
def call(env)
[200, { 'Content-Type' => 'text/plain' }, ["Hello app #{env['firebase.app']}"]]
end
end
use AppCheckVerification, project_number: 1234567890
run Application.new
重放攻击防范(Beta 版)
为保护端点免遭重放攻击,您可以在验证 App Check 令牌后消耗掉该令牌,即使其只能使用一次。
启用重放攻击防范会增加 verifyToken()
调用的网络往返时间,因此启用重放攻击防范的任何端点的延迟时间会增加。因此,我们建议您仅在特别敏感的端点上启用重放攻击防范。
如需使用重放攻击防范,请执行以下操作:
在 Cloud 控制台中,将“Firebase App Check Token Verifier”角色授予用于验证令牌的服务账号。
- 如果您使用从 Firebase 控制台下载的 Admin SDK 服务账号凭据初始化 Admin SDK,则此必要的角色已授予。
- 如果您将第 1 代 Cloud Functions 函数与默认的 Admin SDK 配置搭配使用,请将该角色授予 App Engine 默认服务账号。请参阅更改服务账号权限。
- 如果您将第 2 代 Cloud Functions 函数与默认的 Admin SDK 配置搭配使用,请将该角色授予默认计算服务账号。
然后,如需使用令牌,请将
{ consume: true }
传递给verifyToken()
方法并检查结果对象;如果alreadyConsumed
属性为true
,请拒绝请求或执行某种纠正操作,例如要求调用方通过其他检查。例如:
const appCheckClaims = await getAppCheck().verifyToken(appCheckToken, { consume: true }); if (appCheckClaims.alreadyConsumed) { res.status(401); return next('Unauthorized'); } // If verifyToken() succeeds and alreadyConsumed is not set, okay to continue.
这会验证令牌,然后将其标记为已使用。之后对同一令牌调用
verifyToken(appCheckToken, { consume: true })
会将alreadyConsumed
设为true
。(请注意,如果未设置consume
,verifyToken()
不会拒绝已消耗掉的令牌,甚至不会检查它是否已被消耗。)
若要为特定端点启用此功能,还必须更新应用客户端代码,以获取可用于该端点的限定使用次数型可消耗令牌。请参阅适用于 Apple 平台、Android 和 Web 的客户端文档。