prisma反向代理

概要<a id="sec-1"></a>

接觸 prisma 有段時間了, 期間也使用過其餘幾種 graphql 接口自動生成的框架. 總的來講, 仍是 prisma 生成的接口比較豐富, 使用上也比較方便, 和數據庫之間耦合也低.javascript

prisma 文檔: https://www.prisma.io/docs (寫本文時是 1.34 版)前端

爲何要作 prisma 的反向代理<a id="sec-2"></a>

prisma 服務雖然自動生成了接口, 可是這些接口其實不建議直接暴露給前端來用, 由於實際項目中, 最基本的要對接口進行認證和權限控制. 甚至還有其餘需求, 不可能只用自動生成的接口就能完成全部的功能.java

因此, 通常在使用 prisma 服務的時候, 通常都會再封裝一層(能夠稱爲 gateway), 在 gateway 上作認證, 權限等等, 只有合法的請求才會最終轉發到 prisma 服務上. prisma 服務自己能夠導出 client SDK, 用來方便 gateway 的編寫, 目前支持 4 種格式 (javascript, typescript, golang, flow), javascript 和 typescript 的是 client SDK 功能比較全, golang 功能弱一些, flow 沒有嘗試過.mysql

我在使用 golang client SDK 寫 gateway 的時候, 發現 golang 的 graphql server 相關的庫沒有 js/ts 那麼完善. 因而, 就想用反向代理的方式, 攔截前端的 graphql 請求, 作了相應操做以後直接再將請求內容轉發給 prisma 服務. 這種方式不使用 prisma 生成的 client SDK, 也突破語言的限制, 除了 golang, java, C# 等其餘語言也能夠做爲 prisma 的 gatewaygit

反向代理示例(by golang)<a id="sec-3"></a>

採用 golang 的 gin 做爲 gateway 的 web 服務框架. 認證部分使用 gin-jwt 中間件. 反向代理和權限部分沒有使用現成的框架.github

整個 gateway 的示例包含:golang

  1. prisma 服務(prisma + mysql): 這部分有現成的 docker image, 只要配置示例的表和字段便可
  2. gateway (golang gin): golang gin 的 api 服務

prisma 服務<a id="sec-3-1"></a>

  1. prisma.ymlweb

    endpoint: http://${env:PRISMA_HOST}:${env:PRISMA_PORT}/illuminant/${env:PRISMA_STAGE}
    datamodel: datamodel.prisma
    
    secret: ${env:PRISMA_MANAGEMENT_API_SECRET}
    
    generate:
      - generator: go-client
        output: ./
  2. .env正則表達式

    PRISMA_HOST=localhost
    PRISMA_PORT=4466
    PRISMA_STAGE=dev
    PRISMA_MANAGEMENT_API_SECRET=secret-key
  3. datamodel.prismasql

    type User {
      id: ID! @id
      name: String! @unique
      realName: String!
      password: String!
    
      createdAt: DateTime! @createdAt
      updatedAt: DateTime! @updatedAt
    }
  4. docker-compose.yml

    version: '3'
    services:
      illuminant:
        image: prismagraphql/prisma:1.34
        # restart: always
        ports:
        - "4466:4466"
        environment:
          PRISMA_CONFIG: |
            port: 4466
            managementApiSecret: secret-key
            databases:
              default:
                connector: mysql
                host: mysql-db
                user: root
                password: prisma
                # rawAccess: true
                port: 3306
                migrations: true
    
      mysql-db:
        image: mysql:5.7
        # restart: always
        environment:
          MYSQL_ROOT_PASSWORD: prisma
        volumes:
          - mysql:/var/lib/mysql
    volumes:
      mysql: ~

以上文件放在同一個目錄便可, 包含了全部 prisma 服務和 mysql 服務所須要的文件

gateway 服務<a id="sec-3-2"></a>

gateway 服務是關鍵, 也是從此擴展的部分. 採用 golang gin 框架來編寫.

總體流程<a id="sec-3-2-1"></a>

  1. HTTP 請求
  2. route 路由
  3. 認證 Check
  4. 權限 Check
  5. 請求轉發 prisma 服務(這一步通常都是轉發到 prisma, 若是有上傳/下載, 或者統計之類的需求, 須要另外寫 API)
  6. 返回 Response

