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

posted on 2021-01-27 18:09  荣锋亮  阅读(282)  评论(0编辑  收藏  举报

导航