Shiro在於Spring集成中,需要配置SecurityManager,Realm,ShiroFilterFactoryBean這三個類。在Web環境中SecurityManager一般配置DefaultWebSecurityManager,如果需要擴展或者定制一些額外的功能,可以配置DefaultWebSecurityManager的繼承類;Realm需要先繼承AuthorizingRealm抽象類再配置,如果有多個Realm的話,還需要配置ModularRealmAuthenticator的繼承實現類;ShiroFilterFactoryBean主要是提供ShiroFilter,可以配置一些資源的攔截。下面對一些核心類進行一下總結。
SecurityManager
該類繼承了三個接口,還額外提供登錄,退出和創建用戶的功能。
/**
* 所有與安全有關的操作都會與SecurityManager交互
* 擴展了authenticator、authorizer和sessionmanager接口
*/
public interface SecurityManager extends Authenticator, Authorizer, SessionManager {
Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;
void logout(Subject subject);
Subject createSubject(SubjectContext context);
}
/**
* 認證驗證,登錄校驗
*
*/
public interface Authenticator {
/**
* AuthenticationToken 登錄未驗證的數據
* AuthenticationInfo 身份驗證/登錄過程相關的帳戶信息。
*
*/
public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)
throws AuthenticationException;
}
/**
* 用戶授權,權限校驗
*
*/
public interface Authorizer {
boolean[] isPermitted(PrincipalCollection subjectPrincipal, String... permissions);
boolean[] isPermitted(PrincipalCollection subjectPrincipal, List<Permission> permissions);
boolean isPermittedAll(PrincipalCollection subjectPrincipal, String... permissions);
boolean isPermittedAll(PrincipalCollection subjectPrincipal, Collection<Permission> permissions);
void checkPermissions(PrincipalCollection subjectPrincipal, String... permissions) throws AuthorizationException;
void checkPermissions(PrincipalCollection subjectPrincipal, Collection<Permission> permissions) throws AuthorizationException;
boolean hasRole(PrincipalCollection subjectPrincipal, String roleIdentifier);
boolean[] hasRoles(PrincipalCollection subjectPrincipal, List<String> roleIdentifiers);
boolean hasAllRoles(PrincipalCollection subjectPrincipal, Collection<String> roleIdentifiers);
void checkRole(PrincipalCollection subjectPrincipal, String roleIdentifier) throws AuthorizationException;
void checkRoles(PrincipalCollection subjectPrincipal, Collection<String> roleIdentifiers) throws AuthorizationException;
void checkRoles(PrincipalCollection subjectPrincipal, String... roleIdentifiers) throws AuthorizationException;
}
/**
* 會話管理
*/
public interface SessionManager {
/**
* 基於指定的上下文初始化數據啟動一個新Session,Session通常交由SessionFactory創建
*
*/
Session start(SessionContext context);
/**
* 通過SessionKey查找Session
*
*/
Session getSession(SessionKey key) throws SessionException;
}
SecurityManager的Web部分源代碼實現如下所示。從默認的構造器可以看到在創建SecurityManager的該實現時,會設置一系列默認的值,如ServletContainerSessionManager,CookieRememberMeManager等。而isHttpSessionMode方法判斷是否是HttpSession,還是自己實現的Session。
public class DefaultWebSecurityManager extends DefaultSecurityManager implements WebSecurityManager {
public DefaultWebSecurityManager() {
super();
DefaultWebSessionStorageEvaluator webEvalutator = new DefaultWebSessionStorageEvaluator();
((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(webEvalutator);
this.sessionMode = HTTP_SESSION_MODE;
setSubjectFactory(new DefaultWebSubjectFactory());
setRememberMeManager(new CookieRememberMeManager());
setSessionManager(new ServletContainerSessionManager());
webEvalutator.setSessionManager(getSessionManager());
}
public boolean isHttpSessionMode() {
SessionManager sessionManager = getSessionManager();
return sessionManager instanceof WebSessionManager && ((WebSessionManager)sessionManager).isServletContainerSessions();
}
...
}
以下是SecurityManager中實現SessionManager接口的實現類,從中可以看到SecurityManager並沒有實際處理SessionManager接口的方法,而是采用組合模式,將實際的SessionManager作為SecurityManager的成員變量,實際處理還是交由sessionManager來處理。而且在SessionManager初始化完默認的DefaultSessionManager后(在新繼承的DefaultWebSecurityManager的類中,為ServletContainerSessionManager)后,如果SessionManager實現CacheManagerAware接口,則會將CacheManager也一同設置到SessionManager中。
public abstract class SessionsSecurityManager extends AuthorizingSecurityManager {
private SessionManager sessionManager;
public SessionsSecurityManager() {
super();
this.sessionManager = new DefaultSessionManager();
applyCacheManagerToSessionManager();
}
protected void applyCacheManagerToSessionManager() {
if (this.sessionManager instanceof CacheManagerAware) {
((CacheManagerAware) this.sessionManager).setCacheManager(getCacheManager());
}
}
public void setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
afterSessionManagerSet();
}
protected void afterSessionManagerSet() {
applyCacheManagerToSessionManager();
applyEventBusToSessionManager();
}
public SessionManager getSessionManager() {
return this.sessionManager;
}
public Session start(SessionContext context) throws AuthorizationException {
return this.sessionManager.start(context);
}
public Session getSession(SessionKey key) throws SessionException {
return this.sessionManager.getSession(key);
}
}
從SecurityManager的繼承體系來看,每次的繼承都會添加一個成員變量,並且對外公開的方法也是由該成員來處理。所以現在來看,SecurityManager是通過繼承體系和組合的模式,來充實它的實際功能,並且將Shiro的各個組件都聯系到了一起。SecurityManager是線程安全且真個應用只需要一個即可,因此Shiro提供了SecurityUtils讓我們綁定它為全局的,方便后續操作。
Realm
Realm:域,Shiro從從Realm獲取安全數據(如用戶、角色、權限),就是說 SecurityManager要驗證用戶身份,那么它需要從Realm獲取相應的用戶進行比較以確定用戶身份是否合法;也需要從Realm得到用戶相應的角色/權限進行驗證用戶是否能進行操作;可以把Realm看成 DataSource,即安全數據源。 通常由程序實現AuthorizingRealm類,如果有多個實現,還需要重寫ModularRealmAuthenticator的doAuthenticate的方法,來指定Realm對應處理的AuthenticationToken。另外AuthorizingRealm提供設置緩存,加密和權限的相關功能。
public interface Realm {
/**
* 返回應用中Realm的唯一名字
*/
String getName();
/**
* 多Realm中,該Realm是否匹配AuthenticationToken
*/
boolean supports(AuthenticationToken token);
/**
* 依據未認證的AuthenticationToken,返回認證后的AuthenticationInfo
*/
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
}
ModularRealmAuthenticator
Authenticator的功能是驗證用戶帳號,是Shiro API中身份認證核心的入口點。如果驗證成功,將返回 AuthenticationInfo驗證信息;此信息中包含了身份及憑證;如果驗證失敗將拋出相應的 AuthenticationException實現異常。它的默認實現類是ModularRealmAuthenticator,可以從這個類中看到校驗的整個流程,而且還提供了AuthenticationListener來監聽認證的過程(主要有登錄成功事件,登錄失敗事件和退出事件)。
public class ModularRealmAuthenticator extends AbstractAuthenticator {
private static final Logger log = LoggerFactory.getLogger(ModularRealmAuthenticator.class);
private Collection<Realm> realms;
private AuthenticationStrategy authenticationStrategy;
public ModularRealmAuthenticator() {
this.authenticationStrategy = new AtLeastOneSuccessfulStrategy();
}
public void setRealms(Collection<Realm> realms) {
this.realms = realms;
}
protected Collection<Realm> getRealms() {
return this.realms;
}
public AuthenticationStrategy getAuthenticationStrategy() {
return authenticationStrategy;
}
public void setAuthenticationStrategy(AuthenticationStrategy authenticationStrategy) {
this.authenticationStrategy = authenticationStrategy;
}
protected void assertRealmsConfigured() throws IllegalStateException {
Collection<Realm> realms = getRealms();
if (CollectionUtils.isEmpty(realms)) {
String msg = "Configuration error: No realms have been configured! One or more realms must be " +
"present to execute an authentication attempt.";
throw new IllegalStateException(msg);
}
}
/**
* 單Realm的校驗,最后調用realm.getAuthenticationInfo方法來通過Realm校驗正確性。
*/
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
String msg = "Realm [" + realm + "] does not support authentication token [" +
token + "]. Please ensure that the appropriate Realm implementation is " +
"configured correctly or that the realm accepts AuthenticationTokens of this type.";
throw new UnsupportedTokenException(msg);
}
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
String msg = "Realm [" + realm + "] was unable to find account data for the " +
"submitted AuthenticationToken [" + token + "].";
throw new UnknownAccountException(msg);
}
return info;
}
/**
* 多Realm的校驗,還需要考慮認證的策略(全部成功,至少一個成功)
*/
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
AuthenticationStrategy strategy = getAuthenticationStrategy();
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
if (log.isTraceEnabled()) {
log.trace("Iterating through {} realms for PAM authentication", realms.size());
}
for (Realm realm : realms) {
aggregate = strategy.beforeAttempt(realm, token, aggregate);
if (realm.supports(token)) {
log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
AuthenticationInfo info = null;
Throwable t = null;
try {
info = realm.getAuthenticationInfo(token);
} catch (Throwable throwable) {
t = throwable;
if (log.isDebugEnabled()) {
String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
log.debug(msg, t);
}
}
aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
} else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;
}
/**
* 多Realm的校驗,還需要考慮認證的策略(全部成功,至少一個成功)
*/
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
public void onLogout(PrincipalCollection principals) {
super.onLogout(principals);
Collection<Realm> realms = getRealms();
if (!CollectionUtils.isEmpty(realms)) {
for (Realm realm : realms) {
if (realm instanceof LogoutAware) {
((LogoutAware) realm).onLogout(principals);
}
}
}
}
}
SessionManager
SecurityManager提供了如下接口,另外用於 Web 環境的 WebSessionManager又提供了如下接口,判斷是否是Servlet容器的Session,還是自己維護Session。SecurityManager管理着應用中所有Subject的會話的創建、維護、刪除、失效、驗證等工作。
public interface SessionManager {
/**
* 啟動會話
*/
Session start(SessionContext context);
Session getSession(SessionKey key) throws SessionException;
}
public interface WebSecurityManager extends SecurityManager {
boolean isHttpSessionMode();
}
在web環境中,如果用戶不主動退出是不知道會話是否過期的,因此需要定期的檢測會話是否過期,Shiro 提供了會話驗證調度器SessionValidationScheduler來做這件事情。SecurityManager的實現類中一般都實現了該接口。
Shiro提供了SessionManager的三個默認實現:
-
DefaultSessionManager:DefaultSecurityManager 使用的默認實現,用於JavaSE環境
-
ServletContainerSessionManager:DefaultWebSecurityManager使用的默認實現,用於 Web環境,其直接使用Servlet容器的會話;
-
DefaultWebSessionManager :用於Web環境的實現,可以代替ServletContainerSessionManager,自己維護着會話,直接廢棄了 Servlet 容器的會話管理。
Session
Session是用戶訪問應用時保持的連接關系,在多次交互中應用能夠識別出當前訪問的用戶是誰,且可以在多次交互中保存一些數據。如訪問一些網站時登錄成功后,網站可以記住用戶,且在退出之前都可以識別當前用戶是誰。Shiro的會話支持不僅可以在普通的JavaSE應用中使用,也可以在JavaEE應用中使用,如web應用。且使用方式是一致的 。
public interface Session {
Serializable getId();
Date getStartTimestamp();
Date getLastAccessTime();
long getTimeout() throws InvalidSessionException;
void setTimeout(long maxIdleTimeInMillis) throws InvalidSessionException;
String getHost();
/**
* 如果是 JavaSE 應用需要自己定期調用 session.touch()去更新最后訪問時間;
* 如果是 Web 應用,每次進入 ShiroFilter 都會自動調用 session.touch()來更新最后訪問時間
*/
void touch() throws InvalidSessionException;
/**
* 當Subject.logout()時會自動調用 stop 方法來銷毀會話
*/
void stop() throws InvalidSessionException;
Collection<Object> getAttributeKeys() throws InvalidSessionException;
Object getAttribute(Object key) throws InvalidSessionException;
void setAttribute(Object key, Object value) throws InvalidSessionException;
Object removeAttribute(Object key) throws InvalidSessionException;
}
Session提供了監聽器SessionListener,用於監聽會話創建、過期及停止事件,如果只想監聽某一個事件,可以繼承SessionListenerAdapter實現。
Shiro提 SessionDAO用於會話的CRUD操作,AbstractSessionDAO提供了 SessionDAO的基礎實現,如生成會話 ID等;CachingSessionDAO提供了對開發者透明的會話緩存的功能,只需要設置相應的 CacheManager 即可;MemorySessionDAO直接在內存中進行會話維護;而EnterpriseCacheSessionDAO提供了緩存功能的會話維護,但是都是空方法,需要繼承實現這些方法。
總結
Shiro與Spring的整合中,很多對Shiro的功能擴展,都需要繼承原來的類,再修改為默認的實現。比如在Web環境中可以自己實現Session管理,就需要在SecurityManager中調用setSessionManager()方法,修改默認的SessionManager。
還有在Shiro和Spring整合中碰到了一個問題UserRealm中注入IUserService,導致IUserService的AOP失效(會導致事務失效等),只是查明了原有,解決辦法可以不用注入,改為SpringContextHolder.getBean(IUserService.class)的方式。
