public class UserRealm extends AuthorizingRealm {
private UserService userService = new UserServiceImpl();
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String)principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setRoles(userService.findRoles(username));
authorizationInfo.setStringPermissions(userService.findPermissions(username));
return authorizationInfo;
}
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String)token.getPrincipal();
User user = userService.findByUsername(username);
if(user == null) {
throw new UnknownAccountException();//没找到帐号
}
if(Boolean.TRUE.equals(user.getLocked())) {
throw new LockedAccountException(); //帐号锁定
}
//交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以在此判断或自定义实现
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user.getUsername(), //用户名
user.getPassword(), //密码
ByteSource.Util.bytes(user.getCredentialsSalt()),//salt=username+salt
getName() //realm name
);
return authenticationInfo;
}
}

1、UserRealm父类AuthorizingRealm将获取Subject相关信息分成两步:获取身份验证信息(doGetAuthenticationInfo)及授权信息(doGetAuthorizationInfo);

2、doGetAuthenticationInfo获取身份验证相关信息:首先根据传入的用户名获取User信息;然后如果user为空,那么抛出没找到帐号异常UnknownAccountException;

如果user找到但锁定了抛出锁定异常LockedAccountException;最后生成AuthenticationInfo信息,交给间接父类AuthenticatingRealm使用CredentialsMatcher进行判断密码是否匹配,

如果不匹配将抛出密码错误异常IncorrectCredentialsException;另外如果密码重试此处太多将抛出超出重试次数异常ExcessiveAttemptsException;

在组装SimpleAuthenticationInfo信息时,需要传入:身份信息(用户名)、凭据(密文密码)、盐(username+salt),CredentialsMatcher使用盐加密传入的明文密码和此处的密文密码进行匹配。

3、doGetAuthorizationInfo获取授权信息:PrincipalCollection是一个身份集合,因为我们现在就一个Realm,所以直接调用getPrimaryPrincipal得到之前传入的用户名即可;然后根据用户名调用UserService接口获取角色及权限信息。

我们来看下Realm类的继承关系:

shiro的Realm-LMLPHP

一般我们使用的是AuthorizingRealm、CasRealm、JdbcRealm,AuthorizingRealm前面已经使用过了。CasRealm、JdbcRealm的使用例子如下:  

 @Bean(name = "myCasRealm")
public CasRealm myCasRealm(EhCacheManager cacheManager) {
CasRealm casRealm = new CasRealm();
casRealm.setCacheManager(cacheManager);
casRealm.setCasServerUrlPrefix(ShiroCasConfig.casServerUrlPrefix);
// 客户端回调地址 https://localhost:8081/cas 用来接收cas服务端票据,并且必须要和下面过滤器拦截的地址一致,拦截之后交由casFilter处理验证票据
casRealm.setCasService(ShiroCasConfig.shiroServerUrlPrefix + ShiroCasConfig.casFilterUrlPattern);
return casRealm;
} @Bean(name="myJdbcRealm")
public JdbcRealm myJdbcRealm(@Qualifier("shirocasDataSource") DataSource shirocasDataSource){
JdbcRealm jdbcRealm = new JdbcRealm();
jdbcRealm.setDataSource(shirocasDataSource);
String authenticationQuery = "select password from account where name=?";
jdbcRealm.setAuthenticationQuery(authenticationQuery);
String userRolesQuery="SELECT NAME FROM role WHERE id =(SELECT roleId FROM account_role WHERE userId = (SELECT id FROM account WHERE NAME = ?))";
jdbcRealm.setUserRolesQuery(userRolesQuery);
String permissionsQuery = "SELECT NAME FROM permission WHERE id in (SELECT permissionId FROM permission_role WHERE (SELECT id FROM role WHERE NAME = ?))";
jdbcRealm.setPermissionsQuery(permissionsQuery);
jdbcRealm.setPermissionsLookupEnabled(true);
return jdbcRealm;
}

casRealm的部分源码:

