Apache Shiro Realm 原理和自定义

前言

在 shiro 框架的文章中,了解到Realm是用作获取用户身份信息和权限信息的。shiro 将这块分离出来,使得我们可以自定义,以适应不同的场景。比如用户的信息可以存储数据库中,也在外面添加了一层缓存,我们可以实现自己的Realm,完成缓存和数据库的读取操作。

Realm 接口

先来看看Realm接口的定义,它的方法只有三个,涉及到了AuthenticationTokenAuthenticationInfo两个概念。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public interface Realm {
    // 返回唯一名称,作为该Realm的标识
    String getName();
    
    // 判断是否支持此类认证
    boolean supports(AuthenticationToken token);
    
    // 认证逻辑实现
    AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
}

身份凭证

AuthenticationToken是认证的凭证,可以简单的看作是账户名和密码。

1
2
3
4
5
6
7
8
public interface AuthenticationToken extends Serializable {
    
    // 用户的唯一标识,相当于账户名
    Object getPrincipal();

    // 用户的凭证,相当于密码 
    Object getCredentials();
}

下面是AuthenticationToken相关接口的类图

classDiagram
	class AuthenticationToken
	<<interface>> AuthenticationToken
	
	class HostAuthenticationToken
	<<interface>> HostAuthenticationToken
	AuthenticationToken <|-- HostAuthenticationToken
	
	class RememberMeAuthenticationToken
	<<interface>> RememberMeAuthenticationToken
	AuthenticationToken <|-- RememberMeAuthenticationToken
	
	class UsernamePasswordToken
	HostAuthenticationToken <|-- UsernamePasswordToken
	RememberMeAuthenticationToken <|-- UsernamePasswordToken
	
	class BearerToken
	HostAuthenticationToken <|-- BearerToken

HostAuthenticationToken添加了客户端的ip接口,RememberMeAuthenticationToken添加了是否需要记住我的接口。我们接着需要看看子类的实现,UsernamePasswordTokenBearerToken

UsernamePasswordToken是用于账户名和密码的验证方式,所以它会保存这两个属性,当作PrincipalCredentials

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class UsernamePasswordToken {
    
    private String username;
    
    private char[] password;
   
    // 返回账户名
    public Object getPrincipal() {
        return getUsername();
    }
    
    // 返回密码
    public Object getCredentials() {
        return getPassword();
    }
}
    

BearerToken是用于基于 token 的验证方式,所以它的PrincipalCredentials都是 token。当使用 jwt 验证时,就可以使用BearerToken

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class BearerToken {
    
    private final String token;
    
    // 返回 token
    public Object getPrincipal() {
        return token;
    }

    // 返回 token
    public Object getCredentials() {
        return token;
    }
}

身份认证结果

AuthenticationInfo表示身份认证后的结果,包含了用户的身份信息。需要注意下getPrincipals返回的是身份集合,因为当存在多个Realm时,一个账户有可能是有多个身份的。

1
2
3
4
5
6
7
8
public interface AuthenticationInfo extends Serializable {
    
    // 返回认证后的身份集合
    PrincipalCollection getPrincipals();
    
    // 返回凭证
    Object getCredentials();
}

AuthenticationInfo的相关类图如下所示,注意到右上边的AuthorizationInfo接口,它包含了用户的权限。

classDiagram

	class AuthenticationInfo
	<<interface>> AuthenticationInfo
	
	class SaltedAuthenticationInfo
	<<interface>> SaltedAuthenticationInfo
	AuthenticationInfo <|-- SaltedAuthenticationInfo
	
	class MergableAuthenticationInfo
	<<interface>> MergableAuthenticationInfo
	AuthenticationInfo <|-- MergableAuthenticationInfo
	
    class SimpleAuthenticationInfo
	SaltedAuthenticationInfo <|-- SimpleAuthenticationInfo
	MergableAuthenticationInfo <|-- SimpleAuthenticationInfo
	
	class AuthorizationInfo
	<<interface>> AuthorizationInfo
	
	class Account
	<<interface>> Account
	AuthenticationInfo <|-- Account
	AuthorizationInfo <|-- Account
	
	class SimpleAccount
	Account <|-- SimpleAccount
	MergableAuthenticationInfo <|-- SimpleAccount
	SaltedAuthenticationInfo <|-- SimpleAccount
	

SaltedAuthenticationInfo支持加盐操作,MergableAuthenticationInfo支持合并用户身份信息。

SimpleAuthenticationInfo也只是简单的实现,保存了对应的属性。

SimpleAccount实现了AuthenticationInfo接口,保存了身份信息,也实现了AuthorizationInfo接口,保存了用户的权限。

Realm 子类

现在可以回过来看看 Realm 的子类,首先来看看它的类图

