⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 https://my.oschina.net/xiaoqiyiye/blog/1618341 「xiaoqiyiye」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

本文在于分析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)处理授权过程。

文章目录
  1. 1. CachingRealm抽象类
  2. 2. AuthenticatingRealm抽象类
  3. 3. AuthorizingRealm抽象类
  4. 4. 基于Jdbc的Realm(JdbcRealm类)
  5. 5. 总结