第十四章:OpenFGA — 细粒度授权引擎

“OpenFGA 让你像 Google 一样管理权限 — 基于关系,而非角色。”

        mindmap
  root((OpenFGA))
    核心概念
      Type
      Relation
      Tuple
    API
      Check
      ListObjects
      Expand
      Write
    授权模型
      DSL
      Google Drive
      GitHub
      多租户
    部署
      Server
      PostgreSQL
      SDK
    

14.1 OpenFGA 是什么

OpenFGA(Fine-Grained Authorization)是由 Auth0/Okta 开源的细粒度授权引擎,基于 Google Zanzibar 论文实现。它是 CNCF Sandbox 项目。

        flowchart LR
    subgraph 应用层
        A1[Web 应用]
        A2[API 服务]
        A3[微服务]
    end

    subgraph OpenFGA Server
        direction TB
        AM[授权模型<br/>Authorization Model]
        RT[关系元组<br/>Relationship Tuples]
        GE[图遍历引擎<br/>Graph Engine]
        AM --> GE
        RT --> GE
    end

    subgraph 数据存储
        PG[(PostgreSQL)]
        MY[(MySQL)]
    end

    A1 -->|Check / Write / ListObjects| GE
    A2 -->|gRPC / HTTP| GE
    A3 -->|SDK| GE
    GE --> PG
    GE -.-> MY
    

授权检查流程

        sequenceDiagram
    participant App as 应用
    participant FGA as OpenFGA Server
    participant DB as PostgreSQL

    App->>FGA: Check(user:bob, viewer, document:budget)
    FGA->>DB: 查询 document:budget 的关系元组
    DB-->>FGA: 返回元组列表
    FGA->>FGA: 图遍历:bob 是 editor → editor 隐含 viewer
    FGA-->>App: { allowed: true }

    App->>FGA: ListObjects(user:carol, viewer, type:document)
    FGA->>DB: 查询 carol 相关的所有关系元组
    DB-->>FGA: 返回元组列表
    FGA->>FGA: 图遍历:展开所有可达的 document
    FGA-->>App: { objects: ["document:roadmap", "document:budget"] }
    

关系模型示例

        flowchart TB
    U1([user:alice]) -->|admin| Org[organization:acme]
    U2([user:bob]) -->|member| Org
    U3([user:carol]) -->|member| Org

    Org -->|member → viewer| D1[document:roadmap]
    Org -->|member → editor| D2[document:budget]

    U1 -->|owner| D1
    U2 -->|editor| D2
    U3 -->|viewer| D2

    style U1 fill:#e1f5fe
    style U2 fill:#e1f5fe
    style U3 fill:#e1f5fe
    style Org fill:#fff3e0
    style D1 fill:#e8f5e9
    style D2 fill:#e8f5e9
    

14.2 核心概念

授权模型(Authorization Model)

# OpenFGA DSL 语法
model
  schema 1.1

type user

type organization
  relations
    define admin: [user]
    define member: [user] or admin

