Shiro的几个关键类

  • 2019 年 10 月 3 日
  • 筆記

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