• 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

  • 相关阅读:
    js 判断用户是否联网
    vue cli 2.9.6 低版本安装失败
    'webpack-dev-server' 不是内部或外部命令,也不是可运行 的程序 或批处理文件。
    Jenkins创建运行用例
    python的类变量和成员变量
    Airtest移动端自动化测试环境搭建 一
    pytest使用总结笔记
    Python单元测试框架之pytest---如何执行测试用例
    UI自动化之分层思想pom模式
    【Fiddler篇】抓包工具之Filters(过滤器)进行会话过滤
  • 原文地址:https://www.cnblogs.com/rongfengliang/p/14336290.html
Copyright © 2020-2023  润新知