type document
  relations
    define owner: [user]
    define editor: [user, organization#member]
    define viewer: [user, organization#member] or editor or owner
    define can_edit: editor or owner
    define can_view: viewer
    define can_delete: owner

关系元组(Relationship Tuple)

# 格式:object#relation@subject

# Alice 是 budget 文档的 owner
document:budget#owner@user:alice

# Bob 是 budget 文档的 editor
document:budget#editor@user:bob

# Acme 组织的所有 member 都是 roadmap 文档的 viewer
document:roadmap#viewer@organization:acme#member

# Carol 是 Acme 组织的 member
organization:acme#member@user:carol
→ 因此 Carol 可以查看 roadmap 文档(通过关系链推导)

API 操作

API

功能

示例

Check

检查是否有权限

Carol 能查看 roadmap 吗?→ ✅

ListObjects

列出有权限的对象

Carol 能查看哪些文档?

ListUsers

列出有权限的用户

谁能编辑 budget?

Expand

展开权限关系树

budget 的 viewer 包含哪些人?

Write

写入关系元组

添加/删除权限关系

Read

读取关系元组

查询已有的权限关系

14.3 实战场景:Google Drive 权限模型

model
  schema 1.1

type user

type group
  relations
    define member: [user, group#member]

type folder
  relations
    define owner: [user]
    define editor: [user, group#member] or owner
    define viewer: [user, group#member] or editor
    define parent: [folder]
    # 继承父文件夹的权限
    define can_edit: editor or owner or can_edit from parent
    define can_view: viewer or can_edit or can_view from parent

type document
  relations
    define owner: [user]
    define editor: [user, group#member] or owner
    define viewer: [user, group#member] or editor
    define parent: [folder]
    define can_edit: editor or owner or can_edit from parent
    define can_view: viewer or can_edit or can_view from parent
    define can_share: owner
    define can_delete: owner
关系元组示例:
# 文件夹层次
folder:engineering#parent@folder:root
folder:backend#parent@folder:engineering

# 文档归属
document:api-spec#parent@folder:backend

# 权限分配
folder:root#viewer@group:all-employees#member
folder:engineering#editor@group:eng-team#member
document:api-spec#owner@user:alice

# 权限推导:
# Bob 是 eng-team 的 member
# → Bob 是 engineering 文件夹的 editor
# → Bob 是 backend 文件夹的 editor(继承)
# → Bob 可以编辑 api-spec 文档(继承)

14.4 授权模型设计模式

模式一:文档共享(Google Docs 风格)

model
  schema 1.1

type user

type document
  relations
    define owner: [user]
    define editor: [user] or owner
    define commenter: [user] or editor
    define viewer: [user] or commenter
    # 分享链接:任何人可查看
    define public_viewer: [user:*]
    define can_view: viewer or public_viewer
    define can_comment: commenter
    define can_edit: editor
    define can_share: owner
    define can_delete: owner

模式二:组织层级(多租户 SaaS)

model
  schema 1.1

type user

type organization
  relations
    define owner: [user]
    define admin: [user] or owner
    define member: [user] or admin

type team
  relations
    define org: [organization]
    define lead: [user]
    define member: [user] or lead
    # 组织管理员自动成为团队管理者
    define admin: lead or admin from org

type project
  relations
    define team: [team]
    define manager: [user]
    define contributor: [user, team#member]
    define viewer: [user, team#member, organization#member] or contributor
    define can_edit: contributor or manager
    define can_view: viewer
    define can_admin: manager or admin from team

模式三:GitHub 仓库权限

model
  schema 1.1

type user

type organization
  relations
    define owner: [user]
    define member: [user] or owner

type team
  relations
    define maintainer: [user]
    define member: [user] or maintainer

type repository
  relations
    define org: [organization]
    define admin: [user, team#member] or owner from org
    define maintainer: [user, team#member] or admin
    define writer: [user, team#member] or maintainer
    define reader: [user, team#member, organization#member] or writer
    define can_push: writer
    define can_pull: reader
    define can_admin: admin

14.5 Python SDK 完整示例

import asyncio
from openfga_sdk import (
    ClientConfiguration,
    OpenFgaClient,
    ClientWriteRequest,
    ClientTupleKey,
    ClientCheckRequest,
    ClientListObjectsRequest,
)
from openfga_sdk.client.models import (
    WriteAuthorizationModelRequest,
    CreateStoreRequest,
)


async def main():
    # 1. 配置客户端(无 Store)
    config = ClientConfiguration(
        api_url="http://localhost:8080",
    )

    async with OpenFgaClient(config) as client:
        # 2. 创建 Store
        store = await client.create_store(
            CreateStoreRequest(name="my-app-store")
        )
        print(f"Store ID: {store.id}")

        # 更新配置以使用新 Store
        config.store_id = store.id

    # 3. 使用新 Store 重新连接
    async with OpenFgaClient(config) as client:
        # 4. 创建授权模型
        model = WriteAuthorizationModelRequest(
            schema_version="1.1",
            type_definitions=[
                {"type": "user"},
                {
                    "type": "document",
                    "relations": {
                        "owner": {"this": {}},
                        "editor": {"this": {}},
                        "viewer": {
                            "union": {
                                "child": [
                                    {"this": {}},
                                    {"computedUserset": {"relation": "editor"}},
                                    {"computedUserset": {"relation": "owner"}},
                                ]
                            }
                        },
                    },
                    "metadata": {
                        "relations": {
                            "owner": {
                                "directly_related_user_types": [{"type": "user"}]
                            },
                            "editor": {
                                "directly_related_user_types": [{"type": "user"}]
                            },
                            "viewer": {
                                "directly_related_user_types": [{"type": "user"}]
                            },
                        }
                    },
                },
            ],
        )
        auth_model = await client.write_authorization_model(model)
        print(f"模型 ID: {auth_model.authorization_model_id}")

        # 5. 写入关系元组
        await client.write(
            ClientWriteRequest(
                writes=[
                    ClientTupleKey(
                        user="user:alice",
                        relation="owner",
                        object="document:budget",
                    ),
                    ClientTupleKey(
                        user="user:bob",
                        relation="editor",
                        object="document:budget",
                    ),
                    ClientTupleKey(
                        user="user:carol",
                        relation="viewer",
                        object="document:budget",
                    ),
                ]
            )
        )
        print("关系元组写入成功")

        # 6. 检查权限
        result = await client.check(
            ClientCheckRequest(
                user="user:bob",
                relation="viewer",
                object="document:budget",
            )
        )
        print(f"Bob 能查看 budget? {result.allowed}")  # True(editor 隐含 viewer)

        result = await client.check(
            ClientCheckRequest(
                user="user:carol",
                relation="editor",
                object="document:budget",
            )
        )
        print(f"Carol 能编辑 budget? {result.allowed}")  # False

        # 7. 列出用户可访问的文档
        objects = await client.list_objects(
            ClientListObjectsRequest(
                user="user:alice",
                relation="viewer",
                type="document",
            )
        )
        print(f"Alice 可查看的文档: {objects.objects}")

        # 8. 删除关系元组
        await client.write(
            ClientWriteRequest(
                deletes=[
                    ClientTupleKey(
                        user="user:carol",
                        relation="viewer",
                        object="document:budget",
                    ),
                ]
            )
        )
        print("已移除 Carol 的 viewer 权限")


asyncio.run(main())

FastAPI + OpenFGA 授权中间件

"""FastAPI + OpenFGA 授权中间件完整示例"""
import asyncio
from contextlib import asynccontextmanager
from typing import Optional

from fastapi import FastAPI, Depends, HTTPException, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from openfga_sdk import (
    ClientConfiguration,
    OpenFgaClient,
    ClientCheckRequest,
    ClientWriteRequest,
    ClientTupleKey,
    ClientListObjectsRequest,
)

# ── 全局 OpenFGA 客户端 ──────────────────────────────────────
fga_client: Optional[OpenFgaClient] = None

FGA_CONFIG = ClientConfiguration(
    api_url="http://localhost:8080",
    store_id="your-store-id",
    authorization_model_id="your-model-id",
)


@asynccontextmanager
async def lifespan(app: FastAPI):
    global fga_client
    fga_client = OpenFgaClient(FGA_CONFIG)
    yield
    await fga_client.__aexit__(None, None, None)


app = FastAPI(title="OpenFGA Demo", lifespan=lifespan)
security = HTTPBearer()


# ── 用户解析(简化示例,实际应验证 JWT) ─────────────────────
async def get_current_user(
    credentials: HTTPAuthorizationCredentials = Depends(security),
) -> dict:
    # 实际项目中应验证 JWT 并提取用户信息
    token = credentials.credentials
    return {"id": token, "name": token}


# ── OpenFGA 授权依赖 ─────────────────────────────────────────
class FGAAuthorize:
    """可复用的 OpenFGA 授权依赖"""

    def __init__(self, relation: str, object_type: str, object_id_param: str):
        self.relation = relation
        self.object_type = object_type
        self.object_id_param = object_id_param

    async def __call__(
        self,
        request: Request,
        user: dict = Depends(get_current_user),
    ) -> dict:
        object_id = request.path_params.get(self.object_id_param)
        if not object_id:
            raise HTTPException(status_code=400, detail="Missing resource ID")

        result = await fga_client.check(
            ClientCheckRequest(
                user=f"user:{user['id']}",
                relation=self.relation,
                object=f"{self.object_type}:{object_id}",
            )
        )

        if not result.allowed:
            raise HTTPException(
                status_code=403,
                detail=f"No '{self.relation}' permission on {self.object_type}:{object_id}",
            )
        return user


# ── 路由 ─────────────────────────────────────────────────────
@app.get("/documents/{doc_id}")
async def get_document(
    doc_id: str,
    user: dict = Depends(FGAAuthorize("viewer", "document", "doc_id")),
):
    return {"doc_id": doc_id, "content": "...", "accessed_by": user["id"]}


@app.put("/documents/{doc_id}")
async def update_document(
    doc_id: str,
    user: dict = Depends(FGAAuthorize("editor", "document", "doc_id")),
):
    return {"doc_id": doc_id, "updated_by": user["id"]}


@app.delete("/documents/{doc_id}")
async def delete_document(
    doc_id: str,
    user: dict = Depends(FGAAuthorize("owner", "document", "doc_id")),
):
    return {"doc_id": doc_id, "deleted_by": user["id"]}


@app.post("/documents/{doc_id}/share")
async def share_document(
    doc_id: str,
    target_user: str,
    relation: str = "viewer",
    user: dict = Depends(FGAAuthorize("owner", "document", "doc_id")),
):
    """文档所有者可以分享文档给其他用户"""
    await fga_client.write(
        ClientWriteRequest(
            writes=[
                ClientTupleKey(
                    user=f"user:{target_user}",
                    relation=relation,
                    object=f"document:{doc_id}",
                )
            ]
        )
    )
    return {"shared": True, "doc_id": doc_id, "target": target_user, "relation": relation}


@app.get("/my/documents")
async def list_my_documents(
    user: dict = Depends(get_current_user),
):
    """列出当前用户可查看的所有文档"""
    result = await fga_client.list_objects(
        ClientListObjectsRequest(
            user=f"user:{user['id']}",
            relation="viewer",
            type="document",
        )
    )
    return {"user": user["id"], "documents": result.objects}

14.6 Java SDK 完整示例

// build.gradle
// dependencies {
//     implementation 'dev.openfga:openfga-sdk:0.7.+'
// }

import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.client.ClientConfiguration;
import dev.openfga.sdk.api.client.model.*;
import dev.openfga.sdk.api.model.*;

import java.util.List;

public class OpenFGAExample {

    public static void main(String[] args) throws Exception {
        // 1. 配置客户端
        var config = new ClientConfiguration()
                .apiUrl("http://localhost:8080")
                .storeId("your-store-id");

        var client = new OpenFgaClient(config);

        // 2. 创建 Store
        var createStoreResponse = client
                .createStore(new CreateStoreRequest().name("my-java-store"))
                .get();
        System.out.println("Store ID: " + createStoreResponse.getId());
        config.storeId(createStoreResponse.getId());

        // 3. 写入授权模型
        var model = new WriteAuthorizationModelRequest()
                .schemaVersion("1.1")
                .typeDefinitions(List.of(
                        new TypeDefinition().type("user"),
                        new TypeDefinition()
                                .type("document")
                                .relations(java.util.Map.of(
                                        "owner", new Userset().direct(new DirectUserset()),
                                        "editor", new Userset().direct(new DirectUserset()),
                                        "viewer", new Userset().union(new Usersets().child(List.of(
                                                new Userset().direct(new DirectUserset()),
                                                new Userset().computedUserset(
                                                        new ObjectRelation().relation("editor")),
                                                new Userset().computedUserset(
                                                        new ObjectRelation().relation("owner"))
                                        )))
                                ))
                                .metadata(new Metadata().relations(java.util.Map.of(
                                        "owner", new RelationMetadata()
                                                .directlyRelatedUserTypes(List.of(
                                                        new RelationReference().type("user"))),
                                        "editor", new RelationMetadata()
                                                .directlyRelatedUserTypes(List.of(
                                                        new RelationReference().type("user"))),
                                        "viewer", new RelationMetadata()
                                                .directlyRelatedUserTypes(List.of(
                                                        new RelationReference().type("user")))
                                )))
                ));

        var modelResponse = client.writeAuthorizationModel(model).get();
        System.out.println("模型 ID: " + modelResponse.getAuthorizationModelId());

        // 4. 写入关系元组
        var writeRequest = new ClientWriteRequest()
                .writes(List.of(
                        new ClientTupleKey()
                                .user("user:alice")
                                .relation("owner")
                                ._object("document:budget"),
                        new ClientTupleKey()
                                .user("user:bob")
                                .relation("editor")
                                ._object("document:budget"),
                        new ClientTupleKey()
                                .user("user:carol")
                                .relation("viewer")
                                ._object("document:budget")
                ));
        client.write(writeRequest).get();
        System.out.println("关系元组写入成功");

        // 5. 检查权限
        var checkRequest = new ClientCheckRequest()
                .user("user:bob")
                .relation("viewer")
                ._object("document:budget");
        var checkResponse = client.check(checkRequest).get();
        System.out.println("Bob 能查看 budget? " + checkResponse.getAllowed());
        // true(editor 隐含 viewer)

        // 6. 列出用户可访问的文档
        var listRequest = new ClientListObjectsRequest()
                .user("user:alice")
                .relation("viewer")
                .type("document");
        var listResponse = client.listObjects(listRequest).get();
        System.out.println("Alice 可查看的文档: " + listResponse.getObjects());
    }
}

Spring Boot + OpenFGA 集成

// ── OpenFGA 配置 ────────────────────────────────────────────
// application.yml:
// openfga:
//   api-url: http://localhost:8080
//   store-id: your-store-id
//   authorization-model-id: your-model-id

import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.client.ClientConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@ConfigurationProperties(prefix = "openfga")
record OpenFGAProperties(String apiUrl, String storeId, String authorizationModelId) {}

@Configuration
class OpenFGAConfig {

    @Bean
    OpenFgaClient openFgaClient(OpenFGAProperties props) {
        var config = new ClientConfiguration()
                .apiUrl(props.apiUrl())
                .storeId(props.storeId())
                .authorizationModelId(props.authorizationModelId());
        return new OpenFgaClient(config);
    }
}

// ── 授权服务 ────────────────────────────────────────────────
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.client.model.ClientCheckRequest;
import org.springframework.stereotype.Service;

@Service
class FGAAuthorizationService {

    private final OpenFgaClient fgaClient;

    FGAAuthorizationService(OpenFgaClient fgaClient) {
        this.fgaClient = fgaClient;
    }

    public boolean check(String userId, String relation, String objectType, String objectId) {
        try {
            var request = new ClientCheckRequest()
                    .user("user:" + userId)
                    .relation(relation)
                    ._object(objectType + ":" + objectId);
            var response = fgaClient.check(request).get();
            return Boolean.TRUE.equals(response.getAllowed());
        } catch (Exception e) {
            throw new RuntimeException("OpenFGA check failed", e);
        }
    }
}

// ── 自定义注解 + AOP ────────────────────────────────────────
import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface FGAPreAuthorize {
    String relation();
    String objectType();
    /** SpEL 表达式,从方法参数中提取 objectId */
    String objectId();
}

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;

@Aspect
@Component
class FGAAuthorizationAspect {

    private final FGAAuthorizationService authService;
    private final SpelExpressionParser parser = new SpelExpressionParser();

    FGAAuthorizationAspect(FGAAuthorizationService authService) {
        this.authService = authService;
    }

    @Around("@annotation(fgaAuth)")
    public Object checkPermission(ProceedingJoinPoint joinPoint, FGAPreAuthorize fgaAuth)
            throws Throwable {
        // 从 SecurityContext 获取当前用户
        String userId = SecurityContextHolder.getContext()
                .getAuthentication().getName();

        // 用 SpEL 解析 objectId
        MethodSignature sig = (MethodSignature) joinPoint.getSignature();
        String[] paramNames = sig.getParameterNames();
        Object[] args = joinPoint.getArgs();
        StandardEvaluationContext ctx = new StandardEvaluationContext();
        for (int i = 0; i < paramNames.length; i++) {
            ctx.setVariable(paramNames[i], args[i]);
        }
        String objectId = parser.parseExpression(fgaAuth.objectId()).getValue(ctx, String.class);

        // 调用 OpenFGA 检查
        boolean allowed = authService.check(
                userId, fgaAuth.relation(), fgaAuth.objectType(), objectId);

        if (!allowed) {
            throw new ResponseStatusException(HttpStatus.FORBIDDEN,
                    "No '" + fgaAuth.relation() + "' permission on "
                            + fgaAuth.objectType() + ":" + objectId);
        }

        return joinPoint.proceed();
    }
}

// ── Controller 使用示例 ─────────────────────────────────────
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/documents")
class DocumentController {

    @GetMapping("/{docId}")
    @FGAPreAuthorize(relation = "viewer", objectType = "document", objectId = "#docId")
    public Map<String, Object> getDocument(@PathVariable String docId) {
        return Map.of("docId", docId, "content", "...");
    }

    @PutMapping("/{docId}")
    @FGAPreAuthorize(relation = "editor", objectType = "document", objectId = "#docId")
    public Map<String, Object> updateDocument(@PathVariable String docId,
                                               @RequestBody Map<String, Object> body) {
        return Map.of("docId", docId, "updated", true);
    }

    @DeleteMapping("/{docId}")
    @FGAPreAuthorize(relation = "owner", objectType = "document", objectId = "#docId")
    public Map<String, Object> deleteDocument(@PathVariable String docId) {
        return Map.of("docId", docId, "deleted", true);
    }
}

14.7 Go SDK 完整示例

package main

import (
	"context"
	"fmt"
	"log"

	openfga "github.com/openfga/go-sdk"
	"github.com/openfga/go-sdk/client"
)

func main() {
	ctx := context.Background()

	// 1. 配置客户端
	fgaClient, err := client.NewSdkClient(&client.ClientConfiguration{
		ApiUrl: "http://localhost:8080",
	})
	if err != nil {
		log.Fatalf("创建客户端失败: %v", err)
	}

	// 2. 创建 Store
	storeResp, err := fgaClient.CreateStore(ctx).Body(client.ClientCreateStoreRequest{
		Name: "my-go-store",
	}).Execute()
	if err != nil {
		log.Fatalf("创建 Store 失败: %v", err)
	}
	fmt.Printf("Store ID: %s\n", storeResp.GetId())

	// 更新客户端配置
	fgaClient, err = client.NewSdkClient(&client.ClientConfiguration{
		ApiUrl:  "http://localhost:8080",
		StoreId: storeResp.GetId(),
	})
	if err != nil {
		log.Fatalf("重新创建客户端失败: %v", err)
	}

	// 3. 写入授权模型
	schemaVersion := "1.1"
	modelResp, err := fgaClient.WriteAuthorizationModel(ctx).Body(openfga.WriteAuthorizationModelRequest{
		SchemaVersion: schemaVersion,
		TypeDefinitions: []openfga.TypeDefinition{
			{Type: "user"},
			{
				Type: "document",
				Relations: &map[string]openfga.Userset{
					"owner":  {This: &openfga.DirectUserset{}},
					"editor": {This: &openfga.DirectUserset{}},
					"viewer": {
						Union: &openfga.Usersets{
							Child: []openfga.Userset{
								{This: &openfga.DirectUserset{}},
								{ComputedUserset: &openfga.ObjectRelation{Relation: strPtr("editor")}},
								{ComputedUserset: &openfga.ObjectRelation{Relation: strPtr("owner")}},
							},
						},
					},
				},
				Metadata: &openfga.Metadata{
					Relations: &map[string]openfga.RelationMetadata{
						"owner": {DirectlyRelatedUserTypes: &[]openfga.RelationReference{
							{Type: "user"},
						}},
						"editor": {DirectlyRelatedUserTypes: &[]openfga.RelationReference{
							{Type: "user"},
						}},
						"viewer": {DirectlyRelatedUserTypes: &[]openfga.RelationReference{
							{Type: "user"},
						}},
					},
				},
			},
		},
	}).Execute()
	if err != nil {
		log.Fatalf("写入模型失败: %v", err)
	}
	fmt.Printf("模型 ID: %s\n", modelResp.GetAuthorizationModelId())

	// 4. 写入关系元组
	_, err = fgaClient.Write(ctx).Body(client.ClientWriteRequest{
		Writes: []client.ClientTupleKey{
			{User: "user:alice", Relation: "owner", Object: "document:budget"},
			{User: "user:bob", Relation: "editor", Object: "document:budget"},
			{User: "user:carol", Relation: "viewer", Object: "document:budget"},
		},
	}).Execute()
	if err != nil {
		log.Fatalf("写入元组失败: %v", err)
	}
	fmt.Println("关系元组写入成功")

	// 5. 检查权限
	checkResp, err := fgaClient.Check(ctx).Body(client.ClientCheckRequest{
		User:     "user:bob",
		Relation: "viewer",
		Object:   "document:budget",
	}).Execute()
	if err != nil {
		log.Fatalf("检查权限失败: %v", err)
	}
	fmt.Printf("Bob 能查看 budget? %v\n", checkResp.GetAllowed()) // true

	checkResp, err = fgaClient.Check(ctx).Body(client.ClientCheckRequest{
		User:     "user:carol",
		Relation: "editor",
		Object:   "document:budget",
	}).Execute()
	if err != nil {
		log.Fatalf("检查权限失败: %v", err)
	}
	fmt.Printf("Carol 能编辑 budget? %v\n", checkResp.GetAllowed()) // false

	// 6. 列出用户可访问的文档
	listResp, err := fgaClient.ListObjects(ctx).Body(client.ClientListObjectsRequest{
		User:     "user:alice",
		Relation: "viewer",
		Type:     "document",
	}).Execute()
	if err != nil {
		log.Fatalf("列出对象失败: %v", err)
	}
	fmt.Printf("Alice 可查看的文档: %v\n", listResp.GetObjects())
}

func strPtr(s string) *string { return &s }

Gin + OpenFGA 授权中间件

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/openfga/go-sdk/client"
)

// ── 全局 OpenFGA 客户端 ─────────────────────────────────────
var fgaClient *client.OpenFgaClient

func initFGAClient() {
	var err error
	fgaClient, err = client.NewSdkClient(&client.ClientConfiguration{
		ApiUrl:               "http://localhost:8080",
		StoreId:              "your-store-id",
		AuthorizationModelId: "your-model-id",
	})
	if err != nil {
		log.Fatalf("初始化 OpenFGA 客户端失败: %v", err)
	}
}

// ── 授权中间件 ──────────────────────────────────────────────
func FGAAuthorize(relation, objectType, paramName string) gin.HandlerFunc {
	return func(c *gin.Context) {
		// 从上下文获取用户 ID(由认证中间件设置)
		userID, exists := c.Get("userID")
		if !exists {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
			return
		}

		objectID := c.Param(paramName)
		if objectID == "" {
			c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "缺少资源 ID"})
			return
		}

		// 调用 OpenFGA 检查权限
		resp, err := fgaClient.Check(context.Background()).Body(client.ClientCheckRequest{
			User:     fmt.Sprintf("user:%s", userID),
			Relation: relation,
			Object:   fmt.Sprintf("%s:%s", objectType, objectID),
		}).Execute()

		if err != nil {
			c.AbortWithStatusJSON(http.StatusInternalServerError,
				gin.H{"error": "授权检查失败"})
			return
		}

		if !resp.GetAllowed() {
			c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
				"error": fmt.Sprintf("无 '%s' 权限: %s:%s", relation, objectType, objectID),
			})
			return
		}

		c.Next()
	}
}

// ── 认证中间件(简化示例) ──────────────────────────────────
func AuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		token := c.GetHeader("Authorization")
		if token == "" {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "缺少 token"})
			return
		}
		// 实际项目中应验证 JWT
		c.Set("userID", token)
		c.Next()
	}
}

func main() {
	initFGAClient()

	r := gin.Default()
	api := r.Group("/api", AuthMiddleware())
	{
		docs := api.Group("/documents")
		{
			docs.GET("/:docId",
				FGAAuthorize("viewer", "document", "docId"),
				getDocument)

			docs.PUT("/:docId",
				FGAAuthorize("editor", "document", "docId"),
				updateDocument)

			docs.DELETE("/:docId",
				FGAAuthorize("owner", "document", "docId"),
				deleteDocument)
		}
	}

	r.Run(":9090")
}

func getDocument(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"docId":   c.Param("docId"),
		"content": "...",
	})
}

func updateDocument(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"docId":   c.Param("docId"),
		"updated": true,
	})
}

