Shiro中的多租户

8
我们正在评估Shiro框架,以用于我们正在构建的定制SaaS应用程序。它似乎是一个很棒的框架,可以直接实现我们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..

Scenario 2

Authentication also done via JDBC Relam in their database

问题:

适用于Sc 1和2 我应该如何告诉Shiro使用哪个数据库?我意识到这必须通过某种自定义的身份验证过滤器完成,但是有人可以指导我最合理的方法吗?计划使用虚拟主机URL来告诉Shiro和mybatis使用哪个数据库。

我需要为每个客户端创建一个领域吗?

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

Sc 2(用户名仅在数据库中唯一)如果客户端1和客户端2都创建名为jdoe的用户,则Shiro能够区分客户端1中的jdoe和客户端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 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);
    }

最后,在登录时使用新的令牌

// 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中重新定义它们吗? - aks
1个回答

10
您可能需要一个ServletFilter,它位于所有请求的前面并解析与请求相关的tenantId。您可以将已解析的tenantId存储为请求属性或线程本地变量,以便在请求期间的任何位置都可用。
下一步,可能需要创建AuthenticationToken的子接口,例如TenantAuthenticationToken,并添加一个方法getTenantId(),该方法通过您的请求属性或线程本地变量填充。(例如getTenantId()== 'client1' or 'client2'等)。
然后,Realm实现可以检查Token并在其supports(AuthenticationToken)实现中仅在token是TenantAuthenticationToken实例且Realm正在与该特定租户的数据存储进行通信时返回true。
这意味着每个客户端数据库有一个Realm实现。但要注意 - 如果您在集群中执行此操作,并且任何集群节点都可以执行身份验证请求,则每个客户端节点都需要能够连接到每个客户端数据库。如果授权数据(角色、组、权限等)也跨数据库分区,则情况也是如此。
根据您的环境,这可能不适用于大量客户端 - 您需要进行相应的判断。
至于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
@aks 很高兴能帮忙!你可以采纳答案吗? - Les Hazlewood
仍然是Stack Overflow的新手,所以没有意识到有这样一个概念...再次感谢您的及时、清晰和有用的回复。 - aks

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接