認證<a id="sec-3-2-2"></a>

authMiddleware := controller.JwtMiddleware()
apiV1 := r.Group("/api/v1")

// no auth routes
apiV1.POST("/login", authMiddleware.LoginHandler)

// auth routes
authRoute := apiV1.Group("/")
authRoute.GET("/refresh_token", authMiddleware.RefreshHandler)
authRoute.Use(authMiddleware.MiddlewareFunc())
{
  // proxy prisma graphql
  authRoute.POST("/graphql", ReverseProxy())
}

/api/v1/graphql 在知足 jwt 認證的狀況下才能夠訪問.

反向代理<a id="sec-3-2-3"></a>

func ReverseProxy() gin.HandlerFunc {

  return func(c *gin.Context) {
    director := func(req *http.Request) {
      req.URL.Scheme = "http"
      req.URL.Host = primsa-host
      req.URL.Path = primsa-endpoint
      delete(req.Header, "Authorization")
      req.Header["Authorization"] = []string{"Bearer " + primsa-token}

    }

    // 解析出 body 中的內容, 進行權限檢查
    body, err := c.GetRawData()
    if err != nil {
      fmt.Println(err)
    }

    // 對 body 進行權限 check
    // 權限 Check, 解析出 graphql 中請求的函數, 而後判斷是否有權限
    // 目前的方式是根據請求中函數的名稱來判斷權限, 也就是隻能對錶的 CURD 權限進行判斷, 對於表中的字段權限還沒法檢查
    // 若是權限檢查沒有經過, 直接返回, 不要再進行下面的請求轉發

    // 將 body 反序列化回請求中, 轉發給 prisma 服務
    c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))

    proxy := &httputil.ReverseProxy{Director: director}
    proxy.ModifyResponse = controller.RewriteBody
    proxy.ServeHTTP(c.Writer, c.Request)
  }
}

權限<a id="sec-3-2-4"></a>

// 檢查權限
func CheckAuthority(body []byte, userId string) bool {
        var bodyJson struct {
                Query string `json:"query"`
        }
        log := logger.GetLogger()
        if err := json.Unmarshal(body, &bodyJson); err != nil {
                log.Error("body convert to json error: %s", err.Error())
                return false
        }

        graphqlFunc := RegrexGraphqlFunc(bodyJson.Query)
        if graphqlFunc == "" {
                return false
        }

        // 這裏的 userId 是從 jwt 中解析出來的, 而後再判斷用戶是否有權限

        if graphqlFunc == "users" {
                return false
        }
        return true
}

// 匹配 graphql 請求的函數
func RegrexGraphqlFunc(graphqlReq string) string {
        graphqlReq = strings.TrimSpace(graphqlReq)
        // reg examples:
        // { users {id} }
        // { users(where: {}) {id} }
        // mutation{ user(data: {}) {id} }
        var regStrs = []string{
                `^\{\s*(\w+)\s*\{.*\}\s*\}$`,
                `^\{\s*(\w+)\s*\(.*\)\s*\{.*\}\s*\}$`,
                `^mutation\s*\{\s*(\w+)\s*\(.*\)\s*\{.*\}\s*\}$`,
        }

        for _, regStr := range regStrs {
                r := regexp.MustCompile(regStr)
                matches := r.FindStringSubmatch(graphqlReq)
                if matches != nil && len(matches) > 1 {
                        return matches[1]
                }
        }

        return ""
}

這裏的權限檢查是個實現思路, 不是最終的代碼. 其中用正則表達式的方式來匹配請求中的函數只是臨時的方案, 不是最好的方式, 最好的方式應該用 golang 對應的 graphql 解析庫來解析出請求的結構, 而後再判斷解析出的函數時候有權限

總結<a id="sec-4"></a>

採用反向代理的方式, 是爲了突破 prisma client SDK 的限制, 若是之後 client SDK 完善以後, 仍是基於 client SDK 來開發 gateway 更加可靠.

相關文章
相關標籤/搜索