prisma反向代理

概要

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

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

爲何要作 prisma 的反向代理

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)

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

整個 gateway 的示例包含:golang

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

prisma 服務

  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 服務

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

總體流程

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

認證

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 認證的狀況下才能夠訪問.

反向代理

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)
  }
}

權限

// 檢查權限
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 解析庫來解析出請求的結構, 而後再判斷解析出的函數時候有權限

總結

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

相關文章
相關標籤/搜索