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

posted @ 2019-09-01 21:27  fzsyw  阅读(783)  评论(3编辑  收藏  举报