java - Firebase Multi-Tenancy 与 Play 框架取决于 HTTP 请求的 header 值

标签 java playframework guice multi-tenant

我有一个 Play 框架项目,提供在多个前端之间共享的 API,目前,我正在开发单个前端,但我想创建一个 Multi-Tenancy 后端,每个前端都有自己的 Firebase帐户。

我的问题是,我必须考虑访问哪个 Firebase 项目取决于请求 header 值,而不同的值取决于前端。

我现在拥有的: FirebaseAppProvider.java:

public class FirebaseAppProvider implements Provider<FirebaseApp> {


    private final Logger.ALogger logger;
    private final Environment environment;
    private final Configuration configuration;

    @Inject
    public FirebaseAppProvider(Environment environment, Configuration configuration) {
        this.logger = Logger.of(this.getClass());
        this.environment = environment;
        this.configuration = configuration;
    }

    @Singleton
    @Override
    public FirebaseApp get() {
        HashMap<String, String> firebaseProjects = (HashMap<String, String>) configuration.getObject("firebase");
        firebaseProjects.forEach((websiteId, projectId) -> {
            FileInputStream serviceAccount = null;
            try {
                serviceAccount = new FileInputStream(environment.classLoader().getResource(String.format("firebase/%s.json", projectId)).getPath());
            } catch (FileNotFoundException e) {
                e.printStackTrace();
                return;
            }

            FirebaseOptions options = new FirebaseOptions.Builder().setCredential(FirebaseCredentials.fromCertificate(serviceAccount))
                    .setDatabaseUrl(String.format("https://%s.firebaseio.com/", projectId))
                    .build();


            FirebaseApp firebaseApp = FirebaseApp.initializeApp(options, projectId);

            logger.info("FirebaseApp initialized");
        });

        return FirebaseApp.getInstance();
    }
}

同样适用于数据库: FirebaseDatabaseProvider.java

public class FirebaseDatabaseProvider implements Provider<FirebaseDatabase> {

    private final FirebaseApp firebaseApp;
    public static List<TaxItem> TAXES = new ArrayList<>();

    @Inject
    public FirebaseDatabaseProvider(FirebaseApp firebaseApp) {
        this.firebaseApp = firebaseApp;
        fetchTaxes();
    }

    @Singleton
    @Override
    public FirebaseDatabase get() {
        return FirebaseDatabase.getInstance(firebaseApp);
    }

    @Singleton
    public DatabaseReference getUserDataReference() {
        return this.get().getReference("/usersData");
    }

    @Singleton
    public DatabaseReference getTaxesConfigurationReference() {
        return this.get().getReference("/appData/taxConfiguration");
    }
    private void fetchTaxes() {
        DatabaseReference bundlesRef = getTaxesConfigurationReference().child("taxes");
        bundlesRef.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                TAXES.clear();
                dataSnapshot.getChildren().forEach(tax -> TAXES.add(tax.getValue(TaxItem.class)));
                Logger.info(String.format("==> %d taxes records loaded", TAXES.size()));
            }

            @Override
            public void onCancelled(DatabaseError databaseError) {
                Logger.warn("The read failed: " + databaseError.getCode());
            }
        });
    }
}

所以我也从 Module.java 绑定(bind)它们:

public class Module extends AbstractModule {

    @Override
    public void configure() {        bind(FirebaseApp.class).toProvider(FirebaseAppProvider.class).asEagerSingleton();
        bind(FirebaseAuth.class).toProvider(FirebaseAuthProvider.class).asEagerSingleton();
        bind(FirebaseDatabase.class).toProvider(FirebaseDatabaseProvider.class).asEagerSingleton();
    }

}

我的 ActionCreator:

public class ActionCreator implements play.http.ActionCreator {

    @Inject
    public ActionCreator() {
    }

    @Override
    public Action createAction(Http.Request request, Method actionMethod) {
        switchTenancyId(request);
        return new Action.Simple() {
            @Override
            public CompletionStage<Result> call(Http.Context ctx) {
                return delegate.call(ctx);
            }
        };
    }

    private void switchTenancyId(Http.RequestHeader request) {
        // DO something here
    }

    private Optional<String> getTenancyId(Http.RequestHeader request) {
        String websiteId = request.getHeader("Website-ID");
        System.out.println(websiteId);
        return null;
    }
}

