Shiro的多租户

aks*_*aks 8 multi-tenant shiro

我们正在评估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..
Run Code Online (Sandbox Code Playgroud)

情景2

Authentication also done via JDBC Relam in their database
Run Code Online (Sandbox Code Playgroud)

问题:

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

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

新型令牌..

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;
        }
    }
Run Code Online (Sandbox Code Playgroud)

修改我继承的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);
    }
Run Code Online (Sandbox Code Playgroud)

最后在登录时使用新令牌

// 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();
            }
        }

}
Run Code Online (Sandbox Code Playgroud)

Les*_*ood 9

您可能需要一个位于所有请求前面的ServletFilter,并解析与请求有关的tenantId.您可以将已解析的tenantId存储为请求属性或threadlocal,以便在请求期间的任何位置都可以使用它.

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

然后,您的Realm实现可以检查令牌及其supports(AuthenticationToken)实现,并true仅在令牌是TenantAuthenticationToken实例并且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
Run Code Online (Sandbox Code Playgroud)

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