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..
场景 2
Authentication also done via JDBC Relam in their database
问题:
与 Sc 1 和 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();
}
}
}
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.
- 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
Scenario 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
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 ?
My Solution based on Les's input..
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);
}
}
New Type of token..
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;
}
}
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();
}
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
您可能需要一个位于所有请求前面的 ServletFilter 并解析与该请求相关的tenantId。您可以将解析后的tenantId存储为请求属性或线程本地,以便在请求期间任何地方都可以使用它。
下一步可能是创建 AuthenticationToken 的子接口,例如具有方法
getTenantId()
的TenantAuthenticationToken
,该方法由您的请求属性或 threadlocal 填充。 (例如 getTenantId() == 'client1' 或 'client2' 等)。然后,您的 Realm 实现可以检查令牌及其
supports(AuthenticationToken)
实现,并且仅当令牌是TenantAuthenticationToken
实例时才返回true
并且 Realm 正在与该特定租户的数据存储进行通信。这意味着每个客户端数据库有一个领域。但请注意 - 如果您在集群中执行此操作,并且任何集群节点都可以执行身份验证请求,则每个客户端节点都需要能够连接到每个客户端数据库。如果授权数据(角色、组、权限等)也跨数据库分区,则授权也是如此。
根据您的环境,这可能无法根据客户端数量很好地扩展 - 您需要做出相应的判断。
至于 JNDI 资源,是的,您可以通过 Shiro 的 JndiObjectFactory 在 Shiro INI 中引用它们:
工厂将查找数据源并将其提供给其他 bean,就像直接在 INI 中声明一样。
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.
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).Then, your Realm implementations can inspect the Token and in their
supports(AuthenticationToken)
implementation, and returntrue
only if the token is aTenantAuthenticationToken
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.
As for JNDI resources, yes, you can reference them in Shiro INI via Shiro's JndiObjectFactory:
The factory will look up the datasource and make it available to other beans as if it were declared in the INI directly.