四郎的多租户 [英] Multi tenancy in Shiro

查看:98
本文介绍了四郎的多租户的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我们正在评估Shiro为其构建的自定义Saas应用程序.好像一个好的框架确实可以完成我们想要的90%的需求.我对Shiro的理解是基本的,这就是我想要实现的目标.

We are evaluating Shiro for a custom Saas app that we are building. Seems like a great framework does does 90% of what we want, out of the box. My understanding of Shiro is basic, and here is what I am trying to accomplish.

  • 我们有多个客户端,每个客户端都有一个相同的数据库
  • 所有授权(角色/权限)将由客户端配置 在他们自己的专用数据库中
  • 每个客户都有一个独特的 虚拟主机,例如client1.mycompany.com,client2.mycompany.com等
  • We have multiple clients, each with an identical database
  • All authorization (Roles/Permissions) will be configured by the clients within their own dedicated database
  • Each client will have a unique Virtual host eg. client1.mycompany.com, client2.mycompany.com etc

场景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使用哪个数据库.

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 (由于LDAP,用户名在客户端之间是唯一的)如果用户jdoe 由client1和client2共享,并且通过client1进行了身份验证 并尝试访问client2的资源,Shiro会允许还是拥有 他再次登录?

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 (仅在数据库中唯一的用户名),如果客户端1和客户端 客户端2创建一个名为jdoe的用户,那么Shiro就能 区分客户端1中的jdoe和客户端2中的jdoe吗?

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领域

Modify my inherited 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);
    }

最后在登录时使用新令牌

And finally use the new token when logging in

// 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,它位于所有请求的前面,并解析与该请求有关的tenantId.您可以将已解析的tenantId存储为请求属性或threadlocal,以便在请求持续时间内的任何地方都可以使用.

You will probably need a ServletFilter that sits in front of all requests and resolves a tenantId pertaining to the request. You can store that resolved tenantId as a request attribute or a threadlocal so it is available anywhere for the duration of the request.

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

The next step is to probably create a sub-interface of AuthenticationToken, e.g. TenantAuthenticationToken that has a method: getTenantId(), which is populated by your request attribute or threadlocal. (e.g. getTenantId() == 'client1' or 'client2', etc).

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

Then, your Realm implementations can inspect the Token and in their supports(AuthenticationToken) implementation, and return true only if the token is a TenantAuthenticationToken instance and the Realm is communicating with the datastore for that particular tenant.

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

This implies one realm per client database. Beware though - if you do this in a cluster, and any cluster node can perform an authentication request, every client node will need to be able to connect to every client database. The same would be true for authorization if authorization data (roles, groups, permissions, etc) is also partitioned across databases.

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

Depending on your environment, this might not scale well depending on the number of clients - you'll need to judge accordingly.

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

As for JNDI resources, yes, you can reference them in Shiro INI via Shiro's JndiObjectFactory:

[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中声明的那样.

The factory will look up the datasource and make it available to other beans as if it were declared in the INI directly.

这篇关于四郎的多租户的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