classDiagram
	class Realm
	<<interface>> Realm

	class CachingRealm
	<<abstract>> CachingRealm
	
	Realm <|-- CachingRealm
	
	class AuthenticatingRealm
	<<abstract>> AuthenticatingRealm
	
	CachingRealm <|-- AuthenticatingRealm
	
	class AuthorizingRealm
	<<abstract>> AuthorizingRealm
	
	class Authorizer
	<<interface>> Authorizer
	
    Authorizer <|-- AuthorizingRealm
	AuthenticatingRealm <|-- AuthorizingRealm

CachingRealm提供了缓存管理,在后续子类中会用到。

AuthenticatingRealm实现了身份认证。

AuthorizingRealm实现了权限检查。

由于CachingRealm类比较简单,这里就不做详细介绍了,对于AuthenticatingRealmAuthorizingRealm需要详细介绍。

我们先来仔细看看AuthenticatingRealm的原理,它负责返回用户的身份信息,并且会将结果缓存。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public abstract class AuthenticatingRealm extends CachingRealm implements Initializable {
    
    // 表示这个Realm所能处理的 token 类型
    private Class<? extends AuthenticationToken> authenticationTokenClass;
    
    public boolean supports(AuthenticationToken token) {
        return token != null && getAuthenticationTokenClass().isAssignableFrom(token.getClass());
    }
    
    public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 先从缓存里查找对应的身份信息
        AuthenticationInfo info = getCachedAuthenticationInfo(token);
        if (info == null) {
            // 如果缓存没有,则需要调用doGetAuthenticationInfo方法执行身份验证
            // 子类需要实现此方法
            info = doGetAuthenticationInfo(token);
            if (token != null && info != null) {
                // 将结果存到缓存
                cacheAuthenticationInfoIfPossible(token, info);
            }
        }
        if (info != null) {
            // 验证token是否和身份信息info一致
            assertCredentialsMatch(token, info);
        }
        return info;
    }
}

继续看AuthorizingRealm的原理,它实现了Authorizer接口,负责返回用户的权限,并且也缓存结果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class AuthorizingRealm extends AuthenticatingRealm implements Authorizer {
    
    protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
        AuthorizationInfo info = null;
        // 先从缓存中查询用户权限
        Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();
        Object key = getAuthorizationCacheKey(principals);
        info = cache.get(key);

        if (info == null) {
            // 如果缓存中没有找到,则调用doGetAuthorizationInfo方法获取,
            // 子类需要实现此方法
            info = doGetAuthorizationInfo(principals);
            if (info != null && cache != null) {
                Object key = getAuthorizationCacheKey(principals);
                cache.put(key, info);
            }
        }
        return info;
    }
}

系统自带 Realm

shrio 已经实现了多个 Realm 的子类,如下图所示。

classDiagram
	class AuthorizingRealm
	
	class SimpleAccountRealm
	AuthorizingRealm <|-- SimpleAccountRealm
	
	
	class TextConfigurationRealm
	SimpleAccountRealm <|-- TextConfigurationRealm
	
	class IniRealm
	TextConfigurationRealm <|-- IniRealm
	
	class PropertiesRealm
	TextConfigurationRealm <|-- PropertiesRealm
	
	class JdbcRealm
	AuthorizingRealm <|-- JdbcRealm
	
	class SaltAwareJdbcRealm
	JdbcRealm <|-- SaltAwareJdbcRealm
	
	class AbstractLdapRealm
	<<abstract>> AbstractLdapRealm
	AuthorizingRealm <|-- AbstractLdapRealm
	
	class ActiveDirectoryRealm
	AbstractLdapRealm <|-- ActiveDirectoryRealm

这些子类按照使用场景分为三类,左边以SimpleAccountRealm为父类的,表示用户信息都存储到内存里。TextConfigurationRealm表示从文件中加载信息到内存,它的子类IniRealm表示读取ini格式的配置文件,PropertiesRealm表示读取properties格式的配置文件。

中间以JdbcRealm为父类的,表示用户信息存储在数据库中。它的子类SaltAwareJdbcRealm在其基础之上,增加了盐,可以看作是一种加密手段。

右边以AbstractLdapRealm为父类的,表示用户信息存储在ldap服务中。ActiveDirectoryRealm表示采用了ActiveDirectory服务,它是ldap实现的一种。

自定义 Realm

上面介绍了Realm的设计思想,这里会介绍我们该如何使用Realm。我们在自定义Realm时,只需要继承AuthorizingRealm,然后实现它的两个方法即可。

1
2
3
4
5
6
7
8
class AuthorizingRealm {
    
    // 进行身份验证,返回身份信息
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
    
    // 返回用户的权限
    protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);
}

这里写个简单的例子,只允许admin用户登录,它只有read权限。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyRealm extends AuthorizingRealm {
    
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        
    	String username = token.;
        String password = token.;
        if(username.equals("admin") && username.equals("admin")) {
            return new SimpleAuthenticationInfo(username, password, "myrealm");
        }
        // 如果认证失败,则抛出异常
        throw new AuthenticationException();
    }
    
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = (String) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        if (username.equals("admin")) {
			info.addStringPermission("read");            
        }
        return info;
    }
    
}
updatedupdated2023-07-022023-07-02