不久之前寫過一篇『從 graphql-go 轉換到 gqlgen』,目前團隊舊有的專案還是繼續用 graphql-go 來撰寫,不過之後需求量越來越大,維護 graphql-go 就越來越困難,故有在想怎麼把 gqlgen 跟 graphql-go 相容在一起,那就是把這兩個套件想成不同的服務,再透過 Gateway 方式完成 single data graph。至於怎麼選擇 GraphQL Gateway 套件,最容易的方式就是使用 @apollo/gateway,但是由於個人比較偏好 Go 語言的解決方案,就稍微找看看有無人用 Go 實現了 Gateway,後來找到 nautilus/gateway,官方有提供文件以及教學 Blog 可以供開發者參考。底下會教大家使用 nautilus/gateway 將兩個不同的服務串接在一起。
線上影片
- 00:00 為什麼有 GraphQL Gateway 需求?
- 02:16 用兩個 Routing 來區分 graphql-go 跟 gqlgen
- 03:00 用 jwt token check
- 03:40 選擇 GraphQL Gateway 套件
- 04:58 main.go 撰寫機制介紹
- 06:05 如何將 Token 往後面 Service 發送?
- 06:58 看看完整的程式代碼
- 07:56 最後心得感想
如果對於課程內容有興趣,可以參考底下課程。
如果需要搭配購買請直接透過 FB 聯絡我,直接匯款(價格再減 100)
整合 graphql-go + gqlgen
要把兩個不同的套件整合在一起,最簡單的方式就是分不同的 URL 區隔開來,兩邊都是透過 Bearer Token 來進行使用者身份確認。
g := e.Group("/graphql")
g.Use(auth.Check())
{
g.POST("", graphql.Handler())
if config.Server.GraphiQL {
g.GET("", graphql.Handler())
}
}
q := root.Group("/query")
q.Use(auth.Check())
{
q.POST("", gqlgen.SchemaHandler())
q.GET("", gqlgen.SchemaHandler())
}
透過 jwt 驗證及讀取使用者資料
// Check user bearer token
func Check() gin.HandlerFunc {
return func(c *gin.Context) {
if data, err := jwt.New().GetClaimsFromJWT(c); err != nil {
c.Next()
} else if id, ok := data["id"]; ok {
var userID int64
switch v := id.(type) {
case int:
userID = int64(v)
case string:
i, err := strconv.ParseInt(v, 10, 64)
if err != nil {
log.Error().Err(err).Msg("can't convert user id to int64")
}
userID = i
case float64:
userID = int64(v)
default:
log.Info().Msgf("I don't know about user id type %T from token!", v)
}
user, err := model.GetUserByID(userID)
if err != nil {
log.Error().Err(err).Msg("can't get user data")
}
ctx := context.WithValue(
c.Request.Context(),
config.ContextKeyUser,
user,
)
c.Request = c.Request.WithContext(ctx)
}
}
}
撰寫 graphql-gateway
使用 nautilus/gateway 可以簡單將 Schema 合併成單一 Data,不過此套件尚未支援 subscription。
func main() {
// default port
port := "3001"
server := "api:8080"
if v, ok := os.LookupEnv("APP_PORT"); ok {
port = v
}
if v, ok := os.LookupEnv("APP_SERVER"); ok {
server = v
}
// introspect the apis
schemas, err := graphql.IntrospectRemoteSchemas(
"http://"+server+"/graphql",
"http://"+server+"/query",
)
if err != nil {
panic(err)
}
// create the gateway instance
gw, err := gateway.New(schemas, gateway.WithMiddlewares(forwardUserID))
if err != nil {
panic(err)
}
// add the playground endpoint to the router
http.HandleFunc("/graphql", withUserInfo(gw.PlaygroundHandler))
// start the server
fmt.Printf("🚀 Gateway is ready at http://localhost:%s/graphql\n", port)
err = http.ListenAndServe(fmt.Sprintf(":%s", port), nil)
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
}
由於之後要整合進 Docker 內,故透過 LookupEnv 來決定 Server 跟 Port。這樣可以將 /graphql
及 /query
的 Schema 綁定在一起了。另外要解決的就是如何將 Authorization 傳到後面 GraphQL Server 進行認證。
// the first thing we need to define is a middleware for our handler
// that grabs the Authorization header and sets the context value for
// our user id
func withUserInfo(handler http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// look up the value of the Authorization header
tokenValue := r.Header.Get("Authorization")
// Allow CORS here By * or specific origin
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "authorization, origin, content-type, accept")
w.Header().Set("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS")
// here is where you would perform some kind of validation on the token
// but we're going to skip that for this example and just save it as the
// id directly. PLEASE, DO NOT DO THIS IN PRODUCTION.
// invoke the handler with the new context
handler.ServeHTTP(w, r.WithContext(
context.WithValue(r.Context(), "tokenValue", tokenValue),
))
})
}
// the next thing we need to do is to modify the network requests to our services.
// To do this, we have to define a middleware that pulls the id of the user out
// of the context of the incoming request and sets it as the USER_ID header.
var forwardUserID = gateway.RequestMiddleware(func(r *http.Request) error {
// the initial context of the request is set as the same context
// provided by net/http
// we are safe to extract the value we saved in context and set it as the outbound header
if tokenValue := r.Context().Value("tokenValue"); tokenValue != nil {
r.Header.Set("Authorization", tokenValue.(string))
}
// return the modified request
return nil
})
其中上面的 Access-Control 用來解決 CORS 相關問題。前端用各自電腦開發時,就需要此部分。
心得
用 gqlgen 在開發上效率差很多,現在透過這方式,可以保留舊的 Schema 搭配新的 gqlgen 開發模式,未來也可以將共通的功能獨立拆成單一服務,再透過 gateway 方式將多個模組合併。