shiro源码分析(四)具体的Realm

[size=medium]首先还是Realm的接口设计图:[/size]

[img]http://dl2.iteye.com/upload/attachment/0104/5692/3c8b1b3b-7caf-3977-9700-2df30fc7733b.png[/img]
[size=medium]这里只来说明SimpleAccountRealm和JdbcRealm的实现。
首先是SimpleAccountRealm不用关心数据的具体来源,只提供了与上层的交互,即实现了AuthenticatingRealm遗留的AuthenticationInfo doGetAuthenticationInfo和AuthorizingRealm遗留的AuthorizationInfo doGetAuthorizationInfo。
如下:[/size]

protected final Map<String, SimpleAccount> users; //username-to-SimpleAccount
protected final Map<String, SimpleRole> roles; //roleName-to-SimpleRole
protected final ReadWriteLock USERS_LOCK;
protected final ReadWriteLock ROLES_LOCK;

public SimpleAccountRealm() {
this.users = new LinkedHashMap<String, SimpleAccount>();
this.roles = new LinkedHashMap<String, SimpleRole>();
USERS_LOCK = new ReentrantReadWriteLock();
ROLES_LOCK = new ReentrantReadWriteLock();
//SimpleAccountRealms are memory-only realms - no need for an additional cache mechanism since we're
//already as memory-efficient as we can be:
setCachingEnabled(false);
}

[size=medium]SimpleAccountRealm内部有四个属性,Map<String, SimpleAccount> users:用于存放用户账号信息,Map<String, SimpleRole> roles用于存放角色名的信息。这两个都是各种配置的最终归属存储地。
ReadWriteLock USERS_LOCK:由于这些配置信息,一般不会去修改,大部分时间用于查询,所以要使用读写锁。一般的synchronized同步,不管你是读还是写,都要进行等待。写与写需要进行同步,写与读也要进行同步,但读与读却并不需要进行同步,所以对于那些经常读的场景,要使用读写锁ReadWriteLock 来提升性能。ReadWriteLock ROLES_LOCK同理。
有了以上数据源,实现父类的遗留的方法就比较简单了。如下:[/size]

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
SimpleAccount account = getUser(upToken.getUsername());

if (account != null) {

if (account.isLocked()) {
throw new LockedAccountException("Account [" + account + "] is locked.");
}
if (account.isCredentialsExpired()) {
String msg = "The credentials for account [" + account + "] are expired";
throw new ExpiredCredentialsException(msg);
}

}

return account;
}

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = getUsername(principals);
USERS_LOCK.readLock().lock();
try {
return this.users.get(username);
} finally {
USERS_LOCK.readLock().unlock();
}
}

[size=medium]代码就很简单了,就是从users中取出相应的用户数据。接下来要分析清几个概念:
AuthorizationInfo、AuthenticationInfo、SimpleAccount、SimpleRole、PrincipalCollection。
PrincipalCollection:看下文档介绍[/size]
[quote]
/**
* A collection of all principals associated with a corresponding {@link Subject Subject}. A <em>principal</em> is
* just a security term for an identifying attribute, such as a username or user id or social security number or
* anything else that can be considered an 'identifying' attribute for a {@code Subject}.
* <p/>
* A PrincipalCollection organizes its internal principals based on the {@code Realm} where they came from when the
* Subject was first created. To obtain the principal(s) for a specific Realm, see the {@link #fromRealm} method. You
* can also see which realms contributed to this collection via the {@link #getRealmNames() getRealmNames()} method.
*/
[/quote]
[size=medium]一个principal仅仅是Subject的一个标识而已,如可以是用户名,用户id等。PrincipalCollection则是这些属性的集合。每个用户属性可以来自不同的Realm。Collection fromRealm(String realmName)可以获取某个Realm的所有用户属性。Set<String> getRealmNames()可以获取到与Subject关联的用户的属性来自于哪些Realm。
Object getPrimaryPrincipal():主要是用于获取唯一标示,如UUID、username等。
接口如下:[/size]

[img]http://dl2.iteye.com/upload/attachment/0104/5694/32e4f824-0af3-3e41-9ace-895ff42adc1b.png[/img]
[size=medium]MutablePrincipalCollection如下:[/size]

public interface MutablePrincipalCollection extends PrincipalCollection {
void add(Object principal, String realmName);
void addAll(Collection principals, String realmName);
void addAll(PrincipalCollection principals);
void clear();
}

