multi-tenant - Shiro 的 Multi-Tenancy

标签 multi-tenant shiro

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

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


  • 场景一
    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..
    

    场景二
    Authentication also done via JDBC Relam in their database
    

    Questions:

    Common to Sc 1 & 2 How can I tell Shiro which database to use? I realize it has to be done via some sort of custom authentication filter, but can someone guide me to the most logical way ? Plan to use the virtual host url to tell shiro and mybatis which DB to use.

    Do I create one realm per client?

    Sc 1 (User names are unique across clients due to LDAP) If user jdoe is shared by client1 and client2, and he is authenticated via client1 and tries to access a resource of client2, will Shiro permit or have him login again?

    Sc 2 (User names unique within database only) If both client 1 and client 2 create a user called jdoe, then will Shiro be able to distinguish between jdoe in Client 1 and jdoe in Client 2 ?



    我的解决方案基于 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 Realm
    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);
        }
    

    最后在登录时使用新 token
    // 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();
                }
            }
    
    }
    

    最佳答案

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

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

    然后,您的 Realm 实现可以检查 Token 和它们的 supports(AuthenticationToken)执行,并返回 true仅当 token 是 TenantAuthenticationToken实例和领域正在与该特定租户的数据存储进行通信。

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

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

    至于 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 中声明一样。

    关于multi-tenant - Shiro 的 Multi-Tenancy ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/9268523/

    相关文章:

    java - 如何使用 Shiro 的 Salted AuthenticationInfo?

    java - Apache Shiro 中的用户管理

    angularjs - Mongoose 和 Node.js Multi-Tenancy 架构

    java - Shiro-Guice 过滤器链角色配置

    django - 属性错误 : 'DatabaseWrapper' object has no attribute 'set_schema_to_public' (tenat_schemas)

    azure - 获取 Multi-Tenancy 应用程序中访客/外部用户的 appRoles

    java - 将 Shiro Guice 与 jdbcRealm 结合使用

    java - Shiro 自定义 JDBC 领域

    azure - 使用 OWIN Pipeline 进行 Multi-Tenancy 身份验证

    java - tomcat 中同一应用程序的多个 Url 以测试 Multi-Tenancy