前言

最近在準備嘗試提供 GrapchQL API,所以在開始前先究了一下筆者感性趣的部份。 本篇文章雖然還不會實際使用程式碼產生器 gqlgen,但還是先對於「權限管理」的部份稍稍的提到一些。 於是這篇就這樣誕生啦~

主要內容

為什麼使用 GraphQL

在考慮 API 設計時,同學們可能會傳統地想到 REST。 那麼,為什麼會想要使用 GraphQL 呢?

官方文件:

  • GraphQL 是一種「為你的 API 所定義的查詢語言」與「在服務端執行查詢的執行時 (runtime)」。
  • 它不受限於特定資料庫或儲存引擎。
  • 它允許客戶端「精確查詢自己需要的資料」,而不是被迫取得多餘或不足的資料。
  • 它也支援 API 的演進,而不是透過版本化 (versioning) 的方式來管理變動。

舉個栗子: 如果把 API 想像成「菜單」給前端看,傳統的 REST 有時像「套餐固定」,拿到的是固定內容;而 GraphQL 則「可以自己選菜」—只選想吃的、減少浪費。

因此,如果應用場景是:前端/客戶端需要較為靈活地提出複雜查詢、資料來源多、需要盡量減少 round‐trip 或 overfetch/underfetch,GraphQL 是很值得考慮的。 當然,並非所有場景都適合(例如非常簡單的 CRUD、或者服務間輕量通訊可能仍是 REST 或 gRPC 更合適)。

GraphQL 的語法基礎

GraphQL 的核心由 schema(模式)/type 系統query/mutation、以及 解析 (resolver) 三大要素組成。以下依序說明。

定義 Schema 與 Type

在 GraphQL 中,先必須定義 API 所支援的型別 (types) 及其欄位 (fields),然後為每個欄位撰寫對應的解析函式 (resolver)。 官方範例:

1type Query {
2  me: User
3}
4
5type User {
6  name: String
7}

同學們需要在服務端提供 me 欄位的 resolver,比如:

1function resolveQueryMe(_parent, _args, context, _info) {
2  return context.request.auth.user;
3}
4
5function resolveUserName(user, _args, context, _info) {
6  return context.db.getUserFullName(user.id);
7}

重點在於:

  • API 描述變成 type system,而不是只靠 endpoint 路由。
  • 客戶端查詢 (query) 的語法會「鏡像」我們所需要的資料形狀。
  • 解析器負責把 type 欄位對應到真正的資料來源(例如資料庫、服務端邏輯)。

撰寫 Query(與 Mutation)

一旦 Schema 定義好,就可以接受客戶端提出查詢。範例:

1{
2  me {
3    name
4  }
5}

這會回傳:

1{
2  "data": {
3    "me": {
4      "name": "Luke Skywalker"
5    }
6  }
7}

這裡可以看到:

  • 客戶端只拿到「需要的欄位」name,而不會拿到不需要的 fullName、nickname。
  • 也不需要版本化 API。當有新增欄位時,仍舊支援舊的查詢。官方示例中提到將 Username 擴充至 fullNamenickname,並把 name 標記為 @deprecated
1type User {
2  fullName: String
3  nickname: String
4  name: String @deprecated(reason: "Use `fullName`.")
5}

權限控制

理解了語法與基礎後,接下來看「如何在 Go 語言環境中為 GraphQL 加上驗證 (Authentication) 與授權 (Authorization)」。 以下內容主要參考了 gqlgen + 中介軟體 (middleware) + 自訂指令 (directives) 的實作。

驗證 (Authentication) 與授權 (Authorization) 的差異

  • 驗證 (Authentication):確認使用者是誰(例如用 JWT token 驗證身份)
  • 授權 (Authorization):確認這位使用者「是否有權」執行某操作或存取特定欄位/資源

使用 GraphQL Directive 在 Schema 層做控制

在 Go 的 gqlgen 中,你可以定義自訂 directive。例如:

1directive @auth(roles: [String!]) on FIELD_DEFINITION

並在 Schema 中這樣用:

1type Query {
2  listUsers: [User]! @auth(roles: ["ADMIN"])
3  currentUserProfile: User @auth
4}

上面兩條的意義分別為:

  • listUsers 僅允許角色為 ADMIN 的使用者。
  • currentUserProfile 只要經過認證即可(沒有指定角色)。

實作步驟

  1. 定義 directive:在 schema 文件中宣告 @auth

  2. 在 middleware 驗證 JWT:在 HTTP 層(如使用 Echo、Gin 等)攔截 Authorization header,解析 JWT、抽取 claims(例如 email/role),將使用者資訊存入 context。 ([Medium][3])

     1func AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
     2  return func(c echo.Context) error {
     3    authHeader := c.Request().Header.Get("Authorization")
     4    tokenString := strings.TrimPrefix(authHeader, "Bearer ")
     5    // 驗證 token
     6    // 取出 user、role
     7    ctx := context.WithValue(c.Request().Context(), userContextKey, user)
     8    c.SetRequest(c.Request().WithContext(ctx))
     9    return next(c)
    10  }
    11}
    
  3. 實作 directive 處理函式:例如 AuthDirective,它會從 context 拿出 user,檢查「是否驗證過」及「是否具備指定角色」。若沒通過,則回傳錯誤。

     1func AuthDirective(ctx context.Context, obj interface{}, next graphql.Resolver, roles []string) (interface{}, error) {
     2  user, ok := GetUserFromContext(ctx)
     3  if !ok {
     4    return nil, fmt.Errorf("unauthenticated")
     5  }
     6  if len(roles)==0 {
     7    // 只要驗證過即可
     8    return next(ctx)
     9  }
    10  for _, role := range roles {
    11    if user.Role == role {
    12      return next(ctx)
    13    }
    14  }
    15  return nil, fmt.Errorf("unauthorized: requires roles %v", roles)
    16}
    
  4. 在 gqlgen 設定中註冊 directive:在 gqlgen.yml 或設定函式中指定 Auth: AuthDirective

為什麼這樣做好?

  • 將「誰可以查哪個欄位」的邏輯寫進 Schema(使用 @auth )——可讀性高、維護容易。
  • 將驗證(JWT、中介)與授權(directive)分離,使邏輯清楚、重用容易。
  • 適合使用 Go + gqlgen 架構的情境:已有後端、也想為查詢加上安全層。

注意事項

  • JWT 驗證的密鑰管理、過期控制、Token 刷新機制…這些不是 GraphQL 特有,但必須妥善設計。
  • 欄位級別的授權會比僅在 Query/Mutation 層更細,但也更複雜。你須評估是否真正有需要。
  • Schema 隨時間演進時,若新增欄位但忘記授權指令,可能造成安全漏洞。
  • 當後端還有第三方資料來源、多服務整合、Gateway 架構時(例如多個 GraphQL 服務合併),授權邏輯可能更複雜。

小結

  • GraphQL 是一種強大且靈活的 API 查詢語言/運行時系統,適合客戶端對資料存取有彈性需求的場景。
  • 基礎流程:定義 Schema → 撰寫 Resolver → 客戶端查詢,並且可以進行 API 演進而不用版本化。
  • 在 Go 語言環境中,使用 gqlgen + 自訂 @auth directive + JWT middleware,就可以為 GraphQL API 架構可靠的授權機制。
  • 實作時要同步關注驗證、授權、安全性(例如資料洩漏/過度存取)與維護性。

參考連結