[size=medium]我们知道每一个标示都有所属的realm,所以再添加的时候,要带上realmName。
SimplePrincipalCollection:[/size]

private Map<String, Set> realmPrincipals;

[size=medium]一个重要的数据集合,key是realm的name,value是principal集合。
这个接口分支一直在强调,每个principal都是有所属的realm的。
PrincipalMap:我这一块没有搞明白,先放下。

AuthenticationInfo 它是含有用户和密码信息的地方:[/size]

public interface AuthenticationInfo extends Serializable {
PrincipalCollection getPrincipals();
Object getCredentials();
}

[size=medium]AuthorizationInfo :存放用户权限的地方[/size]

public interface AuthorizationInfo extends Serializable {
Collection<String> getRoles();
Collection<String> getStringPermissions();
Collection<Permission> getObjectPermissions();
}

[size=medium]类图如下:[/size]
[img]http://dl2.iteye.com/upload/attachment/0104/5696/ad4669db-0a16-35a6-9a98-5737a7729721.png[/img]
[size=medium]MergableAuthenticationInfo 意味着AuthenticationInfo可以进行合并:[/size]

public interface MergableAuthenticationInfo extends AuthenticationInfo {
void merge(AuthenticationInfo info);
}

[size=medium]SaltedAuthenticationInfo 主要用于密码匹配,后续文章专门说明:[/size]

public interface SaltedAuthenticationInfo extends AuthenticationInfo {
ByteSource getCredentialsSalt();
}

[size=medium]SimpleAuthenticationInfo:存储了三个重要的属性:[/size]

protected PrincipalCollection principals;
protected Object credentials;
protected ByteSource credentialsSalt;

[size=medium]然后就是实现了MergableAuthenticationInfo 接口,可以进行合并,这里的合并在第一篇文章中Realm认证中提到过:[/size]

public void merge(AuthenticationInfo info) {
if (info == null || info.getPrincipals() == null || info.getPrincipals().isEmpty()) {
return;
}

if (this.principals == null) {
this.principals = info.getPrincipals();
} else {
if (!(this.principals instanceof MutablePrincipalCollection)) {
this.principals = new SimplePrincipalCollection(this.principals);
}
((MutablePrincipalCollection) this.principals).addAll(info.getPrincipals());
}

//only mess with a salt value if we don't have one yet. It doesn't make sense
//to merge salt values from different realms because a salt is used only within
//the realm's credential matching process. But if the current instance's salt
//is null, then it can't hurt to pull in a non-null value if one exists.
//
//since 1.1:
if (this.credentialsSalt == null && info instanceof SaltedAuthenticationInfo) {
this.credentialsSalt = ((SaltedAuthenticationInfo) info).getCredentialsSalt();
}

Object thisCredentials = getCredentials();
Object otherCredentials = info.getCredentials();

if (otherCredentials == null) {
return;
}

if (thisCredentials == null) {
this.credentials = otherCredentials;
return;
}

if (!(thisCredentials instanceof Collection)) {
Set newSet = new HashSet();
newSet.add(thisCredentials);
setCredentials(newSet);
}

// At this point, the credentials should be a collection
Collection credentialCollection = (Collection) getCredentials();
if (otherCredentials instanceof Collection) {
credentialCollection.addAll((Collection) otherCredentials);
} else {
credentialCollection.add(otherCredentials);
}
}

[size=medium]主要分principals、credentialsSalt 和credentials三项的合并,代码也和简单。
SimpleAuthorizationInfo:存放了认证用户的角色和用户权限。[/size]

protected Set<String> roles;
protected Set<String> stringPermissions;
protected Set<Permission> objectPermissions;

[size=medium]最重要的就是SimpleAccount:[/size]

public class SimpleAccount implements Account, MergableAuthenticationInfo, SaltedAuthenticationInfo, Serializable {
private SimpleAuthenticationInfo authcInfo;
private SimpleAuthorizationInfo authzInfo;
private boolean locked;
private boolean credentialsExpired;
}

[size=medium]它有SimpleAuthenticationInfo 、SimpleAuthorizationInfo ,所以是用户认证信息和权限信息的汇总。
还有两个属性locked和credentialsExpired,用来表示用户的锁定和密码过期的状态。
至此整个SimpleAccount便介绍完了。

