Shrio源码分析(4) - 数据域(Realm)

本文在于分析Shiro源码,对于新学习的朋友可以参考
[开涛博客](http://jinnianshilongnian.iteye.com/blog/2018398)进行学习。

本篇主要分析Shiro中的Realm接口。Shiro使用Realm接口作为外部数据源,主要处理认证和授权工作。Realm接口如下。

public interface Realm {

    /**
     * Realm必须要有一个唯一的名称
     */
    String getName();

    /**
     * 判断该Realm是否支持处理给定的token认证
     */
    boolean supports(AuthenticationToken token);

    /**
     * 认证token,并返回已认证的AuthenticationInfo
     * 如果没有账户可以认证,返回null,如果认证失败抛出异常
     */
    AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
}

CachingRealm抽象类

CachingRealm是带有缓存功能的Realm抽象实现。在CachingRealm中提供了对Realm进行缓存功能的缓存管理器CacheManager,但并没有实现具体缓存什么。在CachingRealm中提供了对onLogout的处理,该方法从LogoutAware实现来,用来处理用户登出后清理缓存数据。Shiro默认对Realm开启缓存功能。

// Realm名称
private String name;
// 是否开启缓存,默认构造方法开启缓存
private boolean cachingEnabled;
// 缓存管理器
private CacheManager cacheManager;

public CachingRealm() {
    this.cachingEnabled = true;
    this.name = getClass().getName() + "_" + INSTANCE_COUNT.getAndIncrement();
}

值得一提的是afterCacheManagerSet()这个钩子方法,在设置缓存处理器后会调用这个方法,在后面的分析中会由子类重写。

public void setCacheManager(CacheManager cacheManager) {
	this.cacheManager = cacheManager;
	afterCacheManagerSet();
}

protected void afterCacheManagerSet() {
}

AuthenticatingRealm抽象类

AuthenticatingRealm是一个可认证的Realm抽象实现类。 AuthenticatingRealm继承了CachingRealm,并实现了Initializable。Initializable提供的init()方法在初始化时会调用。下面是AuthenticatingRealm的属性和构造方法。

// 凭证匹配器,用来匹配凭证是否正确
private CredentialsMatcher credentialsMatcher;

// 缓存通过认证的认证数据
private Cache<Object, AuthenticationInfo> authenticationCache;

// 是否认证缓存
private boolean authenticationCachingEnabled;

// 认证缓存的名称
private String authenticationCacheName;

/**
 * 定义Realm支持的AuthenticationToken类型
 */
private Class<? extends AuthenticationToken> authenticationTokenClass;

public AuthenticatingRealm() {
    this(null, new SimpleCredentialsMatcher());
}

public AuthenticatingRealm(CacheManager cacheManager) {
    this(cacheManager, new SimpleCredentialsMatcher());
}

public AuthenticatingRealm(CredentialsMatcher matcher) {
    this(null, matcher);
}

public AuthenticatingRealm(CacheManager cacheManager, CredentialsMatcher matcher) {
    // 默认支持UsernamePasswordToken类型
    authenticationTokenClass = UsernamePasswordToken.class;

    // 认证不缓存
    this.authenticationCachingEnabled = false;

    // 设置认证缓存的名称
    int instanceNumber = INSTANCE_COUNT.getAndIncrement();
    this.authenticationCacheName = getClass().getName() + DEFAULT_AUTHORIZATION_CACHE_SUFFIX;
    if (instanceNumber > 0) {
        this.authenticationCacheName = this.authenticationCacheName + "." + instanceNumber;
    }

    // 设置缓存管理器
    if (cacheManager != null) {
        setCacheManager(cacheManager);
    }
    
    // 设置凭证匹配器
    if (matcher != null) {
        setCredentialsMatcher(matcher);
    }
}

从属性和构造方法我们可以看出,AuthenticatingRealm会进行认证,对认证的结果AuthenticationInfo进行缓存,认证时需要使用凭证匹配器来匹配凭证是否正确。下面,我们根据这个思路可以去看看进行认证的方法getAuthenticationInfo(AuthenticationToken token)。

public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    // 从认证缓存中获取认证结果
    AuthenticationInfo info = getCachedAuthenticationInfo(token);
    if (info == null) {
        // 这是一个抽象方法,子类去完成认证过程
        info = doGetAuthenticationInfo(token);
        if (token != null && info != null) {
            // 如果认证通过,则将认证结果缓存起来
            cacheAuthenticationInfoIfPossible(token, info);
        }
    }

    // 匹配凭证是否正确,如果不正确将会抛出异常
    if (info != null) {
        assertCredentialsMatch(token, info);
    }

    return info;
}

关于AuthenticationInfo缓存过程中的一些细节。在缓存的过程中是以AuthenticationToken中的身份进行缓存的,所有身份肯定要是唯一的。属性authenticationCache可以由外部提供,也可以通过缓存管理器生成,一般情况下authenticationCache不需要外部设置。

AuthorizingRealm抽象类

AuthorizingRealm继承了AuthenticatingRealm,负责处理角色和权限。AuthorizingRealm的实现方式和AuthenticatingRealm一样,提供了一个抽象的doGetAuthorizationInfo(PrincipalCollection principals)方法。这里不做详细介绍,我们会在后面分析角色权限时介绍。