func deleteDocument(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"docId":   c.Param("docId"),
		"deleted": true,
	})
}

14.8 OpenFGA 部署

# docker-compose.yml
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: openfga
      POSTGRES_PASSWORD: openfga_password
      POSTGRES_DB: openfga
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U openfga"]
      interval: 5s
      timeout: 5s
      retries: 5

  openfga-migrate:
    image: openfga/openfga:latest
    command: migrate
    environment:
      OPENFGA_DATASTORE_ENGINE: postgres
      OPENFGA_DATASTORE_URI: postgres://openfga:openfga_password@postgres:5432/openfga?sslmode=disable
    depends_on:
      postgres:
        condition: service_healthy

  openfga:
    image: openfga/openfga:latest
    command: run
    environment:
      OPENFGA_DATASTORE_ENGINE: postgres
      OPENFGA_DATASTORE_URI: postgres://openfga:openfga_password@postgres:5432/openfga?sslmode=disable
      OPENFGA_PLAYGROUND_ENABLED: "true"
      OPENFGA_LOG_LEVEL: info
    ports:
      - "8080:8080"   # HTTP API
      - "8081:8081"   # gRPC API
      - "3000:3000"   # Playground UI
    depends_on:
      openfga-migrate:
        condition: service_completed_successfully
    healthcheck:
      test: ["CMD", "/usr/local/bin/grpc_health_probe", "-addr=:8081"]
      interval: 10s
      timeout: 5s
      retries: 3

