閱讀源碼有助於陶冶情操,本文旨在簡單的分析shiro在Spring中的使用
簡單介紹
Shiro是一個強大易用的Java安全框架,提供了認證、授權、加密和會話管理等功能
AuthorizingRealm的繼承關系
Realm->CachingRealm->AuthenticatingRealm->AuthorizingRealm
本文只針對以上關系進行講解,其余的實現類請自行查看源碼
Realm接口類
Realm提供了安全的訪問應用的相關實體類,比如用戶、角色、權限,對其中的訪問應用相應的認證或者授權操作。其提供的主要的方法為AuthenticationInfo#getAuthenticationInfo,涉及的內容是關於信息的認證,這主要由AuthencatingRealm類實現
CachingRealm抽象類
提供緩存功能,簡單看下其下的變量以及主要方法。
- 相關變量如下
//設置是否允許緩存,構造函數中默認為true
private boolean cachingEnabled;
//設置緩存管理器,需要另外引入
private CacheManager cacheManager;
- 主要方法
- CachingRealm#afterCacheManagerSet
默認實現為空,供子類調用,在設置cacheManager變量調用此方法 - CachingRealm#getAvailablePrincipal
//獲取principal對象,一般都是子類在執行授權操作賦予的
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;
}
AuthenticatingRealm-驗證Realm抽象類
簡單分析下其主要私有變量以及方法
- 主要參數
//與父類的cachingEnabled結合使用,在構造函數里其默認是false
private boolean authenticationCachingEnabled;
//用戶憑證驗證類,比如校驗密碼是否一致
private CredentialsMatcher credentialsMatcher;
//認證Token類,默認為UsernamePasswordToken類
private Class<? extends AuthenticationToken> authenticationTokenClass;
- 主要方法
- AuthenticatingRealm#setAuthenticationCachingEnabled-設置緩存是否許可
//可見如果只設置authenticationCachingEnable,其為true,也會設置cachingEnable=true
public void setAuthenticationCachingEnabled(boolean authenticationCachingEnabled) {
this.authenticationCachingEnabled = authenticationCachingEnabled;
if (authenticationCachingEnabled) {
setCachingEnabled(true);
}
}
- AuthenticatingRealm#isAuthenticationCachingEnabled
//可見是否應用緩存是根據authenticationCachingEnabled和cachingEnabled聯合判斷的
public boolean isAuthenticationCachingEnabled() {
return this.authenticationCachingEnabled && isCachingEnabled();
}
- AuthenticatingRealm#getAuthenticationInfo
認證接口實現方法,該方法的回調一般是通過subject.login(token)方法來實現的
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//獲取緩存中的認證信息,其中也會涉及到調用isAuthenticationCachingEnabled
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//otherwise not cached, perform the lookup:
//調用doGetAuthenticationInfo方法,此處為抽象類,供子類調用
info = doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
cacheAuthenticationInfoIfPossible(token, info);
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
if (info != null) {
//對獲取的認證信息進行校驗,一般是比對憑證加密后是否還一致
assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info;
}
- AuthenticatingRealm#doGetAuthenticationInfo
獲取認證信息方法,抽象方法供子類實現
protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException ;
- AuthenticatingRealm#assertCredentialsMatch
對憑證信息的校驗,涉及到加密方式
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
//獲取私有屬性credentialsMatcher
CredentialsMatcher cm = getCredentialsMatcher();
if (cm != null) {
//校驗方法
if (!cm.doCredentialsMatch(token, info)) {
//not successful - throw an exception to indicate this:
String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
throw new IncorrectCredentialsException(msg);
}
} else {
//可見credentialsMatcher屬性必須要設定,否則會拋異常
throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " +
"credentials during authentication. If you do not wish for credentials to be examined, you " +
"can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");
}
}
AuthorizingRealm-授權抽象類
其中的內容和其父類驗證抽象類基本相似,這里就不贅述了,主要提下主要方法
- AuthorizingRealm#getAuthorizationInfo
//授權
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
if (principals == null) {
return null;
}
AuthorizationInfo info = null;
if (log.isTraceEnabled()) {
log.trace("Retrieving AuthorizationInfo for principals [" + principals + "]");
}
//是否引用cache
Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();
if (cache != null) {
if (log.isTraceEnabled()) {
log.trace("Attempting to retrieve the AuthorizationInfo from cache.");
}
Object key = getAuthorizationCacheKey(principals);
info = cache.get(key);
if (log.isTraceEnabled()) {
if (info == null) {
log.trace("No AuthorizationInfo found in cache for principals [" + principals + "]");
} else {
log.trace("AuthorizationInfo found in cache for principals [" + principals + "]");
}
}
}
if (info == null) {
// Call template method if the info was not found in a cache
//調用授權抽象方法,供子類實現
info = doGetAuthorizationInfo(principals);
// If the info is not null and the cache has been created, then cache the authorization info.
if (info != null && cache != null) {
if (log.isTraceEnabled()) {
log.trace("Caching authorization info for principals: [" + principals + "].");
}
Object key = getAuthorizationCacheKey(principals);
cache.put(key, info);
}
}
return info;
}
- AuthorizingRealm#doGetAuthorizationInfo
真實授權抽象方法,供子類調用
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);
示例
下述示例結合了授權與驗證,即繼承AuthorizingRealm即可
- spring-shiro配置文件樣例
<!--緩存管理器-->
<bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"></bean>
<!-- 憑證匹配器 -->
<bean id="credentialsMatcher" class="com.jing.test.cas.admin.core.shiro.RetryLimitHashedCredentialsMatcher">
<constructor-arg ref="cacheManager"/>
<property name="hashAlgorithmName" value="md5"/>
<property name="hashIterations" value="2"/>
<property name="storedCredentialsHexEncoded" value="true"/>
</bean>
<!--密碼處理類-->
<bean id="passwordService" class="com.jing.test.cas.admin.core.shiro.ShiroPasswordService"></bean>
<!-- Realm實現 -->
<bean id="shiroDBRealm" class="com.jing.test.cas.admin.core.shiro.ShiroDBRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"/>
<property name="cachingEnabled" value="false"/>
<property name="shiroPasswordService" ref="passwordService"/>
<property name="shiroUserService" ref="shiroUserService"/>
</bean>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="shiroDBRealm"/>
<property name="sessionManager" ref="sessionManager"/>
</bean>
<!-- 相當於調用SecurityUtils.setSecurityManager(securityManager) -->
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
<property name="arguments" ref="securityManager"/>
</bean>
<!-- Shiro的Web過濾器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login"/>
<property name="successUrl" value="/index.html"/>
<property name="unauthorizedUrl" value="/403.html"/>
<property name="filters">
<util:map>
<entry key="authc" value-ref="formAuthenticationFilter"/>
<entry key="perm" value-ref="permissionsAuthorizationFilter"/>
<entry key="captcha" value-ref="captchaValidateFilter"/>
<entry key="sysUser" value-ref="sysUserFilter"/>
<entry key="user" value-ref="userFilter"/>
</util:map>
</property>
<property name="filterChainDefinitions">
<!-- 如果不應用acm cas方案則添加 /logout=logout -->
<value>
/ossmanager/api/** = anon
/test/** = anon
/login = captcha,authc
/logout=logout
/index = anon
/jcaptcha.jpeg = anon
/403.html = anon
/login.html = anon
/favicon.ico = anon
/static/** = anon
/index.html=user,sysUser
/welcome.html=user,sysUser
/admin/user/modifyPwd.html=user,sysUser
/admin/user/updatePassword=user,sysUser
/admin/user/role/list=user,sysUser
/** = user,sysUser,perm
</value>
</property>
</bean>
<!-- Shiro生命周期處理器-->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
- Realm接口實現類
package com.jing.test.cas.admin.core.shiro;
import com.jing.test.cas.admin.core.exceptions.BizException;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.Set;
/**
*Realm接口實現類
*/
public class ShiroDBRealm extends AuthorizingRealm {
/**
* 日志對象
*/
private static final Logger logger = LoggerFactory.getLogger(ShiroDBRealm.class);
/**
* 賬戶禁用
*/
private static final String USER_STATUS_FORBIDDEN = "1";
/**
* 權限相關用戶服務接口
*/
private IShiroUserService shiroUserService;
/**
* 密碼服務類 加密作用
*/
private ShiroPasswordService shiroPasswordService;
/**
* 授權
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 因為非正常退出,即沒有顯式調用 SecurityUtils.getSubject().logout()
// (可能是關閉瀏覽器,或超時),但此時緩存依舊存在(principals),所以會自己跑到授權方法里。
if (!SecurityUtils.getSubject().isAuthenticated()) {
doClearCache(principalCollection);
SecurityUtils.getSubject().logout();
return null;
}
ShiroUser shiroUser = (ShiroUser)principalCollection.getPrimaryPrincipal();
String userName = shiroUser.getUserName();
if(StringUtils.isNotBlank(userName)){
SimpleAuthorizationInfo sazi = new SimpleAuthorizationInfo();
try {
Set<String> roleIds= shiroUserService.getRoles(userName);
for (String roleId: roleIds){
shiroUser.setRoleId(roleId);
}
sazi.addRoles(roleIds);
sazi.addStringPermissions(shiroUserService.getPermissions(userName));
return sazi;
} catch (Exception e) {
logger.error(e.getMessage(),e);
}
}
return null;
}
/**
* 認證
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
ShiroUser shiroUser = shiroUserService.getShiroUser(token.getUsername());
checkUserStatus(shiroUser);
if(shiroUser != null){
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(shiroUser,shiroUser.getPassword(),ByteSource.Util.bytes(shiroUser.getUserName()+shiroPasswordService.getPublicSalt()),getName());
return authenticationInfo;
}
return null;
}
/**
* 檢查用戶狀態
* @param shiroUser
*/
private void checkUserStatus(ShiroUser shiroUser) {
if(StringUtils.equalsIgnoreCase(shiroUser.getUserStatus(),USER_STATUS_FORBIDDEN)){
throw new ForbiddenException("用戶已被禁用");
}
}
/**
* 初始化方法
* 設定Password校驗的Hash算法與迭代次數.
**/
public void initCredentialsMatcher() {
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(
shiroPasswordService.getHashAlgorithm());
matcher.setHashIterations(shiroPasswordService.getHashInterations());
setCredentialsMatcher(matcher);
}
public void setShiroUserService(IShiroUserService shiroUserService) {
this.shiroUserService = shiroUserService;
}
public void setShiroPasswordService(ShiroPasswordService shiroPasswordService) {
this.shiroPasswordService = shiroPasswordService;
}
}
總結
授權與驗證的邏輯做了簡單的了解,其中授權可通過subject.isPermited()等方法調用;驗證則可通過subject.login()等方法來調用
