【问题标题】:Firebase multi-tenancy with play framework depends on header value of HTTP request带有播放框架的 Firebase 多租户取决于 HTTP 请求的标头值
【发布时间】:2017-11-22 16:25:48
【问题描述】:

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

我必须考虑访问哪个 firebase 项目的问题取决于请求标头值,而不同的值则取决于前端。

我现在拥有的: 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 绑定它们:

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 项目。

但是有点丢了,尤其是请求只能从控制器或 ActionCreator 访问,所以我从上面的问题中得到的是通过密钥将提供程序加载到 ThreadLocal 并为每个请求切换它们取决于注释,但我是由于缺乏知识而无法做到这一点。


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

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

【问题讨论】:

    标签: java playframework guice multi-tenant


    【解决方案1】:

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

    因此,在发表评论后,我将尝试在此处进行说明,您的传入请求将被路由到控制器,如下所示(如果您需要清除路由等,请告诉我):

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

    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 访问标头,您唯一需要的是通过 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. 
     }
    }
    

    如果这与您的要求接近,请告诉我,我会为您清理。

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

    【讨论】:

    • 谢谢你的回答,其实这是一个来自API的正常请求,而不是WS,而且我使用的是firebase SDK,这就是为什么我在Github上提供了一个迷你项目,我的意思是在这里,如果我从前端发送“Website-ID”标头,我必须阅读此标头并基于此使用 Firebase 初始化应用程序。
    • 是的,这将解决这个问题。让我为你清理一下,它应该更清楚。我会省略 FireBase 代码,因为我可能会搞砸。
    • 但我不明白你为什么从前端发送 URL,而且我不能以你的方式使用 Android SDK,我必须更新 Firebase 数据库,并验证令牌。
    • 对,所以看看你的项目,我想我知道你想要什么。
    【解决方案2】:

    我认为为此使用自定义范围有点过头了。我建议从Guice's own wiki 进行请求范围播种。在你的情况下,就像

    public class TenancyFilter implements Filter {
        @Override
        public void doFilter(ServletRequest request,  ServletResponse response, FilterChain chain) throws IOException, ServletException {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            String tenancyId = httpRequest.getHeader("YOUR-TENANCY-ID-HEADER-NAME");
            httpRequest.setAttribute(
                    Key.get(String.class, Names.named("tenancyId")).toString(),
                    userId
            );
            chain.doFilter(request, response);
        }
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException { }
    
        @Override
        public void destroy() { }
    };
    

    它必须绑定在ServletModule

    public class YourModule extends ServletModule {
        @Override
        protected void configureServlets() {
            filter("/*").through(TenancyFilter.class);
        }
    
        @Provides
        @RequestScoped
        @Named("tenancyId")
        String provideTenancyId() {
            throw new IllegalStateException("user id must be manually seeded");
        }
    }
    

    然后在任何需要获取您刚刚注入的租户 ID 的地方

    public class SomeClass {
        private final Provider<String> tenancyIdProvider;
    
        @Inject
        SomeClass(@Named("tenancyId") Provider<String> tenancyIdProvider) {
            this.tenancyIdProvider = tenancyIdProvider;
        }
    
        // Methods in request call tenancyIdProvider.get() to get and take action based on Tenancy ID.
    }
    

    【讨论】:

    • 感谢回复,我这里用的是play框架,所以有@RequestScoped我的问题,我在play框架上发现这个问题有点困难。
    猜你喜欢
    • 2014-12-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-03
    • 1970-01-01
    • 2016-11-13
    • 1970-01-01
    相关资源
    最近更新 更多