初探 GraphQL
前言
最近在準備嘗試提供 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。當有新增欄位時,仍舊支援舊的查詢。官方示例中提到將
User從name擴充至fullName與nickname,並把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只要經過認證即可(沒有指定角色)。
實作步驟
定義 directive:在 schema 文件中宣告
@auth。在 middleware 驗證 JWT:在 HTTP 層(如使用 Echo、Gin 等)攔截
Authorizationheader,解析 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}實作 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}在 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 架構可靠的授權機制。
- 實作時要同步關注驗證、授權、安全性(例如資料洩漏/過度存取)與維護性。