/**
* This realm implementation acts as a CAS client to a CAS server for authentication and basic authorization.
* <p/>
* This realm functions by inspecting a submitted {@link org.apache.shiro.cas.CasToken CasToken} (which essentially
* wraps a CAS service ticket) and validates it against the CAS server using a configured CAS
* {@link org.jasig.cas.client.validation.TicketValidator TicketValidator}.
* <p/>
/**
* Authenticates a user and retrieves its information.
*
* @param token the authentication token
* @throws AuthenticationException if there is an error during authentication.
*/
@Override
@SuppressWarnings("unchecked")
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
CasToken casToken = (CasToken) token;
if (token == null) {
return null;
} String ticket = (String)casToken.getCredentials();
if (!StringUtils.hasText(ticket)) {
return null;
} TicketValidator ticketValidator = ensureTicketValidator(); try {
// contact CAS server to validate service ticket
Assertion casAssertion = ticketValidator.validate(ticket, getCasService());
// get principal, user id and attributes
AttributePrincipal casPrincipal = casAssertion.getPrincipal();
String userId = casPrincipal.getName();
log.debug("Validate ticket : {} in CAS server : {} to retrieve user : {}", new Object[]{
ticket, getCasServerUrlPrefix(), userId
}); Map<String, Object> attributes = casPrincipal.getAttributes();
// refresh authentication token (user id + remember me)
casToken.setUserId(userId);
String rememberMeAttributeName = getRememberMeAttributeName();
String rememberMeStringValue = (String)attributes.get(rememberMeAttributeName);
boolean isRemembered = rememberMeStringValue != null && Boolean.parseBoolean(rememberMeStringValue);
if (isRemembered) {
casToken.setRememberMe(true);
}
// create simple authentication info
List<Object> principals = CollectionUtils.asList(userId, attributes);
PrincipalCollection principalCollection = new SimplePrincipalCollection(principals, getName());
return new SimpleAuthenticationInfo(principalCollection, ticket);
} catch (TicketValidationException e) {
throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e);
}
} /**
* Retrieves the AuthorizationInfo for the given principals (the CAS previously authenticated user : id + attributes).
*
* @param principals the primary identifying principals of the AuthorizationInfo that should be retrieved.
* @return the AuthorizationInfo associated with this principals.
*/
@Override
@SuppressWarnings("unchecked")
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// retrieve user information
SimplePrincipalCollection principalCollection = (SimplePrincipalCollection) principals;
List<Object> listPrincipals = principalCollection.asList();
Map<String, String> attributes = (Map<String, String>) listPrincipals.get();
// create simple authorization info
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// add default roles
addRoles(simpleAuthorizationInfo, split(defaultRoles));
// add default permissions
addPermissions(simpleAuthorizationInfo, split(defaultPermissions));
// get roles from attributes
List<String> attributeNames = split(roleAttributeNames);
for (String attributeName : attributeNames) {
String value = attributes.get(attributeName);
addRoles(simpleAuthorizationInfo, split(value));
}
// get permissions from attributes
attributeNames = split(permissionAttributeNames);
for (String attributeName : attributeNames) {
String value = attributes.get(attributeName);
addPermissions(simpleAuthorizationInfo, split(value));
}
return simpleAuthorizationInfo;
}

JdbcRealm的部分源码:

  /**
* The default query used to retrieve account data for the user.
*/
protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?"; /**
* The default query used to retrieve account data for the user when {@link #saltStyle} is COLUMN.
*/
protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?"; /**
* The default query used to retrieve the roles that apply to a user.
*/
protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?"; /**
* The default query used to retrieve permissions that apply to a particular role.
*/
protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?"; 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)[];
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[];
salt = queryResults[];
break;
case EXTERNAL:
password = getPasswordForUser(conn, username)[];
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) {
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;
} /**
* This implementation of the interface expects the principals collection to return a String username keyed off of
* this realm's {@link #getName() name}
*
* @see #getAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)
*/
@Override
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; } protected Set<String> getRoleNamesForUser(Connection conn, String username) throws SQLException {
PreparedStatement ps = null;
ResultSet rs = null;
Set<String> roleNames = new LinkedHashSet<String>();
try {
ps = conn.prepareStatement(userRolesQuery);
ps.setString(, username); // Execute query
rs = ps.executeQuery(); // Loop over results and add each returned role to a set
while (rs.next()) { String roleName = rs.getString(); // Add the role to the list of names if it isn't null
if (roleName != null) {
roleNames.add(roleName);
} else {
if (log.isWarnEnabled()) {
log.warn("Null role name found while retrieving role names for user [" + username + "]");
}
}
}
} finally {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(ps);
}
return roleNames;
} protected Set<String> getPermissions(Connection conn, String username, Collection<String> roleNames) throws SQLException {
PreparedStatement ps = null;
Set<String> permissions = new LinkedHashSet<String>();
try {
ps = conn.prepareStatement(permissionsQuery);
for (String roleName : roleNames) { ps.setString(, roleName); ResultSet rs = null; try {
// Execute query
rs = ps.executeQuery(); // Loop over results and add each returned role to a set
while (rs.next()) { String permissionString = rs.getString(); // Add the permission to the set of permissions
permissions.add(permissionString);
}
} finally {
JdbcUtils.closeResultSet(rs);
} }
} finally {
JdbcUtils.closeStatement(ps);
} return permissions;
}

