Shiro在于Spring集成中,需要配置SecurityManager,Realm,ShiroFilterFactoryBean这三个类。在Web环境中SecurityManager一般配置DefaultWebSecurityManager,如果需要扩展或者定制一些额外的功能,可以配置DefaultWebSecurityManager的继承类;Realm需要先继承AuthorizingRealm抽象类再配置,如果有多个Realm的话,还需要配置ModularRealmAuthenticator的继承实现类;ShiroFilterFactoryBean主要是提供ShiroFilter,可以配置一些资源的拦截。下面对一些核心类进行一下总结。
该类继承了三个接口,还额外提供登录,退出和创建用户的功能。
/** * 所有与安全有关的操作都会与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:域,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; }
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); } } } } }
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是用户访问应用时保持的连接关系,在多次交互中应用能够识别出当前访问的用户是谁,且可以在多次交互中保存一些数据。如访问一些网站时登录成功后,网站可以记住用户,且在退出之前都可以识别当前用户是谁。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)
的方式。