Shiro 的多租户

Posted

技术标签:

【中文标题】Shiro 的多租户【英文标题】:Multi tenancy in Shiro 【发布时间】:2012-03-05 07:51:15 【问题描述】:

我们正在为我们正在构建的自定义 Saas 应用评估 Shiro。似乎一个伟大的框架可以完成我们想要的 90% 的工作,开箱即用。我对 Shiro 的理解是基本的,这就是我想要完成的。

我们有多个客户,每个客户都有一个相同的数据库 所有授权(角色/权限)将由客户端配置 在他们自己的专用数据库中 每个客户端都有一个唯一的 虚拟主机例如。 client1.mycompany.com、client2.mycompany.com 等

场景 1

Authentication done via LDAP (MS Active Directory)
Create unique users in LDAP, make app aware of LDAP users, and have client admins provision them into whatever roles..

场景 2

Authentication also done via JDBC Relam in their database

问题:

Sc 1 & 2 通用 我如何告诉 Shiro 使用哪个数据库?一世 意识到必须通过某种自定义身份验证来完成 过滤器,但是有人可以指导我采用最合乎逻辑的方式吗?计划使用 用于告诉 shiro 和 mybatis 使用哪个 DB 的虚拟主机 url。

我是否为每个客户端创建一个领域?

Sc 1(由于 LDAP,用户名在客户端之间是唯一的)如果用户 jdoe 由client1和client2共享,通过client1认证 并尝试访问 client2 的资源,Shiro 是否允许或有 他又登录了?

Sc 2(用户名仅在数据库中唯一)如果客户端 1 和 客户端2创建一个名为jdoe的用户,然后Shiro就可以 区分Client 1中的jdoe和Client 2中的jdoe?

我的解决方案基于 Les 的意见..

public class MultiTenantAuthenticator extends ModularRealmAuthenticator 

    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException 
        assertRealmsConfigured();
        TenantAuthenticationToken tat = null;
        Realm tenantRealm = null;

        if (!(authenticationToken instanceof TenantAuthenticationToken)) 
            throw new AuthenticationException("Unrecognized token , not a typeof TenantAuthenticationToken ");
         else 
            tat = (TenantAuthenticationToken) authenticationToken;
            tenantRealm = lookupRealm(tat.getTenantId());
        

        return doSingleRealmAuthentication(tenantRealm, tat);

    

    protected Realm lookupRealm(String clientId) throws AuthenticationException 
        Collection<Realm> realms = getRealms();
        for (Realm realm : realms) 
            if (realm.getName().equalsIgnoreCase(clientId)) 
                return realm;
            
        
        throw new AuthenticationException("No realm configured for Client " + clientId);
    

新类型的令牌..

public final class TenantAuthenticationToken extends UsernamePasswordToken 

       public enum TENANT_LIST 

            CLIENT1, CLIENT2, CLIENT3 
        
        private String tenantId = null;

        public TenantAuthenticationToken(final String username, final char[] password, String tenantId) 
            setUsername(username);
            setPassword(password);
            setTenantId(tenantId);
        

        public TenantAuthenticationToken(final String username, final String password, String tenantId) 
            setUsername(username);
            setPassword(password != null ? password.toCharArray() : null);
            setTenantId(tenantId);
        

        public String getTenantId() 
            return tenantId;
        

        public void setTenantId(String tenantId) 
            try 
                TENANT_LIST.valueOf(tenantId);
             catch (IllegalArgumentException ae) 
                throw new UnknownTenantException("Tenant " + tenantId + " is not configured " + ae.getMessage());
            
            this.tenantId = tenantId;
        
    

修改我继承的 JDBC 领域

public class TenantSaltedJdbcRealm extends JdbcRealm 

    public TenantSaltedJdbcRealm() 
        // Cant seem to set this via beanutils/shiro.ini
        this.saltStyle = SaltStyle.COLUMN;
    

    @Override
    public boolean supports(AuthenticationToken token) 
        return super.supports(token) && (token instanceof TenantAuthenticationToken);
    

最后在登录时使用新令牌

// This value is set via an Intercepting Servlet Filter
String client = (String)request.getAttribute("TENANT_ID");

        if (!currentUser.isAuthenticated()) 
            TenantAuthenticationToken token = new TenantAuthenticationToken(user,pwd,client);
            token.setRememberMe(true);
            try 
                currentUser.login(token);
             catch (UnknownAccountException uae) 
                log.info("There is no user with username of " + token.getPrincipal());
             catch (IncorrectCredentialsException ice) 
                log.info("Password for account " + token.getPrincipal() + " was incorrect!");
             catch (LockedAccountException lae) 
                log.info("The account for username " + token.getPrincipal() + " is locked.  "
                        + "Please contact your administrator to unlock it.");
             // ... catch more exceptions here (maybe custom ones specific to your application?
            catch (AuthenticationException ae) 
                //unexpected condition?  error?
                ae.printStackTrace();
            
        


【问题讨论】:

如果我在 Tomcat 中配置 JDBC 资源,我可以让 shiro 获取它而不是在 shiro.ini 中重新定义它们吗? 【参考方案1】:

您可能需要一个 ServletFilter,它位于所有请求的前面并解析与请求相关的租户 ID。您可以将解析后的tenantId 存储为请求属性或线程本地,以便在请求期间任何地方都可用。

下一步可能是创建一个 AuthenticationToken 的子接口,例如TenantAuthenticationToken 有一个方法:getTenantId(),它由您的请求属性或线程本地填充。 (例如 getTenantId() == 'client1' 或 'client2' 等)。

然后,您的 Realm 实现可以检查令牌及其 supports(AuthenticationToken) 实现,并且仅当令牌是 TenantAuthenticationToken 实例并且 Realm 正在与该特定租户的数据存储进行通信时才返回 true

这意味着每个客户端数据库有一个领域。但请注意 - 如果您在集群中执行此操作,并且任何集群节点都可以执行身份验证请求,则 每个 客户端节点将需要能够连接到 每个 客户端数据库。如果授权数据(角色、组、权限等)也跨数据库分区,则授权也是如此。

根据您的环境,这可能无法很好地扩展,具体取决于客户端的数量 - 您需要做出相应的判断。

至于JNDI资源,是的,你可以通过Shiro的JndiObjectFactory在Shiro INI中引用它们:

[main]
datasource = org.apache.shiro.jndi.JndiObjectFactory
datasource.resourceName = jdbc/mydatasource
# if the JNDI name is prefixed with java:comp/env (like a Java EE environment),
# uncomment this line:
#datasource.resourceRef = true

jdbcRealm = com.foo.my.JdbcRealm
jdbcRealm.datasource = $datasource

工厂将查找数据源并将其提供给其他 bean,就像它直接在 INI 中声明一样。

【讨论】:

感谢一百万 @Les Hazelwood。 @aks 很高兴为您提供帮助!你能奖励答案吗? 对 SO 还是新手,所以没有意识到有这样的概念.. 再次感谢您及时、清晰和有用的回复。

以上是关于Shiro 的多租户的主要内容,如果未能解决你的问题,请参考以下文章

休眠中的多租户数据库

使用 .NET 和 IIS 的多租户 SaaS

EEPlat PaaS中的多租户数据隔离模式

使用 spring-data-elasticsearch 的多租户

带有 sequelize 和 nest.js 的多租户

基于RBAC的模式的多租户权限设计