volumes:
  pgdata:
# 启动
docker compose up -d

# 验证
curl http://localhost:8080/healthz

# 打开 Playground
open http://localhost:3000

# 创建 Store
curl -X POST http://localhost:8080/stores \
  -H 'Content-Type: application/json' \
  -d '{"name": "my-store"}'

14.9 OpenFGA vs Casbin vs OPA 对比

维度

OpenFGA

Casbin

OPA

模型

ReBAC(关系驱动)

RBAC/ABAC/RESTful

ABAC(策略驱动)

语言

DSL(类型+关系)

PERM 模型 + 策略文件

Rego

数据存储

PostgreSQL/MySQL

文件/DB/Redis

内存/Bundle

核心能力

关系图遍历

模型适配器

通用策略评估

Check 性能

~5ms(图遍历+缓存)

~0.1ms(内存)

~1ms(内存)

ListObjects

✅ 原生支持

❌ 需自行实现

❌ 需自行实现

权限继承

✅ 关系链自动推导

⚠️ 需手动配置

⚠️ 需 Rego 编写

多租户

✅ Store 隔离

⚠️ 需自行实现

⚠️ 需策略设计

部署

独立服务

嵌入式库

独立服务/嵌入式

适用场景

协作应用、SaaS 权限

简单 RBAC、中小项目

