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 更加可靠.

相关文章
相关标签/搜索