回到SimpleAccountRealm,SimpleAccountRealm已经拥有Map<String, SimpleAccount> users和Map<String, SimpleRole> roles数据了,但是这些数据是怎么产生的呢?这就需要交给它的子类TextConfigurationRealm来完成:[/size]

private volatile String userDefinitions;
private volatile String roleDefinitions;

[size=medium]仅仅两个字符串包含了所有的用户和角色的配置总来源。所以TextConfigurationRealm主要就是对这两个字符串的解析:[/size]

@Override
protected void onInit() {
super.onInit();
processDefinitions();
}
protected void processDefinitions() {
try {
//解析角色配置
processRoleDefinitions();
//解析用户配置
processUserDefinitions();
} catch (ParseException e) {
String msg = "Unable to parse user and/or role definitions.";
throw new ConfigurationException(msg, e);
}
}
protected void processRoleDefinitions() throws ParseException {
String roleDefinitions = getRoleDefinitions();
if (roleDefinitions == null) {
return;
}
//先将角色字符串按行分割,然后每行再按照key value分割
Map<String, String> roleDefs = toMap(toLines(roleDefinitions));
processRoleDefinitions(roleDefs);
}
protected void processRoleDefinitions(Map<String, String> roleDefs) {
if (roleDefs == null || roleDefs.isEmpty()) {
return;
}
for (String rolename : roleDefs.keySet()) {
String value = roleDefs.get(rolename);

SimpleRole role = getRole(rolename);
if (role == null) {
role = new SimpleRole(rolename);
add(role);
}

Set<Permission> permissions = PermissionUtils.resolveDelimitedPermissions(value, getPermissionResolver());
role.setPermissions(permissions);
}
}

[size=medium]再通过PermissionResolver将字符串形式的权限转化成Permission对象,知道大致情况了,就可以了,不需要每一步都弄清楚。
TextConfigurationRealm主要用于解析两个配置字符串,这两个配置字符串的产生则继续交给子类来完成。IniRealm则是通过ini配置文件来产生这两个字符串,PropertiesRealm则是通过properties文件来产生这两个字符串。

至此,SimpleAccountRealm这一路就大致走通了,接下来就是另一条路JdbcRealm了。[/size]

public class JdbcRealm extends AuthorizingRealm {
protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
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 = ?";
protected DataSource dataSource;
}

[size=medium]首先是含有这几个默认的sql和DataSource dataSource,用于从数据库中获取相应的用户、角色、权限等数据。
根据上一篇文章我们知道JdbcRealm 要实现AuthenticatingRealm遗留的AuthenticationInfo doGetAuthenticationInfo和AuthorizingRealm遗留的AuthorizationInfo doGetAuthorizationInfo。下面就看看是怎么来实现的:[/size]

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();

// Null username is invalid
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不存在数据库中,默认的值为username
salt = getSaltForUser(username);
}

if (password == null) {
throw new UnknownAccountException("No account found for user [" + username + "]");
}
//根据用户名、密码、盐值构建一个SimpleAuthenticationInfo
info = new SimpleAuthenticationInfo(username, password.toCharArray(), getName());

if (salt != null) {
info.setCredentialsSalt(ByteSource.Util.bytes(salt));
}

} catch (SQLException e) {
final String message = "There was a SQL error while authenticating user [" + username + "]";
if (log.isErrorEnabled()) {
log.error(message, e);
}

// Rethrow any SQL errors as an authentication exception
throw new AuthenticationException(message, e);
} finally {
JdbcUtils.closeConnection(conn);
}

return info;
}
private String[] getPasswordForUser(Connection conn, String username) throws SQLException {

String[] result;
boolean returningSeparatedSalt = false;
switch (saltStyle) {
case NO_SALT:
case CRYPT:
case EXTERNAL:
result = new String[1];
break;
default:
result = new String[2];
returningSeparatedSalt = true;
}

PreparedStatement ps = null;
ResultSet rs = null;
try {
ps = conn.prepareStatement(authenticationQuery);
ps.setString(1, username);

// Execute query
rs = ps.executeQuery();

// Loop over results - although we are only expecting one result, since usernames should be unique
boolean foundResult = false;
while (rs.next()) {

// Check to ensure only one row is processed
if (foundResult) {
throw new AuthenticationException("More than one user row found for user [" + username + "]. Usernames must be unique.");
}

result[0] = rs.getString(1);
if (returningSeparatedSalt) {
result[1] = rs.getString(2);
}

foundResult = true;
}
} finally {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(ps);
}

return result;
}
protected String getSaltForUser(String username) {
return username;
}

