Shiro的幾個關鍵類


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)的方式。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM