zoukankan      html  css  js  c++  java
  • cube.js 多租户的实践

    几个问题

    安全

    • 应该开启checkAuth 处理
      同时基于此进行用户相关资源的配置(部分信息可以放jwt token 中,也可以基于用户配置后边查询)
     
    module.exports = {
      checkAuth: (req, auth) => {
            console.log("authinfo")
            console.log("jwt",auth)
            if (auth) {
                var decoded = jwt.verify(auth, "secret");
                if (decoded) {
                   // 同时可以进行一些扩展操作,比如添加新的token信息
                    req.authInfo = decoded
                }
            }
            else{
                throw new Error(`Unauthorized`);
            }
        },
        ....
    }

    资源配置问题

    主要是关于数据库链接、用户appid、用户schema查找定义,关于appid 信息主要是通过jwt 生成以及包含的
    对于jwt 的信息,cube.js 会自动进行数据的处理,同时包含到请求上下文的authInfo

    • 请求上下文的签名为
     
    RequestContext {
      authInfo: Object,
      requestId: String
    }
    • appid 问题
      appid在支持多租户的模型中有很重要的作用,对于schema 的获取以及schema 的编译处理都是依赖此appid的
      部分参考代码
     
    public getCompilerApi(context: RequestContext) {
        const appId = this.contextToAppId(context);
        let compilerApi = this.compilerCache.get(appId);
        const currentSchemaVersion = this.options.schemaVersion && (() => this.options.schemaVersion(context));
        if (!compilerApi) {
          compilerApi = this.createCompilerApi(
            this.repositoryFactory(context), {
              dbType: (dataSourceContext) => this.contextToDbType({ ...context, ...dataSourceContext }),
              externalDbType: this.contextToExternalDbType(context),
              dialectClass: (dialectContext) => this.options.dialectFactory &&
                this.options.dialectFactory({ ...context, ...dialectContext }),
              externalDialectClass: this.options.externalDialectFactory && this.options.externalDialectFactory(context),
              schemaVersion: currentSchemaVersion,
              preAggregationsSchema: this.preAggregationsSchema(context),
              context,
              allowJsDuplicatePropsInSchema: this.options.allowJsDuplicatePropsInSchema
            }
          );
          this.compilerCache.set(appId, compilerApi);
        }
        compilerApi.schemaVersion = currentSchemaVersion;
        return compilerApi;
      }

    上下文appid 的处理, 可以参考此配置说明,可以基于用户id也可以通过jwt 扩展

    module.exports = {
      contextToAppId: ({ authInfo }) => `CUBEJS_APP_${authInfo.user_id}`,
    };

    注意此处官方的代码是有问题的,通过代码查看官方的用意应该很明确是定时预处理的,
    依赖了一个scheduledRefreshContexts: async () => [null] 的配置,但是对于多租户处理有点问题
    默认使用了一个,默认的一般是不配置contextToAppId,但是如果配置了之后就会依赖,因为没有
    合理的进行数据状态存储,造成此对象的数据为空(也不是,至少有一个[null],问题代码
    @cubejs-backend/server-core/src/core/server.ts (注意是当前版本)

     
    if (scheduledRefreshTimer) {
                this.scheduledRefreshTimerInterval = shared_1.createCancelableInterval(async () => {
                    const contexts = await options.scheduledRefreshContexts();
                    console.log(contexts,"from core server context")
                    if (contexts.length < 1) {
                        this.logger('Refresh Scheduler Error', {
                            error: 'At least one context should be returned by scheduledRefreshContexts'
                        });
                    }
                    await Promise.all(contexts.map(async (context) => {
                        const queryingOptions = { concurrency: options.scheduledRefreshConcurrency };
                        if (options.scheduledRefreshTimeZones) {
                            queryingOptions.timezones = options.scheduledRefreshTimeZones;
                        }
                        await this.runScheduledRefresh(context, queryingOptions);
                    }));
                }, {
                    interval: scheduledRefreshTimer,
                    onDuplicatedExecution: (intervalId) => this.logger('Refresh Scheduler Interval Error', {
                        error: `Previous interval #${intervalId} was not finished with ${scheduledRefreshTimer} interval`
                    }),
                    onDuplicatedStateResolved: (intervalId, elapsed) => this.logger('Refresh Scheduler Long Execution', {
                        warning: `Interval #${intervalId} finished after ${shared_1.formatDuration(elapsed)}`
                    })
                });
            }

    解决方法

    1. 固定配置几个预定的参数,关于多租户appid 以及schema 关联信息的
    2. 基于API 进行数据的统一管理
    3. 手工进行runScheduledRefresh 的处理
     

    固定模式参考

    module.exports = {
        devServer: false,
        dbType: ({ dataSource } = {}) => {
            return 'postgres';
        },
        contextToAppId: ({ authInfo }) => {
            console.log("contextToAppId:", authInfo)
            return `CUBEJS_APP_${authInfo.myappid}`
        },
        contextToOrchestratorId: ({ authInfo }) => {
            console.log("contextToOrchestratorId:", authInfo)
            return `CUBEJS_APP_${authInfo.myappid}`
        },
        scheduledRefreshContexts: async () => {
          // 固定的几个app信息,同时关联了schema 信息(基于u,可以自己扩展)
            return [{
                authInfo: {
                    myappid: "demoappid",
                    u:{
                        bucket: "demo"
                    }
                }
            }]
        },
        preAggregationsSchema: ({ authInfo }) => `pre_aggregations_${authInfo.myappid}`,
    • 一个参考实现
      jwt token demo (注意jwt payload 的格式很重要)
     
    const jwt = require('jsonwebtoken');
    const CUBE_API_SECRET = 'b2db7688e328d316d85e924d8b9a0737d87162a9f2cf36325f1ca0ae08dbdaa990520750847226cf8dcbb1fb4c07afe1087c7cb03b8f9f05b9abad3eb4058f3f';
    const cubejsToken = jwt.sign({ u: { user_id: 42 ,bucket:"demo"},myappid:"demoappid" }, CUBE_API_SECRET, {
      expiresIn: '30d',
    });
    const cubejsToken2 = jwt.sign({ u: { user_id: 43 ,bucket:"demo2"},myappid:"demoappid2" }, CUBE_API_SECRET, {
        expiresIn: '30d',
      });
    console.log(cubejsToken)
    console.log(cubejsToken2)

    cube.js 配置(基于appid支持多租户)

    // Cube.js configuration options: https://cube.dev/docs/config
    const PostgresDriver = require("@cubejs-backend/postgres-driver");
    const CubeStoreDriver = require("@cubejs-backend/cubestore-driver")
    const myS3FileRepository = require("@dalongrong/cube-s3repository")
    const fetch = require('node-fetch');
    const jwt = require('jsonwebtoken');
    module.exports = {
        devServer: false,
        dbType: ({ dataSource } = {}) => {
            return 'postgres';
        },
        // 基于token 获取的数据,请求header Authorization
        contextToAppId: ({ authInfo }) => {
            return `CUBEJS_APP_${authInfo.myappid}`
        },
        // 基于token 获取的数据,请求header Authorization
        contextToOrchestratorId: ({ authInfo }) => {
            return `CUBEJS_APP_${authInfo.myappid}`
        },
       //  此处解决官方的bug 问题,造成scheuder 数据异常
        scheduledRefreshContexts: async () => {
            return [{
                authInfo: {
                    myappid: "demoappid",
                    u: {
                        bucket: "demo"
                    }
                }
            },
            {
                authInfo: {
                    myappid: "demoappid2",
                    u: {
                        bucket: "demo2"
                    }
                }
            }]
        },
        preAggregationsSchema: ({ authInfo }) => {
            return `pre_aggregations_${authInfo.myappid}`
        },
        checkAuth: (req, auth) => {
            console.log("checkAuth=====",auth)
            if (auth) {
                var decoded = jwt.verify(auth, "b2db7688e328d316d85e924d8b9a0737d87162a9f2cf36325f1ca0ae08dbdaa990520750847226cf8dcbb1fb4c07afe1087c7cb03b8f9f05b9abad3eb4058f3f");
                if (decoded) {
                    req.authInfo = decoded
                }
            }
            else {
                throw new Error(`Unauthorized`);
            }
        },
        telemetry: false,
        apiSecret: "b2db7688e328d316d85e924d8b9a0737d87162a9f2cf36325f1ca0ae08dbdaa990520750847226cf8dcbb1fb4c07afe1087c7cb03b8f9f05b9abad3eb4058f3f",
        driverFactory: ({ dataSource } = {}) => {
            return new PostgresDriver({
                user: "postgres",
                database: "postgres",
                password: "dalong",
                port: 5432,
                host: "127.0.0.1",
                readOnly: true
            });
        },
        repositoryFactory: ({ authInfo }) => {
           // 不同租户使用不同的s3 bucket
            console.log("repositoryFactory=====",authInfo);
            if (authInfo && authInfo.u.bucket) {
                return new myS3FileRepository.S3FileRepository(authInfo.u.bucket)
            }
            return new myS3FileRepository.S3FileRepository()
        },
        externalDbType: 'cubestore',
        externalDriverFactory: () => new CubeStoreDriver({
            user: "root",
            port: 3306,
            host: "127.0.0.1"
        })
    };

    其他模式的多租户数据处理

    • 相同数据库&&不同schema
      可以基于USER_CONTEXT 进行扩展,同时支持row级别的数据访问控制
     
    cube(`Products`, {
      sql: `select * from products where ${USER_CONTEXT.categoryId.filter('categoryId')}`
    })

    COMPILE_CONTEXT 可以支持不同的数据库模式

    const { authInfo: { tenantId } } = COMPILE_CONTEXT;
    cube(`Products`, {
      sql: `select * from ${tenantId}.products`
    })
    • 不同数据库的支持
      这个我们可以基于appid 模式解决,使用不同的数据库驱动

    说明

    目前的多租户在数据预聚合调度上是点问题的,因为内部调度处理依赖了scheduledRefreshContexts,但是代码处理有异常

    参考资料

    https://cube.dev/docs/multitenancy-setup
    https://cube.dev/docs/cube#context-variables-user-context
    https://cube.dev/docs/security
    https://cube.dev/docs/deployment/production-checklist

  • 相关阅读:
    ajax专题
    luogu P1346 电车 最短路
    luogu P1462 通往奥格瑞玛的道路 最短路
    luogu P1328 生活大爆炸版石头剪刀布
    luogu P1315 联合权值 枚举
    luogu P1156 垃圾陷阱 背包问题
    luogu P1217 回文质数 枚举
    luogu P3650 滑雪课程设计 枚举
    luogu1209 修理牛棚 贪心
    luogu P1223 排队接水 贪心
  • 原文地址:https://www.cnblogs.com/rongfengliang/p/14336290.html
Copyright © 2011-2022 走看看