AuthenticationToken

shiro的Realm-LMLPHP

AuthenticationToken用于收集用户提交的身份(如用户名)及凭据(如密码):

public interface AuthenticationToken extends Serializable {
Object getPrincipal(); //身份
Object getCredentials(); //凭据
}

扩展接口RememberMeAuthenticationToken:提供了“boolean isRememberMe()”现“记住我”的功能;

扩展接口是HostAuthenticationToken:提供了“String getHost()”方法用于获取用户“主机”的功能。

Shiro提供了一个直接拿来用的UsernamePasswordToken,用于实现用户名/密码Token组,另外其实现了RememberMeAuthenticationToken和HostAuthenticationToken,可以实现记住我及主机验证的支持。

AuthenticationInfo

shiro的Realm-LMLPHP

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

AuthenticationInfo有两个作用:

1、如果Realm是AuthenticatingRealm子类,则提供给AuthenticatingRealm内部使用的CredentialsMatcher进行凭据验证;(如果没有继承它需要在自己的Realm中自己实现验证);

2、提供给SecurityManager来创建Subject(提供身份信息);

CredentialsMatcher:

public interface CredentialsMatcher {
boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info);
}

它的实现:

 public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenCredentials = getCredentials(token);
Object accountCredentials = getCredentials(info);
return equals(tokenCredentials, accountCredentials);
}

PrincipalCollection

shiro的Realm-LMLPHP

因为我们可以在Shiro中同时配置多个Realm,所以呢身份信息可能就有多个;因此其提供了PrincipalCollection用于聚合这些身份信息:

public interface PrincipalCollection extends Iterable, Serializable {
Object getPrimaryPrincipal(); //得到主要的身份
<T> T oneByType(Class<T> type); //根据身份类型获取第一个
<T> Collection<T> byType(Class<T> type); //根据身份类型获取一组
List asList(); //转换为List
Set asSet(); //转换为Set
Collection fromRealm(String realmName); //根据Realm名字获取
Set<String> getRealmNames(); //获取所有身份验证通过的Realm名字
boolean isEmpty(); //判断是否为空
}

因为PrincipalCollection聚合了多个,此处最需要注意的是getPrimaryPrincipal,如果只有一个Principal那么直接返回即可,如果有多个Principal,则返回第一个(因为内部使用Map存储,所以可以认为是返回任意一个);

AuthorizationInfo

shiro的Realm-LMLPHP

AuthorizationInfo用于聚合授权信息的:

public interface AuthorizationInfo extends Serializable {
Collection<String> getRoles(); //获取角色字符串信息
Collection<String> getStringPermissions(); //获取权限字符串信息
Collection<Permission> getObjectPermissions(); //获取Permission对象信息
}

当我们使用AuthorizingRealm时,如果身份验证成功,在进行授权时就通过doGetAuthorizationInfo方法获取角色/权限信息用于授权验证。

Shiro提供了一个实现SimpleAuthorizationInfo,大多数时候使用这个即可。

摘自:第六章 Realm及相关对象——《跟我学Shiro》

05-11 22:18