在自定义后端验证 App Check 令牌

您可以使用 App Check 来保护应用的非 Firebase 资源,例如自托管的后端。为此,您需要执行以下两项操作:

  • 修改您的应用客户端,以将 App Check 令牌随每个请求一起发送到后端,如 iOS+AndroidWeb 对应的页面所述。
  • 按照本页所述,修改后端以要求将有效的 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 令牌。

您的令牌验证逻辑必须完成以下步骤:

  1. 从 App Check JWKS 端点 (https://firebaseappcheck.googleapis.com/v1/jwks) 获取 Firebase App Check 公共 JSON Web 密钥 (JWK) 集
  2. 验证 App Check 令牌的签名以确保其合法性。
  3. 确保令牌的标头使用 RS256 算法。
  4. 确保令牌的标头类型为 JWT。
  5. 确保令牌由 Firebase App Check 在您的项目下颁发。
  6. 确保令牌未过期。
  7. 确保令牌的目标设备与您的项目匹配。
  8. 可选:检查令牌的主题是否与应用的 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() 调用的网络往返时间,因此启用重放攻击防范的任何端点的延迟时间会增加。因此,我们建议您仅在特别敏感的端点上启用重放攻击防范。

如需使用重放攻击防范,请执行以下操作:

  1. Cloud 控制台中,将“Firebase App Check Token Verifier”角色授予用于验证令牌的服务账号。

    • 如果您使用从 Firebase 控制台下载的 Admin SDK 服务账号凭据初始化 Admin SDK,则此必要的角色已授予。
    • 如果您将第 1 代 Cloud Functions 函数与默认的 Admin SDK 配置搭配使用,请将该角色授予 App Engine 默认服务账号。请参阅更改服务账号权限
    • 如果您将第 2 代 Cloud Functions 函数与默认的 Admin SDK 配置搭配使用,请将该角色授予默认计算服务账号
  2. 然后,如需使用令牌,请将 { 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。(请注意,如果未设置 consumeverifyToken() 不会拒绝已消耗掉的令牌,甚至不会检查它是否已被消耗。)

若要为特定端点启用此功能,还必须更新应用客户端代码,以获取可用于该端点的限定使用次数型可消耗令牌。请参阅适用于 Apple 平台AndroidWeb 的客户端文档。