我想要的是当我使用数据库服务或身份验证服务时,我读取网站 ID 并决定访问哪个 firebase 项目,我真的尝试了像这里答案这样的解决方案: Multi tenancy with Guice Custom Scopes and Jersey

请注意,我愿意为每个前端使用不同的项目,而不是相同的 Firebase 项目。

但是有点迷失,特别是请求只能从 Controller 或 ActionCreator 访问,所以我从上面的问题中得到的是通过键将提供程序加载到 ThreadLocal 中,并为每个请求切换它们取决于注释,但由于缺乏知识,我无法做到这一点。


我的项目的最小化版本可以在这里找到:https://github.com/almothafar/play-with-multi-tenant-firebase

此外,我还上传了 taxes-data-export.json 文件以导入到 firebase 项目中进行测试。

最佳答案

是的,所以我知道 Play 比 FireBase 好得多,但在我看来,您想在将其输入 FrieBase 后端之前从请求中提取租户 ID 吗?在运行中编写 Java 时,上下文是线程本地的,但即使在异步执行操作时,您也可以通过注入(inject)执行上下文来确保 Http.context 信息顺利进行。我不会通过操作创建者执行此操作,除非您想拦截调用哪个操作。 (尽管我也有一个黑客解决方案。)

因此,在发表评论后,我将尝试在这里阐明,您传入的请求将被路由到 Controller ,如下所示(如果您需要清理路由等,请告诉我):

下面是一个基于从请求中检索到的“Website-ID”来缓存检索到的 FireBaseApp 的解决方案,尽管我可能会将 tenancyId 放入 session 中。

import javax.inject.Inject;
import java.util.concurrent.CompletionStage;

public class MyController extends Controller {
    private HttpExecutionContext ec; //This is the execution-context.
    private FirebaseAppProvider appProvider;
    private CacheApi cache;
    @Inject
    public MyController(HttpExecutionContext ec, FireBaseAppProvider provider,CacheApi cache) {
        this.ec = ec;
        this.appProvider = provider;
        this.cache = cache;

    }
    /**
    *Retrieves a website-id from request and attempts to retrieve 
    *FireBaseApp object from Cache.
    *If not found a new FireBaseApp will be constructed by 
    *FireBaseAppProvider and cached.
    **/
    private FireBaseApp getFireBaseApp(){
     String tenancyId = request.getHeader("Website-ID);
     FireBaseApp app = (FireBaseApp)cache.get(tenancyId);
     if(app==null){
       app=appProvider.get();
       cache.put(tenancyId,app);
       }
     return app;
    }    
    public CompletionStage<Result> index() {
        return CompletableFuture.supplyAsync(() -> {
           FireBaseApp app = getFireBaseApp();
           //Do things with app.
        }, ec.current()); //Used here.
    }
}

现在,在 FireBaseAppProvider 中,您可以通过 play.mvc.Controller 访问 header ,您唯一需要的就是通过 ec.current 提供 HttpExecutionContext。因此(我再次避免任何 FireBase 特定的内容),在 FireBaseProvider 中:

import play.mvc.Controller;
public class FireBaseAppProvider {


public  String getWebsiteKey(){
        String website = Controller.request().getHeader("Website-ID");
        //If you need to handle a missing header etc, do it here.
        return website;
    }

 public FireBaseApp get(){
     String tenancyId = getWebsiteKey();
     //Code to do actual construction here. 
 }
}

请告诉我这是否接近您所要求的内容,我会为您解决。

此外,如果您想存储 token 验证等,最好将它们放在返回请求的“ session ”中,这由 Play Framework 签名并允许通过请求存储数据。对于更大的数据,您可以使用 session ID 作为 key 的一部分来缓存它。

关于java - Firebase Multi-Tenancy 与 Play 框架取决于 HTTP 请求的 header 值,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44647753/

相关文章:

java - HashMap 和可见性

java - 如何在 hibernate 中从父表检索子表记录?

java - Tomcat有几个request不用EJBs,Thread?

inheritance - Java & Guice - 如何处理继承和抽象?

java - Hibernate 查询语言中的 SQL 子查询

java - 从 int[] 到图像 png

scala - 如何展平数组[Future[Seq[T]]]

scala - sbt - scala 项目 dockerization 中的问题

java - 如何在 play-java 2.5.4 中配置电子邮件功能

java - 如何使用 Guice 注入(inject) SLF4J 绑定(bind)?