[size=medium]代码很简单就不再一一细说。再看下doGetAuthorizationInfo是怎么实现的:[/size]

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

//null usernames are invalid
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();

// Retrieve roles and permissions from database
roleNames = getRoleNamesForUser(conn, username);
if (permissionsLookupEnabled) {
permissions = getPermissions(conn, username, roleNames);
}

} catch (SQLException e) {
final String message = "There was a SQL error while authorizing user [" + username + "]";
if (log.isErrorEnabled()) {
log.error(message, e);
}

// Rethrow any SQL errors as an authorization exception
throw new AuthorizationException(message, e);
} finally {
JdbcUtils.closeConnection(conn);
}

SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
info.setStringPermissions(permissions);
return info;

}

[size=medium]第一步先根据PrincipalCollection 来获取用户名,第二步根据用户名来获取角色,第三部根据角色和用户名来获取权限。后两步都是执行简单的sql,不再说,看下如何由PrincipalCollection 获取用户名,该方法定义在CachingRealm中:[/size]

protected Object getAvailablePrincipal(PrincipalCollection principals) {
Object primary = null;
if (!CollectionUtils.isEmpty(principals)) {
Collection thisPrincipals = principals.fromRealm(getName());
if (!CollectionUtils.isEmpty(thisPrincipals)) {
primary = thisPrincipals.iterator().next();
} else {
//no principals attributed to this particular realm. Fall back to the 'master' primary:
primary = principals.getPrimaryPrincipal();
}
}

return primary;
}

[size=medium]两种情况,首先是获取当前Realm的Principals,如果有取其第一个。如果没有,则调用getPrimaryPrincipal()方法。然后看下JdbcRealm的一个简单使用:
如果默认按照JdbcRealm的sql来作为数据库的查询来说,建表如下:
users表:[/size]

CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(45) NOT NULL,
`password` varchar(45) NOT NULL,
`password_salt` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username_UNIQUE` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

[size=medium]user_roles表:[/size]

CREATE TABLE `user_roles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(45) DEFAULT NULL,
`role_name` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

[size=medium]roles_permissions表:[/size]

CREATE TABLE `roles_permissions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`role_name` varchar(45) DEFAULT NULL,
`permission` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

[size=medium]文章最后会给出数据库sql文件。
然后就是配置ini文件:[/size]

[main]
#realm
dataSource=com.mchange.v2.c3p0.ComboPooledDataSource
dataSource.driverClass=com.mysql.jdbc.Driver
dataSource.jdbcUrl=jdbc:mysql://localhost:3306/shiro
dataSource.user=root
dataSource.password=XXXXXX
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
jdbcRealm.dataSource=$dataSource
jdbcRealm.permissionsLookupEnabled=true
securityManager.realms=$jdbcRealm

[size=medium]使用的dataSource是c3p0的dataSource,mysql驱动也是必然不能少的,所以maven中要加入依赖:[/size]

<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.29</version>
</dependency>

<!-- 连接池 -->
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>

[size=medium]为了输出方便代码更改为:[/size]

public class ShiroTest {

@Test
public void testHelloworld() {
init();

Subject subject=login("lg","123");
System.out.println(subject.hasRole("role1"));
System.out.println(subject.hasRole("role2"));
System.out.println(subject.hasRole("role3"));
}

private Subject login(String userName,String password){
//3、得到Subject及创建用户名/密码身份验证Token(即用户身份/凭证)
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(userName,password);
subject.login(token);
return subject;
}

private void init(){
//1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager
Factory<org.apache.shiro.mgt.SecurityManager> factory =
new IniSecurityManagerFactory("classpath:shiro.ini");
//2、得到SecurityManager实例 并绑定给SecurityUtils
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
}
}

[size=medium]对于lg用户,在数据库中它是有两个角色的,role1和role2。所以结果为true、true、false。[/size]

true
true
false

[size=medium]
OK,通过。最后附上JdbcRealm的使用例子。


作者:乒乓狂魔
[/size]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值