基于Jdbc的Realm(JdbcRealm类)

JdbcRealm类可以直接和数据库连接,从数据中获取用户名、密码、角色、权限等数据信息。通过和数据库的直接连接来判断认证是否正确,是否有角色权限功能。

在JdbcRealm中提供了一些Sql语句常量,通过这些sql来做数据库操作。当然,操作数据肯定需要数据库数据源。

// 通过用户名查找密码的Sql语句
protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";

/**
 * 通过用户名称查找密码和加密盐的Sql语句
 */
protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";

/**
 * 通过用户名查找用户所有角色
 */
protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?";

/**
 * 通过角色名称查找角色拥有的权限
 */
protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";

/**
 * 定义了几种加盐模式:
 *   NO_SALT - 密码没有加密盐
 *   CRYPT - unix加密(这种模式目前还支持)
 *   COLUMN - 加密盐存储在数据库表字段中 
 *   EXTERNAL - 加密盐没有存储在数据库
 */
public enum SaltStyle {NO_SALT, CRYPT, COLUMN, EXTERNAL};

// 数据库数据源
protected DataSource dataSource;

// 查询密码和加密盐的SQL
protected String authenticationQuery = DEFAULT_AUTHENTICATION_QUERY;

// 查询用户角色的SQL
protected String userRolesQuery = DEFAULT_USER_ROLES_QUERY;

// 查询角色拥有权限的SQL
protected String permissionsQuery = DEFAULT_PERMISSIONS_QUERY;

protected boolean permissionsLookupEnabled = false;

// 密码没有加密盐模式
protected SaltStyle saltStyle = SaltStyle.NO_SALT;

对于不同的数据库,这些默认的Sql是可以更改的,JdbcRealm都提供了相应的setter方法。那么,Jdbc是如何认证和获取角色权限的呢?下面继续分析doGetAuthenticationInfo和doGetAuthorizationInfo这两个方法。

  1. 认证过程
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    // 只支持UsernamePasswordToken类型
    UsernamePasswordToken upToken = (UsernamePasswordToken) token;
    
    // 用户名
    String username = upToken.getUsername();

    // 用户名空判断
    if (username == null) {
        throw new AccountException("Null usernames are not allowed by this realm.");
    }

    Connection conn = null;
    SimpleAuthenticationInfo info = null;
    try {
    
        // 获取数据库连接
        conn = dataSource.getConnection();

        String password = null;
        String salt = null;
        switch (saltStyle) {
            case NO_SALT:
                password = getPasswordForUser(conn, username)[0];
                break;
            case CRYPT:
                // TODO: separate password and hash from getPasswordForUser[0]
                throw new ConfigurationException("Not implemented yet");
                //break;
            case COLUMN:
                String[] queryResults = getPasswordForUser(conn, username);
                password = queryResults[0];
                salt = queryResults[1];
                break;
            case EXTERNAL:
                password = getPasswordForUser(conn, username)[0];
                // 以用户名作为加密盐
                salt = getSaltForUser(username);
        }

        if (password == null) {
            throw new UnknownAccountException("No account found for user [" + username + "]");
        }

        // 创建一个认证信息
        info = new SimpleAuthenticationInfo(username, password.toCharArray(), getName());
        
        if (salt != null) {
            info.setCredentialsSalt(ByteSource.Util.bytes(salt));
        }

    } catch (SQLException e) {
        throw new AuthenticationException(message, e);
    } finally {
        // 关闭数据连接
        JdbcUtils.closeConnection(conn);
    }

    return info;
}

2 授权过程

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

    // 身份不能为空
    if (principals == null) {
        throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
    }

    // 从身份中获取用户名
    String username = (String) getAvailablePrincipal(principals);

    Connection conn = null;
    Set<String> roleNames = null;
    Set<String> permissions = null;
    try {
        // 获取数据库连接
        conn = dataSource.getConnection();

        // 获取角色集合
        roleNames = getRoleNamesForUser(conn, username);
        if (permissionsLookupEnabled) {
            // 获取权限集合
            permissions = getPermissions(conn, username, roleNames);
        }
    } catch (SQLException e) {
        throw new AuthorizationException(message, e);
    } finally {
        JdbcUtils.closeConnection(conn);
    }

    // 返回带有角色权限的认证信息
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
    info.setStringPermissions(permissions);
    return info;

}

在Shiro中还提供了一些其他的Realm。SimpleAccountRealm、TextConfigurationRealm、IniRealm、PropertiesRealm。这里就不一一介绍了,有兴趣可以自己去看。

总结

在Shiro中Realm接口作为一个与应用程序外接的接口,可以通过Realm提供认证和授权的数据信息。在开发使用中最常用的就是从AuthenticatingRealm或AuthorizingRealm抽象类来实现业务中具体的Realm实例。doGetAuthenticationInfo(AuthenticationToken token)处理认证过程,doGetAuthorizationInfo(PrincipalCollection principals)处理授权过程。

转载于:https://my.oschina.net/xiaoqiyiye/blog/1618341

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值