K8s 策略、合规审计

CNCF

Sandbox

Graduated

灵感来源

Google Zanzibar

ACL 模型

Datalog

选型建议

  • 资源级细粒度权限(谁能访问哪个文档)→ OpenFGA

  • 简单角色权限(管理员/编辑/查看者)→ Casbin

  • 复杂条件策略(时间、IP、合规规则)→ OPA

  • 组合使用:OpenFGA 管理关系 + OPA 管理条件策略

14.10 OpenFGA vs RBAC

维度

RBAC

OpenFGA (ReBAC)

授权依据

角色

关系

粒度

角色级

对象级

“谁能访问X”

需要遍历所有角色

直接查询

“X能访问什么”

需要遍历所有资源

ListObjects API

继承

角色层次

关系链(更灵活)

性能

O(1) 角色检查

图遍历(有缓存)

适用场景

简单权限

协作应用、SaaS

14.11 小结

  • OpenFGA 基于 Google Zanzibar,提供关系驱动的细粒度授权

  • 核心是关系元组object#relation@subject

  • 支持权限继承(通过关系链推导)

  • 提供 Check、ListObjects、ListUsers 等丰富的 API

  • 适合协作应用(文档、项目管理)和多租户 SaaS

  • 与 RBAC 相比,OpenFGA 在资源级别的细粒度控制上更有优势

  • 提供 Python、Java、Go 等多语言 SDK,可与主流 Web 框架深度集成