@[toc]
注意:此文章可以搭配着shiro注释翻译后的源码进行阅读:项目地址:https://gitee.com/mr1ght/shiro-traslate.git
一. Session 用户会话
1.1 Session 有状态数据上下文
Session是与单个Subject(用户、守护进程等,该Subject在一段时间内与软件系统交互)相关联的有状态数据上下文。
会话由业务层管理,并可通过其他层访问,而无需绑定到任何给定的客户端技术。 这对Java系统来说是一个巨大的好处,因为到目前为止,唯一可行的会话机制是javax.servlet.http.HttpSession或有状态会话EJB的,它们常常不必要地将应用程序与web或EJB技术耦合在一起。
Session的功能类似HttpSession。Session
的子类中使用了很多代理模式,很多都是通过其他来实现session管理。主要属性有id
、过期时间、上次访问时间(LastAccessTime)、session创建时间(StartTimestamp)、用户主机信息以及一个属性(attribute)等等
-
DelegatingSession:
DelegatingSession
是服务器端Session的客户端表示。 这个实现基本上是服务器端NativeSessionManager
的代理,内部封装了一个NativeSessionManager
,DelegatingSession
的操作基本都是调用NativeSessionManager
的方法。 -
ProxiedSession:内部存储了另一个
Session
,ProxiedSession
的操作基本都是基于封装的这个Session
-
ImmutableProxiedSession:实现了
ProxiedSession
并不允许使用父类的写方法,调用写方法就会报错。 -
StoppingAwareProxiedSession:这是
DelegatingSubject
的内部类,除了像父类一样内部封装了一个Session
,还封装了一个DelegatingSubject
类。
-
ImmutableProxiedSession:实现了
-
HttpServletSession:完全由标准
servlet
容器HttpSession
实例支持的Session
实现。 它不与Shiro
的任何与会话相关的组件SessionManager
、SecurityManager
等进行交互,而是通过与servlet
容器提供的HttpSession
实例进行交互来满足所有方法实现。
1.2 SessionContext、MapContext、SubjectContext
-
MapContext:内部维护了一个Map,其方法都是在操作这个内部的Map,并且新增了
getTypedValue
方法,以获取对应类型的value。
protected <E> E getTypedValue(String key, Class<E> type) {
E found = null;
Object o = backingMap.get(key);
if (o != null) {
if (!type.isAssignableFrom(o.getClass())) {
String msg = "Invalid object found in SubjectContext Map under key [" + key + "]. Expected type " +
"was [" + type.getName() + "], but the object under that key is of type " +
"[" + o.getClass().getName() + "].";
throw new IllegalArgumentException(msg);
}
found = (E) o;
}
return found;
}
-
SessionContext:SessionContext是一个提供给
SessionFactory
的数据“桶”,SessionFactory
解释这些数据来构造Session
实例。 它本质上是一个数据的Map,带有一些额外的类型安全方法,用于轻松检索通常用于构造Subject
实例的对象。 -
SubjectContext: SubjectContext是一个提交给
SecurityManager
的数据“桶”,SecurityManager
解释该数据来构造Subject
实例。 它本质上是一个数据的Map ,带有一些额外的类型安全方法,用于轻松检索通常用于构造Subject
实例的对象。这个接口的resolveXXX方法是对普通方法的拓展,在实际构造Subject实例时,应该使用resolveXXX方法,以确保可以使用最具体/最准确的数据 - DefaultSubjectContext: SubjectContext接口的默认实现。 注意,getter和setter不是简单的传递底层属性的方法; getter将雇用大量的启发式获得尽可能最好的数据属性(例如,如果getPrincipals被调用时,如果principals不在backingMap中,它会检查是否有一个Subject或Session在map中并从这些对象中获取principals )。
- DefaultWebSessionContext: WebSessionContext接口的默认实现,它提供了将交互包装到底层后台上下文映射的getter和setter方法。
1.3 EventBus、EventListener、EventListenerResolver
-
EventListener: 这是一个事件侦听器,它知道如何接受和处理特定类型(或多个类型)的事件。是事件监听最后真正的执行者。提供了
accepts
(是否支持这个事件)和onEnvent
(执行事件)两个方法-
TypedEventListener:有一个
getEventType
方法供子类实现,作用是获取要通知的类的方法的参数类型 -
SingleArgumentMethodEventListener:
EventListener
的默认实现。只接受方法为public
修饰并只有一个参数的事件。
-
TypedEventListener:有一个
-
**EventListenerResolver **:EventListenerResolver知道如何解析(创建或查找)EventListener实例作为检查订阅者对象(很可能是Subscribe注解的对象实例)的结果。
-
AnnotationEventListenerResolver:这个类可以传入一个注解的class,并且将
getEventListeners
方法的入参传入的类上加了这个注解的方法解析成一个EventListener集合,一般配合Subscription类使用。
-
AnnotationEventListenerResolver:这个类可以传入一个注解的class,并且将
-
EventBus:事件总线,可以向事件订阅者发布事件,也可以提供注册和注销事件订阅者的机制。
-
DefaultEventBus:是
EventBus
的默认实现。其方法加了读写锁,保证了安全。最主要的方法是publish
方法,即事件发布,这个方法主要是遍历其内部类Subscription
集合并调用onEvent
方法实现。 -
Subscription:其内部封装了一个
EventListener
集合,其onEvent
方法就是执行EventListener
内封装的要执行的方法,这样那个方法就被通知了。
-
DefaultEventBus:是
- 如果你想发生某些事件时Shiro会通知到你,那你就可以在你想被通知的方法上加
Subscribe
注解,然后将其传入EventBus
消息总线中,其就会将相关事件发布给你。
1.4 SessionDAO 会话存储
- SessionDAO:数据访问对象设计模式规范,以支持对EIS(企业信息系统)的会话访问。 它提供了四个典型的CRUD方法:create, readSession(Serializable), update(Session)和 delete(Session)。
- AbstractSessionDAO:一个抽象的SessionDAO实现,它对会话的创建和读取执行一些完整性检查,如果需要,还允许可插拔的会话ID生成策略。 SessionDAO的更新和删除方法留给子类。
-
CachingSessionDAO:基于
Cach
或者CacheManager
的DAO,对Session
的操作都是基于Cache
或者CacheManager
的,优点是可以扩展成第三方的持久化机制(例如文件系统、数据库、企业网格/云等)-
EnterpriseCacheSessionDAO:这个类只是实现了
CachingSessionDao
的抽象方法,并且这些方法都没有进行任何操作,其实就是依赖父类的操作即可,这个类没有进行任何操作,需要用户去对Cache
、CacheManager
进行配置和实现。
-
EnterpriseCacheSessionDAO:这个类只是实现了
-
MemorySessionDAO:简单的基于内存的
SessionDAO
实现,它将所有会话存储在内存中的ConcurrentMap
中,此实现不会分页到磁盘,因此不适合可能经历大量会话并因此导致OutOfMemoryException
的应用程序。 在大多数环境中,不建议将其用于生产环境。
1.5 SessionManager 会话管理器
SessionManager
管理所有应用程序会话的创建、维护和清理。SecurityManager
后面再分析。现在主要分析上图的左半部分。
-
SessionManager:顶层接口,提供了两个方法:
Session start(SessionContext context);
(开启会话)和Session getSession(SessionKey key) throws SessionException;
获取会话- NativeSessionManager:本地会话管理器是本地管理会话的管理器。它直接负责创建、持久化和删除会话实例及其生命周期。
-
AbstractSessionManager:这只是为了使一个公共属性
globalSessionTimeout
对子类可用,特别是使它可用于AbstractNativeSessionManager
和ServletContainerSessionManager
子类树。 但是,ServletContainerSessionManager
实现不使用这个值,这意味着只有NativeSessionManager
需要globalSessionTimeout
属性,因此这个类是不必要的。 -
AbstractNativeSessionManager:支持
NativeSessionManager
接口的抽象实现,支持SessionListeners
和globalSessionTimeout
的应用。- 这个Manager封装了一个
EventBus
(消息总线)和SessionListener
(Session监听器),并在开启、停止、过期Session
的时候进行通知。 - 这个
Manager
将具体的开启、获取Session
的方法交给子类处理(具体的方法都是抽象的,需要子类去实现) - 对
Session
本身的方法进行了封装。
- 这个Manager封装了一个
-
ValidatingSessionManager:这个类只提供了一个方法,
void validateSessions();
是用来校验所有存储的Session
是否过期的, 如果发现会话无效(例如已过期),就会将Session
的相关字段更新(例如Session
过期,就会更新expired
并通过SessionDao
进行存储) -
AbstractValidatingSessionManager:对父类方法的进一步实现
- 内部封装了一个
SessionValidationScheduler
以实现ValidatingSessionManager
的validateSessions
方法,原理就是通过SessionValidationScheduler
定时的去查询存储的Session
,并调用每个Session
的validate
方法,将无效的Session
相关字段更新后通过SessionDao
进行更新存储。 - 对
createSession
、doGetSession
等方法进行进一步封装,主要是在这些方法中前置或后置了 启用/关闭SessionValidationScheduler
的方法
- 内部封装了一个
-
DefaultSessionManager:
ValidatingSessionManager
的默认业务层实现。 所有会话CRUD操作都委托给内部SessionDAO
。 -
WebSessionManager:特定于web应用程序的SessionManager。只有一个方法,
boolean isServletContainerSessions();
如果会话管理和存储由底层Servlet容器管理则返回true,如果由Shiro直接管理则返回false(称为“本机”会话)。 -
ServletContainerSessionManager:这个
SessionManager
仅仅是Servlet
容器的HttpSession
的包装器, -
DefaultWebSessionManager:
DefaultWebSessionManager
既支持传统的基于web的访问,也支持非基于web的客户端。
二.Authenticator 认证器
Authenticator负责验证应用程序中的帐户。 它是Shiro API的主要入口点之一。
虽然不是必须的,但通常我们会为应用程序配置一个“master”身份验证器 Authenticator。 启用可插入身份验证模块(PAM)行为(两阶段提交等)通常是由单个 Authenticator协调并与应用程序配置的一组Realm交互实现的。
注意,大多数Shiro用户不会直接与 Authenticator实例交互。 Shiro的默认架构是基于一个完整的 SecurityManager,它通常包装一个 Authenticator实例。
Authenticator
只有一个方法,是用来认证的
/**
*基于提交的{@code AuthenticationToken}验证用户。
*如果认证成功,则返回一个{@link AuthenticationInfo}实例,该实例表示与Shiro相关的用户帐户数据。 这个返回的对象通常被用来构造一个{@code Subject},
*代表一个更完整的安全特定的帐户“视图”,也允许访问{@code Session}。
**/
AuthenticationInfo authenticate(AuthenticationToken authenticationToken)
throws AuthenticationException;
其两个实现类分别是SecurityManager
和AbstractAuthenticator
。
2.1 AbstractAuthenticator 认证器超类
AbstractAuthenticator
是几乎所有{@link Authenticator} 实现的超类,执行围绕认证尝试的常见工作。
< p / >
这个类将实际的身份验证尝试委托给子类,但支持成功、失败登录和注销的通知。 当这些条件发生时,通知被发送到一个或多个已注册的{@link AuthenticationListener AuthenticationListener},以允许自定义处理逻辑。
< p / >
在大多数情况下,子类需要做的唯一一件事(通过它的{@link #doAuthenticate}实现)
为提交的{@code AuthenticationToken}执行实际的主体/凭据验证过程。
这个抽象类里面包含了一个AuthenticationListener
监听器集合,并且在authenticate
方法执行认证时根据认证情况调用notifySuccess
、notifyFailure
、notifyLogout
、onLogout
这几个方法来通知 用户 认证结果。(如果想被通知,就需要自己实现监听器,监听器分析在2.1.1 AuthenticationListener 认证监听器)
2.1.1 AuthenticationListener 认证监听器
这个监听器默认是没有实现类的,留给需要的用户自己去实现,可以实现在用户认证成功、认证失败、登出时通知用户以实现具体功能
2.1.2 authenticate 认证方法
authenticate
主要是调用子类实现的doAuthenticate
方法进行认证。并且通知认证结果
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
if (token == null) {
throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
}
log.trace("Authentication attempt received for token [{}]", token);
AuthenticationInfo info;
try {
//执行认证
info = doAuthenticate(token);
if (info == null) {
String msg = "No account information found for authentication token [" + token + "] by this " +
"Authenticator instance. Please check that it is configured correctly.";
throw new AuthenticationException(msg);
}
} catch (Throwable t) {
AuthenticationException ae = null;
if (t instanceof AuthenticationException) {
ae = (AuthenticationException) t;
}
if (ae == null) {
//Exception thrown was not an expected AuthenticationException. Therefore it is probably a little more
//severe or unexpected. So, wrap in an AuthenticationException, log to warn, and propagate:
String msg = "Authentication failed for token submission [" + token + "]. Possible unexpected " +
"error(Typical or expected login exceptions should extend from AuthenticationException).";
ae = new AuthenticationException(msg, t);
if (log.isWarnEnabled())
log.warn(msg, t);
}
try {
//认证失败,通知用户
notifyFailure(token, ae);
} catch (Throwable t2) {
if (log.isWarnEnabled()) {
String msg = "Unable to send notification for failed authentication attempt - listener error?. " +
"Please check your AuthenticationListener implementation(s). Logging sending exception " +
"and propagating original AuthenticationException instead...";
log.warn(msg, t2);
}
}
throw ae;
}
log.debug("Authentication successful for token [{}]. Returned account [{}]", token, info);
//认证成功,通知用户
notifySuccess(token, info);
return info;
}
//具体的认证逻辑。是一个抽象方法,需要子类去实现
protected abstract AuthenticationInfo doAuthenticate(AuthenticationToken token)
throws AuthenticationException;
2.2 ModularRealmAuthenticator 默认实现的认证类
-
ModularRealmAuthenticator
的认证是依赖于Realms
和AuthenticationStrategy
来完成。你可以配置一个(默认)或多个realm
来执行相应的认证流程。然后通过AuthenticationStrategy
来控制需要一个realm
成功或多个realm
成功或其他策略。大多数情况下我们只需要配置一个realm
即可。 - 这个类重写了
AbstractAuthenticator
的doAuthenticate
方法。
2.3 AuthenticationStrategy 可插拔的Realms交互策略
-
AuthenticationStrategy:
AuthenticationStrategy
实现在可插入领域(PAM)环境的登录过程中协助ModularRealmAuthenticator
。
ModularRealmAuthenticator
将咨询该接口的实现,以确定在与配置的Realms
进行每次交互时该做什么。 这允许一种可插拔的策略,即验证尝试是否必须在所有realm
、只有1个或多个realm
、没有realm
等都成功。 -
AbstractAuthenticationStrategy:
AuthenticationStrategy
实现的抽象基础实现。实现了所有方法,并且奠定了基础逻辑。 -
FirstSuccessfulStrategy:只接受来自第一个成功咨询的
Realm
的帐户数据,而忽略所有后续realm
-
AtLeastOneSuccessfulStrategy:在已配置的
realm
集合中,只要有一个realm
可以同时校验token通过(realm
的supports
方法返回true)并可以获得AuthenticationInfo
令牌,这个策略就会允许其登录 -
AllSuccessfulStrategy:要求所有配置的realm在登录尝试期间成功处理提交的
AuthenticationToken
2.4 AuthenticationInfo (用户帐户信息-账户、密码等) 和 AuthorizationInfo (用户授权数据-角色、权限等)
①区别:AuthenticationInfo
仅表示身份验证尝试期间Shiro需要的帐户数据,而AuthorizationInfo
用于在授权过程中引用角色和权限等访问控制数据。
②AuthenticationInfo
表示仅与身份验证/登录过程相关的Subject(也就是用户)存储的帐户信息。其有三个子接口。分别是Account
、MergableAuthenticationInfo
、SaltedAuthenticationInfo
。
③AuthorizationInfo
表示仅在授权(访问控制)检查期间使用的单个Subject存储的授权数据(角色、权限等)。其有两个子类。SimpleAuthorizationInfo
和Account
④子类介绍
-
Account:
Account
是一个方便的接口,它扩展了AuthenticationInfo
和AuthorizationInfo
,并表示在单个Realm
中的单个帐户的身份验证和授权。 当Realm
实现发现使用单个对象封装authc
和authz
操作使用的身份验证和授权信息更为方便时,此接口可能会很有用。 -
MergableAuthenticationInfo:
AuthenticationInfo
接口的扩展,由支持与其他AuthenticationInfo
实例合并的类实现。 这允许这个类的实例是来自多个Realms
(而不仅仅是一个realm)的帐户数据的聚合或组合。这在多realms
身份验证配置中非常有用——从每个realm
获得的单个AuthenticationInfo
对象可以合并到单个实例中。 然后可以在身份验证过程结束时返回该实例,从而给人留下一个基础realm
/data
的印象。 -
SaltedAuthenticationInfo:表示帐户信息的接口,该帐户信息在对凭证进行散列处理时可能使用
salt
。 这个接口的存在主要是为了支持散列用户凭证(例如密码)的环境。盐类通常应该由一个安全的伪随机数生成器生成,因此它们实际上是不可能猜测的。 盐值应该与帐户信息一起安全地存储,以确保它与帐户凭据一起得到维护。 这个接口的存在是为了让Shiro
获得salt
,以便在尝试登录时正确地执行凭据匹配。 -
SimpleAuthorizationInfo:
AuthorizationInfo
接口的简单POJO
实现,该接口将角色和权限存储为内部属性。主要包含以下三个变量和他们的get
、Set
方法
/**
* The internal roles collection.
*/
protected Set<String> roles;
/**
* Collection of all string-based permissions associated with the account.
*/
protected Set<String> stringPermissions;
/**
* Collection of all object-based permissions associated with the account.
*/
protected Set<Permission> objectPermissions;
-
SimpleAuthenticationInfo:保存主体和凭据的
MergableAuthenticationInfo
接口的简单实现。主要保存以下变量并封装对其的操作。这个类的特性是多个账户的合并。
/**
* The principals identifying the account associated with this AuthenticationInfo instance.
* <pre>
* 标识与此AuthenticationInfo实例关联的帐户的主体。
* </pre>
*/
protected PrincipalCollection principals;
/**
* The credentials verifying the account principals.
* <pre>
* 验证帐户主体的凭据。
* </pre>
*/
protected Object credentials;
/**
* Any salt used in hashing the credentials.
*
* @since 1.1
*/
protected ByteSource credentialsSalt = SimpleByteSource.empty();
-
SimpleAccount:这个类既包括了认证信息,也包括了授权信息,是对用户认证信息和授权信息的总封装。本质上是调对
SimpleAuthenticationInfo
和SimpleAuthorizationInfo
的封装,利用SimpleAuthenticationInfo
来封装认证信息,利用SimpleAuthorizationInfo
来封装授权信息。并新增了locked
(表示该帐户已被锁定)字段和credentialsExpired
(指示此帐户上的凭据已过期)字段。
2.5 PrincipalCollection 与相应Subject关联的所有主体(Principal)的集合
/**
* A collection of all principals associated with a corresponding {@link Subject Subject}. A <em>principal</em> is
* just a security term for an identifying attribute, such as a username or user id or social security number or
* anything else that can be considered an 'identifying' attribute for a {@code Subject}.
* <p/>
* A PrincipalCollection organizes its internal principals based on the {@code Realm} where they came from when the
* Subject was first created. To obtain the principal(s) for a specific Realm, see the {@link #fromRealm} method. You
* can also see which realms contributed to this collection via the {@link #getRealmNames() getRealmNames()} method.
*
* <pre>
* 与相应{@link Subject Subject}关联的所有主体的集合。主体(principal)只是一个用于标识属性的安全术语,例如用户名或用户id或社会保险号或任何其他可以被认为是Subject的标识属性的东西。
* 当主体(subject)第一次被创建时,一个PrincipalCollection内部的principals基于它们来自的realm。要获取特定Realm的主体(subject),请参见{@link #fromRealm}方法。
* 您还可以通过{@link #getRealmNames() getRealmNames()}方法查看哪些realm给这个集合做了贡献。
* </pre>
*
* @see #getPrimaryPrincipal()
* @see #fromRealm(String realmName)
* @see #getRealmNames()
* @since 0.9
*/
public interface PrincipalCollection extends Iterable, Serializable {
/**
* <pre>
* 返回应用程序范围内用于唯一标识所属帐户/Subject的主principal。
* 该值通常是特定于检索帐户数据的数据源的惟一标识属性。一些例子:
* <li>一个{@link java.util.UUID UUID}</li>
* <li>一个{@code long}值,例如关系数据库中的代理主键</li>
* <li>LDAP UUID或静态DN</li>
* <li>一个字符串用户名唯一的所有用户帐户</li>
* <h3>Multi-Realm应用程序</h3>
* 在单个realm应用程序中,通常只保留一个惟一的主体,即此方法返回的值。然而,在多realm应用程序中,PrincipalCollection可能保留多个领域中的主体,从该方法返回的值应该是唯一标识整个应用程序主题的单一主体。
* 这个值当然是特定于应用程序的,但是大多数应用程序通常会从一个领域(realm)中选择一个主要的主体(principals)。
* Shiro这个接口的默认实现通常只是简单地返回{@link #iterator()}.{@link java.util.Iterator#next() next()},它只返回在身份验证尝试期间从第一个咨询/配置的Realm获得的第一个返回的主体。
* 这意味着在多领域(realm)应用程序中,如果您希望保留此默认启发式,那么领域(realm)配置顺序很重要。
* 如果这种启发式方法还不够,大多数Shiro最终用户将需要实现一个自定义的{@link org.apache.shiro.authc.pam.AuthenticationStrategy}。
* {@code AuthenticationStrategy}通过<code>AuthenticationStrategy#{@link org.apache.shiro.authc.pam.AuthenticationStrategy#afterAllAttempts(org.apache.shiro.authc.AuthenticationToken, org.apache.shiro.authc.AuthenticationInfo) afterAllAttempts}实现对身份验证尝试结束时返回的{@link PrincipalCollection}进行精确控制。
* </pre>
*
*
* @return the primary principal used to uniquely identify the owning account/Subject
* @since 1.0
*/
Object getPrimaryPrincipal();
/**
* <pre>
* 返回第一个发现的可从指定类型分配的主体(principal),如果没有指定类型则为空。
* 注意,如果“所属”主题还没有登录,则返回null。
* </pre>
*
* @param type the type of the principal that should be returned. 应该返回的主体的类型。
* @return a principal of the specified type or {@code null} if there isn't one of the specified type.
*/
<T> T oneByType(Class<T> type);
/**
* <pre>
* 返回从指定类型可分配的所有主体,如果不包含该类型的主体,则返回空Collection。
* 注意,如果“所属”主题还没有登录,这将返回一个空的Collection。
* </pre>
*
* @param type the type of the principals that should be returned.
* @return a Collection of principals that are assignable from the specified type, or
* an empty Collection if no principals of this type are associated.
*/
<T> Collection<T> byType(Class<T> type);
/**
* <pre>
* 返回从所有配置的领域(realms)检索到的单个Subject的主体(principals)作为列表,如果没有主体则返回空列表。
* 请注意,如果“所属”主题还没有登录,将返回一个空List。
* </pre>
*
* @return a single Subject's principals retrieved from all configured Realms as a List.
*/
List asList();
/**
* Returns a single Subject's principals retrieved from all configured Realms as a Set, or an empty Set if there
* are not any principals.
* <p/>
* Note that this will return an empty Set if the 'owning' subject has not yet logged in.
*
* @return a single Subject's principals retrieved from all configured Realms as a Set.
*/
Set asSet();
/**
* <pre>
* 返回从指定领域检索到的单个Subject主体作为集合,如果该领域没有任何主体,则返回空集合。
* 注意,如果“所属”主题还没有登录,这将返回一个空的Collection。
* </pre>
*
* @param realmName the name of the Realm from which the principals were retrieved.
* @return the Subject's principals from the specified Realm only as a Collection or an empty Collection if there
* are not any principals from that realm.
*/
Collection fromRealm(String realmName);
/**
* Returns the realm names that this collection has principals for.
*
* @return the names of realms that this collection has one or more principals for.
*/
Set<String> getRealmNames();
/**
* Returns {@code true} if this collection is empty, {@code false} otherwise.
*
* @return {@code true} if this collection is empty, {@code false} otherwise.
*/
boolean isEmpty();
}
前文中介绍AuthenticationInfo
时,其有个子类SimpleAuthenticationInfo
的merge
方法就是通过PrincipalCollection
来实现的。AuthenticationInfo
是存放与subject关联的所有主体(principals)的集合。有两个子类MutablePrincipalCollection
和PrincipalMap
,而后者被作者标识为实验性的——暂时不要使用。因此有兴趣的可以去单独看一下。主要使用还是MutablePrincipalCollection
及其子类SimplePrincipalCollection
。
SimplePrincipalCollection:这个类主要是依靠一个key
为realmName
,value
为Set<Principal>
集合的map
来存放用户的主要信息。提供了一些写方法和读方法,都是对这个map
的基本封装。
2.6 AuthenticationToken 账户信息存储
AuthenticationToken
: AuthenticationToken是账户 主体(principals) 的整合,并支持用户在身份验证尝试期间提交的 凭证(credentials)。
- RememberMeAuthenticationToken是用来判断是否启用RememberMe功能的
- HostAuthenticationToken是用来记录身份验证尝试产生的主机信息
- UsernamePasswordToken是针对账户、密码这种形式的存储的,主要是存储用户的账户密码和主机信息以便获取
- BearerToken包含承载令牌或API密钥的AuthenticationToken,通常通过HTTP Authorization头接收。 这个类还实现了HostAuthenticationToken接口,以保留身份验证尝试所在的主机名或IP地址位置。
三. Authorizer 授权器
Authorizer(授权器)对任何给定的Subject(即“应用程序用户”)执行授权(访问控制)操作。 每个方法都需要一个主题主体(subject principal)来为相应的subject/user执行操作。 这个主要参数通常是一个表示用户数据库主键的对象,或者一个String用户名或类似的东西,它唯一地标识一个应用程序用户。 这个主体的运行时值是特定于应用程序的,由应用程序配置的Realms提供。 注意,在这个接口中有许多Permission方法重载,以接受String参数而不是Permission实例。 它们是一种方便,允许调用者在需要时使用Permission的字符串表示。 这个接口的大多数实现将简单地将这些String值转换为Permission实例,然后只调用相应的类型安全方法。 (Shiro的默认实现使用PermissionResolvers.)为这些方法进行字符串到权限的转换。) 为了方便和简单,这些重载的*Permission方法放弃了类型安全,所以您应该根据您的偏好和需要选择使用哪个方法。
Authorizer有三个子类,分别是SecurityManager
、AuthorizingRealm
、ModularRealmAuthorizer
。在此先只分析ModularRealmAuthorizer
。后面再分析另外两个。
3.1 ModularRealmAuthorizer Realm集合操作
ModularRealmAuthorizer:是一种授权器实现,它在授权操作期间调用一个或多个已配置的realm的方法。可以把这个类理解成一个realm集合,里面的各个方法就是遍历调用realm
接口的具体方法。如果此时的realm
集合里的realm
也实现了Authorizer
接口,则执行Authorizer
的相应接口,其实本质上还是在操作Authorizer
接口的子类。
3.2 Permission 系统安全策略中最细粒度的单元,构建细粒度安全模型的基石
Permission
类十分重要,是shiro
的鉴权部分(Authorization
)的基础。
Permission
类有两个子类。WildcardPermission
和AllPermission
。
-
AllPermission:分配所有权限,一般只会给类似于
root
的超级角色。
package org.apache.shiro.authz;
/**
* <pre>
* 权限表示执行操作或访问资源的能力。Permission是系统安全策略中最细粒度或原子的单元,是构建细粒度安全模型的基石。
* 重要的是要理解Permission实例只表示功能或访问——它并不授予它。授予对应用程序功能或特定资源的访问权是由应用程序的安全配置完成的,通常是通过将Permissions分配给用户、角色和/或组。
* 大多数典型的系统本质上都是Shiro团队所称的基于角色的系统,其中角色代表特定用户类型的常见行为。例如,系统可能有Administrator角色、User角色或Guest角色等。
* 但是如果您有一个动态安全模型,其中角色可以在运行时创建和删除,那么您就不能在代码中硬编码角色名。在这种环境中,角色本身并不是非常有用。重要的是给这些角色分配了什么权限。
* 在这种模式下,权限是不可变的,反映了应用程序的原始功能(打开文件、访问web URL、创建用户等)。这就是为什么系统的安全策略是动态的:因为权限代表原始的功能,只有当应用程序的源代码改变时才会改变,它们在运行时是不可变的——它们代表系统可以做的“什么”。角色、用户和组是应用程序的“谁”。然后,确定“谁”可以做“什么”就变成了以某种方式将权限与角色、用户和组关联起来的简单练习。
* 大多数应用程序是通过将命名角色与权限关联起来(例如,一个角色“拥有”一组权限),然后将用户与角色关联起来(例如,一个用户“拥有”一组角色),这样通过传递关联,用户“拥有”他们角色中的权限。这个主题(subject)有许多变体(权限直接分配给用户,或分配给组,用户添加到组,这些组又有角色,等等)。当使用基于权限的安全模型而不是基于角色的安全模型时,用户、角色和组都可以在运行时创建、配置和/或删除。这支持一个非常强大的安全模型。
* Shiro的一个好处是,尽管它假定大多数系统都是基于这些类型的静态或动态角色w /许可计划,它不需要这样一个系统模型的安全数据,所有权限检查降级{@link org.apache.shiro.realm.Realm}实现,只有这些实现才能真正决定用户是否“拥有”权限。Realm可以使用这里描述的语义,也可以完全利用其他一些机制——这始终取决于应用程序开发人员。
* Shiro以{@link org.apache.shiro.authz.permission.WildcardPermission WildcardPermission}的形式提供了这个接口的一个非常强大的默认实现。我们强烈建议您在尝试实现自己的权限之前研究这个类。
* </pre>
*
* @see org.apache.shiro.authz.permission.WildcardPermission WildcardPermission
* @since 0.2
*/
public interface Permission {
/**
* <pre>
* 如果当前实例implies了指定Permission参数描述的所有功能和/或资源访问,则返回true,否则返回false。
* 也就是说,当前实例必须完全等于给定Permission参数所描述的功能和/或资源访问的超集。另一种说法是:
* 如果“permission1 implies permission2”,即permission1.implies(permission2),那么授予permission1的任何Subject的能力都将大于或等于permission2定义的能力。
* </pre>
*
* @param p the permission to check for behavior/functionality comparison.
* @return {@code true} if this current instance <em>implies</em> all the functionality and/or resource access
* described by the specified {@code Permission} argument, {@code false} otherwise. 如果当前实例<em>implies</em>指定的{@code Permission}参数描述的所有功能和/或资源访问,则{@code true},否则为{@code false}。
*/
boolean implies(Permission p);
}
-
WildcardPermission:通配权限匹配,通过冒号和逗号分割权限字符串进行权限匹配。例如"product,user:view,eidt:13,15",这样意味着产品和用户模块的13和15这两个的查看、编辑权限。
shiro
的这个配置可以说十分灵活,相当于将一个权限分配成了许多个namespace
,有分成了许多个group
,又分成了许多细粒度的操作权限。具体解释请看下方代码的注释。
package org.apache.shiro.authz.permission;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.util.CollectionUtils;
import org.apache.shiro.lang.util.StringUtils;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* <pre>
* </p>
* <code>WildcardPermission</code>是一种非常灵活的权限构造,支持多级权限匹配。然而,大多数人可能会遵循一些标准约定,如下所述。
* <h3>简单的使用</h3>
* 在最简单的形式中,WildcardPermission可以用作简单的权限字符串。您可以授予用户“editNewsletter”权限,然后通过调用来检查用户是否具有editNewsletter权限
* <p>
* <code>subject.isPermitted("editNewsletter")</code>
* </p>
* 这(大部分)相当于
* <p/>
* <code>subject.isPermitted( new WildcardPermission("editNewsletter") )</code>
* <p/>
* 稍后再详细介绍。
* </p>
* 简单的权限字符串可能对简单的应用程序工作,但它需要你有权限像"viewNewsletter", " deletenewletter ", " createnewletter "等。您还可以使用通配符授予用户“*”权限(给出该类的名称),这意味着他们拥有所有权限。但是在使用这种方法时,没有办法仅仅说一个用户拥有“所有的newsletter权限(all newsletter permissions)”。
* 因此,WildcardPermission支持多级权限。
* <h3>多个层面(Multiple Levels)</h3>
* WildcardPermission还支持多个级别(Multiple Levels)的概念。例如,您可以通过授予用户“newsletter:edit”权限来重组前面的简单示例。本例中的冒号是WildcardPermission使用的特殊字符,用于分隔权限中的下一个标记。
* 在本例中,第一个标记是正在操作的域,第二个标记是正在执行的操作。每个级别可以包含多个值。因此,您可以简单地授予用户权限“newsletter:view、edit、creat”,这使他们能够在newsletter域中执行查看、编辑和创建操作。然后您可以通过调用来检查用户是否具有“newsletter:create”权限
* <p>
* <code>subject.isPermitted("newsletter:create")</code>
* </p>
* (返回true)。
* </p>
* 除了通过单个字符串授予多个权限外,您还可以授予特定级别的所有权限。因此,如果你想在newsletter域中授予一个用户所有的操作,你可以简单地给他们“newsletter:*”。现在,任何“newsletter:XXX”的权限检查将返回true。也可以在域级别使用通配符令牌(或两者都使用):因此您可以在所有域“*:view”中授予用户“view”操作。
* <h3>实例级访问控制</h3>
* WildcardPermission的另一个常见用法是建模实例级访问控制列表。在这个场景中,您使用三个标记—第一个是域,第二个是动作,第三个是您正在操作的实例。
* 例如,你可以授予用户"newsletter:edit:12,13,18"。在本例中,假设第三个令牌是通讯的系统ID。这将允许用户编辑newsletter12、13和18。这是一个非常强大的方式来表达权限,因为现在你可以这样说“newsletter:*:13”(授予用户所有操作newsletter13),“newsletter:view、create、edit:*”(允许用户查看、创建或编辑任何newsletter),或“newsletter:*:*(允许用户执行任何行动通讯)。
* 要对这些实例级权限执行检查,应用程序应该像这样在权限检查中包含实例ID:
* <p/>
* <code>subject.isPermitted( "newsletter:edit:13" )</code>
* <p/>
* 可以使用的令牌(token)数量没有限制,因此在应用程序中使用令牌(token)的方式取决于您的想象力。然而,Shiro团队喜欢标准化上面显示的一些常见用法,以帮助人们开始并在Shiro社区中提供一致性
* </pre>
*
* @since 0.9
*/
public class WildcardPermission implements Permission, Serializable {
//TODO - JavaDoc methods
/*--------------------------------------------
| C O N S T A N T S |
============================================*/
protected static final String WILDCARD_TOKEN = "*";
protected static final String PART_DIVIDER_TOKEN = ":";
protected static final String SUBPART_DIVIDER_TOKEN = ",";
/**
* 是否忽略大小写,默认不忽略
*/
protected static final boolean DEFAULT_CASE_SENSITIVE = false;
/*--------------------------------------------
| I N S T A N C E V A R I A B L E S |
============================================*/
/**
* parts的结构:
* [[user,product],[view,edit],[13,15]]
*/
private List<Set<String>> parts;
/*--------------------------------------------
| C O N S T R U C T O R S |
============================================*/
/**
* Default no-arg constructor for subclasses only - end-user developers instantiating Permission instances must
* provide a wildcard string at a minimum, since Permission instances are immutable once instantiated.
* <p/>
* Note that the WildcardPermission class is very robust and typically subclasses are not necessary unless you
* wish to create type-safe Permission objects that would be used in your application, such as perhaps a
* {@code UserPermission}, {@code SystemPermission}, {@code PrinterPermission}, etc. If you want such type-safe
* permission usage, consider subclassing the {@link DomainPermission DomainPermission} class for your needs.
*
* <pre>
* 默认无参数构造函数仅适用于子类——实例化Permission实例的最终用户开发人员必须至少提供一个通配符字符串,因为一旦实例化Permission实例就不可变了。
* 请注意,WildcardPermission类非常健壮,通常不需要子类,除非您希望创建类型安全的Permission对象,以便在应用程序中使用,比如{@code UserPermission}, {@code SystemPermission}, {@code PrinterPermission}等。
* 如果你想要使用这种类型安全的权限,考虑根据你的需要子类化{@link DomainPermission DomainPermission}类。
* </pre>
*/
protected WildcardPermission() {
}
public WildcardPermission(String wildcardString) {
this(wildcardString, DEFAULT_CASE_SENSITIVE);
}
public WildcardPermission(String wildcardString, boolean caseSensitive) {
setParts(wildcardString, caseSensitive);
}
protected void setParts(String wildcardString) {
setParts(wildcardString, DEFAULT_CASE_SENSITIVE);
}
/**
* 将wildcardString根据冒号和逗号分割并添加到parts中
* @param wildcardString
* @param caseSensitive
*/
protected void setParts(String wildcardString, boolean caseSensitive) {
//例如:wildcardString = “user,product:view,edit:13,15”
wildcardString = StringUtils.clean(wildcardString);
if (wildcardString == null || wildcardString.isEmpty()) {
throw new IllegalArgumentException("Wildcard string cannot be null or empty. Make sure permission strings are properly formatted.");
}
if (!caseSensitive) {
wildcardString = wildcardString.toLowerCase();
}
//此时parts为:["user,product","view,edit","13,15"]
List<String> parts = CollectionUtils.asList(wildcardString.split(PART_DIVIDER_TOKEN));
this.parts = new ArrayList<Set<String>>();
for (String part : parts) {
Set<String> subparts = CollectionUtils.asSet(part.split(SUBPART_DIVIDER_TOKEN));
if (subparts.isEmpty()) {
throw new IllegalArgumentException("Wildcard string cannot contain parts with only dividers. Make sure permission strings are properly formatted.");
}
//此时parts为:[[user,product],[view,edit],[13,15]]
this.parts.add(subparts);
}
if (this.parts.isEmpty()) {
throw new IllegalArgumentException("Wildcard string cannot contain only dividers. Make sure permission strings are properly formatted.");
}
}
/*--------------------------------------------
| A C C E S S O R S / M O D I F I E R S |
============================================*/
protected List<Set<String>> getParts() {
return this.parts;
}
/**
* Sets the pre-split String parts of this <code>WildcardPermission</code>.
* @since 1.3.0
* @param parts pre-split String parts.
*/
protected void setParts(List<Set<String>> parts) {
this.parts = parts;
}
/*--------------------------------------------
| M E T H O D S |
============================================*/
@Override
public boolean implies(Permission p) {
// By default only supports comparisons with other WildcardPermissions
//默认情况下,只支持与其他WildcardPermissions进行比较
if (!(p instanceof WildcardPermission)) {
return false;
}
WildcardPermission wp = (WildcardPermission) p;
List<Set<String>> otherParts = wp.getParts();
int i = 0;
for (Set<String> otherPart : otherParts) {
// If this permission has less parts than the other permission, everything after the number of parts contained
// in this permission is automatically implied, so return true
//如果这个权限比其他权限拥有更少的部件,则是包含部件数量之后的所有内容
//在此权限中是自动隐含的,因此返回true
//此处的i代表的是遍历到了parts的第几位。如果getParts().size() - 1 < i,说明本类的parts已被遍历完并且已被遍历部分与传入的Permission都已成功匹配。此时返回true
//这意味着如果构造函数传递的是 product:view ,此方法传递的是 product:view:13 ,也会返回true
if (getParts().size() - 1 < i) {
return true;
} else {
//如果本WildcardPermission的parts中的第i个part不包括*并且otherPart不是part的子集,说明权限不匹配,返回false;否则,i++;
// 注意:parts的结构为:[[user,product],[view,edit],[13,15]]
Set<String> part = getParts().get(i);
if (!part.contains(WILDCARD_TOKEN) && !part.containsAll(otherPart)) {
return false;
}
i++;
}
}
// If this permission has more parts than the other parts, only imply it if all of the other parts are wildcards
for (; i < getParts().size(); i++) {
Set<String> part = getParts().get(i);
if (!part.contains(WILDCARD_TOKEN)) {
return false;
}
}
return true;
}
@Override
public String toString() {
StringBuilder buffer = new StringBuilder();
for (Set<String> part : parts) {
if (buffer.length() > 0) {
buffer.append(PART_DIVIDER_TOKEN);
}
Iterator<String> partIt = part.iterator();
while(partIt.hasNext()) {
buffer.append(partIt.next());
if (partIt.hasNext()) {
buffer.append(SUBPART_DIVIDER_TOKEN);
}
}
}
return buffer.toString();
}
@Override
public boolean equals(Object o) {
if (o instanceof WildcardPermission) {
WildcardPermission wp = (WildcardPermission) o;
return parts.equals(wp.parts);
}
return false;
}
@Override
public int hashCode() {
return parts.hashCode();
}
}
-
DomainPermission:如果您希望创建类型安全的Permission对象,以便在应用程序中使用,比如UserPermission, SystemPermission, PrinterPermission等,则需要用的这个类,您可以 以这个类为基类,实现一些自己想要的类。这个类会根据类名创建一个默认前缀来进行匹配,比如你的类叫做
SystemPermission
,在通过构造函数传入的通配符的前面会加上“system:*:”来进行匹配。相当于分配了很多个namespace
。
package org.apache.shiro.authz.permission;
import org.apache.shiro.lang.util.StringUtils;
import java.util.Set;
/**
* Provides a base Permission class from which type-safe/domain-specific subclasses may extend. Can be used
* as a base class for JPA/Hibernate persisted permissions that wish to store the parts of the permission string
* in separate columns (e.g. 'domain', 'actions' and 'targets' columns), which can be used in querying
* strategies.
*
* <pre>
* 提供一个基本的Permission类,类型安全的/特定于域的子类可以从中扩展。可以用作JPA/Hibernate持久性权限的基类,希望将权限字符串的各个部分存储在单独的列中(例如。'domain', 'actions'和'targets'列),可以用于查询策略。
* </pre>
*
* @since 1.0
*/
public class DomainPermission extends WildcardPermission {
private String domain;
private Set<String> actions;
private Set<String> targets;
private static final long serialVersionUID = 1l;
/**
* Creates a domain permission with *all* actions for *all* targets;
*/
public DomainPermission() {
this.domain = getDomain(getClass());
setParts(getDomain(getClass()));
}
public DomainPermission(String actions) {
domain = getDomain(getClass());
this.actions = StringUtils.splitToSet(actions, SUBPART_DIVIDER_TOKEN);
encodeParts(domain, actions, null);
}
public DomainPermission(String actions, String targets) {
this.domain = getDomain(getClass());
this.actions = StringUtils.splitToSet(actions, SUBPART_DIVIDER_TOKEN);
this.targets = StringUtils.splitToSet(targets, SUBPART_DIVIDER_TOKEN);
encodeParts(this.domain, actions, targets);
}
protected DomainPermission(Set<String> actions, Set<String> targets) {
this.domain = getDomain(getClass());
setParts(domain, actions, targets);
}
private void encodeParts(String domain, String actions, String targets) {
if (!StringUtils.hasText(domain)) {
throw new IllegalArgumentException("domain argument cannot be null or empty.");
}
StringBuilder sb = new StringBuilder(domain);
if (!StringUtils.hasText(actions)) {
if (StringUtils.hasText(targets)) {
sb.append(PART_DIVIDER_TOKEN).append(WILDCARD_TOKEN);
}
} else {
sb.append(PART_DIVIDER_TOKEN).append(actions);
}
if (StringUtils.hasText(targets)) {
sb.append(PART_DIVIDER_TOKEN).append(targets);
}
//"domain:*:actions:targets"
//"类名去掉Permission后小写:*:传入的actions字符串:传入的targets"
setParts(sb.toString());
}
protected void setParts(String domain, Set<String> actions, Set<String> targets) {
String actionsString = StringUtils.toDelimitedString(actions, SUBPART_DIVIDER_TOKEN);
String targetsString = StringUtils.toDelimitedString(targets, SUBPART_DIVIDER_TOKEN);
encodeParts(domain, actionsString, targetsString);
this.domain = domain;
this.actions = actions;
this.targets = targets;
}
protected String getDomain(Class<extends DomainPermission> clazz) {
String domain = clazz.getSimpleName().toLowerCase();
//strip any trailing 'permission' text from the name (as all subclasses should have been named):
int index = domain.lastIndexOf("permission");
if (index != -1) {
domain = domain.substring(0, index);
}
return domain;
}
public String getDomain() {
return domain;
}
protected void setDomain(String domain) {
if (this.domain != null && this.domain.equals(domain)) {
return;
}
this.domain = domain;
setParts(domain, actions, targets);
}
public Set<String> getActions() {
return actions;
}
protected void setActions(Set<String> actions) {
if (this.actions != null && this.actions.equals(actions)) {
return;
}
this.actions = actions;
setParts(domain, actions, targets);
}
public Set<String> getTargets() {
return targets;
}
protected void setTargets(Set<String> targets) {
if (this.targets != null && this.targets.equals(targets)) {
return;
}
this.targets = targets;
setParts(domain, actions, targets);
}
}
3.3 PermissionResolver 权限解析器
PermissionResolver
解析字符串值并将其转换为Permission实例,可以通过向Shiro组件提供此接口的任何实例来提供自定义的String-to-Permission转换。
此解析器只有一个子类,但我们可以将这唯一的一个 子类当做shiro
给我们的一个例子,算是教我们怎么使用这个接口。代码也很简单,这里贴出来,就不做过多解释了。
/**
* <tt>PermissionResolver</tt> implementation that returns a new {@link WildcardPermission WildcardPermission}
* based on the input string.
*
* <pre>
* 基于输入字符串返回一个新的WildcardPermission的PermissionResolver实现。
* </pre>
*
* @since 0.9
*/
public class WildcardPermissionResolver implements PermissionResolver {
boolean caseSensitive;
/**
* Constructor to specify case sensitivity for the resolved permissions.
* @param caseSensitive true if permissions should be case sensitive.
*/
public WildcardPermissionResolver(boolean caseSensitive) {
this.caseSensitive=caseSensitive;
}
/**
* Default constructor.
* Equivalent to calling WildcardPermissionResolver(false)
*
* @see WildcardPermissionResolver#WildcardPermissionResolver(boolean)
*/
public WildcardPermissionResolver() {
this(WildcardPermission.DEFAULT_CASE_SENSITIVE);
}
/**
* Set the case sensitivity of the resolved Wildcard permissions.
* @param state the caseSensitive flag state for resolved permissions.
*/
public void setCaseSensitive(boolean state) {
this.caseSensitive = state;
}
/**
* Return true if this resolver produces case sensitive permissions.
* @return true if this resolver produces case sensitive permissions.
*/
public boolean isCaseSensitive() {
return caseSensitive;
}
/**
* Returns a new {@link WildcardPermission WildcardPermission} instance constructed based on the specified
* <tt>permissionString</tt>.
*
* @param permissionString the permission string to convert to a {@link Permission Permission} instance.
* @return a new {@link WildcardPermission WildcardPermission} instance constructed based on the specified
* <tt>permissionString</tt>
*/
public Permission resolvePermission(String permissionString) {
return new WildcardPermission(permissionString, caseSensitive);
}
}
四. Realm 安全组件
Realm是一个安全组件,它可以访问特定于应用程序的安全实体,如用户、角色和权限,以确定身份验证和授权操作。
其有一个抽象实现类CachingRealm,这个类是个非常基本的抽象扩展点,为子类提供缓存支持。主要依靠CacheManager来进行缓存。并提供了登出和获取这个realm
对应的账户身份的方法。这里只贴它的获取账户的方法
/**
* A utility method for subclasses that returns the first available principal of interest to this particular realm.
* The heuristic used to acquire the principal is as follows:
* <ul>
* <li>Attempt to get <em>this particular Realm's</em> 'primary' principal in the {@code PrincipalCollection} via a
* <code>principals.{@link PrincipalCollection#fromRealm(String) fromRealm}({@link #getName() getName()})</code>
* call.</li>
* <li>If the previous call does not result in any principals, attempt to get the overall 'primary' principal
* from the PrincipalCollection via {@link org.apache.shiro.subject.PrincipalCollection#getPrimaryPrincipal()}.</li>
* <li>If there are no principals from that call (or the PrincipalCollection argument was null to begin with),
* return {@code null}</li>
* </ul>
*
* <pre>
* </p>
* 用于子类的实用程序方法,它向这个特定realm返回第一个可用的principal。 用于获取原则的启发式如下:
* 尝试通过principal . fromrealm (getName())调用在PrincipalCollection中获取这个特定领域的“主”主体。
* 如果前面的调用没有导致任何主体,尝试通过PrincipalCollection. getprimaryprincipal()从PrincipalCollection获取总体的“主”主体。
* 如果没有来自该调用的主体(或者PrincipalCollection参数一开始是null),则返回null
* </pre>
*
* @param principals the PrincipalCollection holding all principals (from all realms) associated with a single Subject.
* @return the 'primary' principal attributed to this particular realm, or the fallback 'master' principal if it
* exists, or if not {@code null}.
* @since 1.2
*/
protected Object getAvailablePrincipal(PrincipalCollection principals) {
Object primary = null;
if (!isEmpty(principals)) {
Collection thisPrincipals = principals.fromRealm(getName());
if (!CollectionUtils.isEmpty(thisPrincipals)) {
primary = thisPrincipals.iterator().next();
} else {
//no principals attributed to this particular realm. Fall back to the 'master' primary:
//PrincipalCollection中没有存储叫这个名字的principals集合,返回PrincipalCollection中存储的主身份
//因为shiro是可以配置多个Realm的。如果有多个realm,就会以每个realm的name(唯一标识)为key,每个realm解析出来的身份为value存储在PrincipalCollection中,如果从PrincipalCollection中
//没有查询到这个realm存储的解析好的用户身份。那么就会取一个主身份。主身份的策略是可配置的,如果没有配置,默认是取第一个身份
primary = principals.getPrimaryPrincipal();
}
}
return primary;
}
4.1 CacheManager 缓存
CacheManager
是shiro的缓存管理器。Shiro
本身并没有实现完整的缓存机制,因为这超出了安全框架的核心能力。 相反,这个接口在底层缓存框架的主管理器组件(例如JCache, Ehcache, JCS, OSCache, JBossCache, TerraCotta, Coherence, GigaSpaces,等等)上提供了一个抽象(包装器)API,允许Shiro
用户配置他们选择的任何缓存机制。
AbstractCacheManager和MemoryConstrainedCacheManager是shiro
基于ConcurrentMap
实现的线程安全的缓存器,很简单的实现,就是利用了map的put/get方法,但它不提供任何企业级特性,如缓存一致性、乐观锁定、故障转移或其他类似特性。 对于更多的企业级特性,可以考虑使用由企业级缓存产品(Hazelcast, EhCache, TerraCotta, Coherence, GigaSpaces,等等)支持的不同的缓存管理器实现
HazelcastCacheManager和EhCacheManager是分别利用Hazelcast
和EhCache
做缓存机制,有兴趣的可以去了解一下。
4.2 CredentialsMatcher 凭证匹配器
用户登录时token中会携带凭证信息,CredentialsMatcher
是用来将token中的凭证信息与保存的用户凭证进行匹配验证的Matcher
。
AllowAllCredentialsMatcher:这个匹配器总是返回true。意味着不论token携带的是什么凭证,都会认为其实符合要求的
PasswordMatcher: 这是一个对散列文本密码采用最佳实践比较的CredentialsMatcher。如果用户存储的凭证信息是Hash类型的,就用Hash去判断密码是 否正确,如果用户存储的凭证信息是普通字符串。就用
PasswordService
来进行判断SimpleCredentialsMatcher:简单CredentialsMatcher实现,如果是byte[],char[],ByteSource,String,File,InputStream中的一种,则对传入的密码和存储的密码利用
MessageDigest.isEqual()
进行判断,否则就用最简单的equels进行判断。-
HashedCredentialsMatcher:如果是简单的对密码进行匹配的话,是有很大风险的,这个类的注释里也列举了例子和说明了
SimpleCredentialsMatcher
那种简单的判断方式的弊端,而这个类对其做的优化是进行了Salt和Multiple Hash Iterations,即加盐和多重Hash迭代,主要是重写了其``SimpleCredentialsMatcher.doCredentialsMatch方法,在获取凭证时不再是简单的返回存储的凭证,而是重构一个Hash实例,并将其解码后的byte[]设置进去;也不再简单的返回token的凭证,而是将token的凭证根据
AuthenticationInfo存储的
salt`进行一次Hash,再对这两个凭证进行一个匹配。/** * Returns a {@link Hash Hash} instance representing the already-hashed AuthenticationInfo credentials stored in the system. * <p/> * This method reconstructs a {@link Hash Hash} instance based on a {@code info.getCredentials} call, * but it does <em>not</em> hash that value - it is expected that method call will return an already-hashed value. * <p/> * This implementation's reconstruction effort functions as follows: * <ol> * <li>Convert {@code account.getCredentials()} to a byte array via the {@link #toBytes toBytes} method. * <li>If {@code account.getCredentials()} was originally a String or char[] before {@code toBytes} was * called, check for encoding: * <li>If {@link #storedCredentialsHexEncoded storedCredentialsHexEncoded}, Hex decode that byte array, otherwise * Base64 decode the byte array</li> * <li>Set the byte[] array directly on the {@code Hash} implementation and return it.</li> * </ol> * * <pre> * </p> * 返回一个{@link Hash Hash}实例,表示存储在系统中已经散列的AuthenticationInfo凭据。 * * 此方法基于信息重建{@link Hash Hash}实例。 {@code info.getCredentials}调用,但它不散列该值—方法调用将返回一个已经散列的值。 * * 这项实施工作的重建工作职能如下: * <ol> * <li>通过{@link #toBytes toBytes}方法将{@code account.getCredentials()}转换为字节数组。</li> * <li>如果{@code account.getCredentials()}在{@code toBytes}被调用之前最初是一个String或char[],检查编码:</li> * <li>如果{@link #storedCredentialsHexEncoded storedCredentialsHexEncoded},则Hex解码该字节数组,否则Base64解码该字节数组</li> * <li>在{@code Hash}实现上直接设置byte[]数组并返回它。</li> * </ol> * </pre> * * @param info the AuthenticationInfo from which to retrieve the credentials which assumed to be in already-hashed form. * @return a {@link Hash Hash} instance representing the given AuthenticationInfo's stored credentials. */ @Override protected Object getCredentials(AuthenticationInfo info) { Object credentials = info.getCredentials(); byte[] storedBytes = toBytes(credentials); if (credentials instanceof String || credentials instanceof char[]) { //account.credentials were a char[] or String, so //we need to do text decoding first: if (isStoredCredentialsHexEncoded()) { storedBytes = Hex.decode(storedBytes); } else { storedBytes = Base64.decode(storedBytes); } } AbstractHash hash = newHashInstance(); hash.setBytes(storedBytes); return hash; }
/**
* This implementation first hashes the {@code token}'s credentials, potentially using a
* {@code salt} if the {@code info} argument is a
* {@link org.apache.shiro.authc.SaltedAuthenticationInfo SaltedAuthenticationInfo}. It then compares the hash
* against the {@code AuthenticationInfo}'s
* {@link #getCredentials(org.apache.shiro.authc.AuthenticationInfo) already-hashed credentials}. This method
* returns {@code true} if those two values are {@link #equals(Object, Object) equal}, {@code false} otherwise.
*
* <pre>
* </p>
* 这个实现首先散列{@code token}的凭证,如果{@code info}参数是{@link org.apache.shiro.authc.SaltedAuthenticationInfo SaltedAuthenticationInfo},则可能使用一个{@code salt}。
* 然后将哈希值与{@link #getCredentials(org.apache.shiro.authc.AuthenticationInfo) already-hashed credentials}已经哈希的凭据进行比较。 如果这两个值相等,该方法返回true,否则返回false
* </pre>
*
* @param token the {@code AuthenticationToken} submitted during the authentication attempt.
* @param info the {@code AuthenticationInfo} stored in the system matching the token principal
* @return {@code true} if the provided token credentials hash match to the stored account credentials hash,
* {@code false} otherwise
* @since 1.1
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenHashedCredentials = hashProvidedCredentials(token, info);
Object accountCredentials = getCredentials(info);
return equals(tokenHashedCredentials, accountCredentials);
}
/**
* Hash the provided {@code token}'s credentials using the salt stored with the account if the
* {@code info} instance is an {@code instanceof} {@link SaltedAuthenticationInfo SaltedAuthenticationInfo} (see
* the class-level JavaDoc for why this is the preferred approach).
* <p/>
* If the {@code info} instance is <em>not</em>
* an {@code instanceof} {@code SaltedAuthenticationInfo}, the logic will fall back to Shiro 1.0
* backwards-compatible logic: it will first check to see {@link #isHashSalted() isHashSalted} and if so, will try
* to acquire the salt from {@link #getSalt(AuthenticationToken) getSalt(AuthenticationToken)}. See the class-level
* JavaDoc for why this is not recommended. This 'fallback' logic exists only for backwards-compatibility.
* {@code Realm}s should be updated as soon as possible to return {@code SaltedAuthenticationInfo} instances
* if account credentials salting is enabled (highly recommended for password-based systems).
*
*
* <pre>
* </p>
* 如果信息实例是{@link SaltedAuthenticationInfo SaltedAuthenticationInfo}的实例(请参阅类级JavaDoc了解为什么这是首选方法),则使用与帐户存储的盐对提供的令牌凭据进行散列。
*
* 如果info实例不是一个{@code SaltedAuthenticationInfo}实例,逻辑将退回到Shiro 1.0向后兼容的逻辑:它将首先检查是否看到{@link #isHashSalted() isHashSalted},如果是,将尝试从{@link #getSalt(AuthenticationToken) getSalt(AuthenticationToken)}获取盐。 请参阅类级别的JavaDoc了解为什么不建议这样做。 这种“回退”逻辑只存在于向后兼容。 如果启用了账户凭证salt(强烈推荐基于密码的系统),Realms应该尽快更新以返回{@code SaltedAuthenticationInfo}实例。
* </pre>
*
* @param token the submitted authentication token from which its credentials will be hashed
* @param info the stored account data, potentially used to acquire a salt
* @return the token credentials hash
* @since 1.1
*/
protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) {
final Object salt;
if (info instanceof SaltedAuthenticationInfo) {
salt = ((SaltedAuthenticationInfo) info).getCredentialsSalt();
} else if (isHashSalted()) {
//retain 1.0 backwards compatibility:
salt = getSalt(token);
} else {
salt = SimpleByteSource.empty();
}
return hashProvidedCredentials(token.getCredentials(), salt, getHashIterations());
}
4.3 PasswordService 密码服务
PasswordService
主要有两个方法,加密密码和密码匹配,而其子接口HashingPasswordService
则提供了Hash密码
和Hash密码匹配
。其默认实现类DefaultPasswordService
主要依赖hashService
、hashFormat
、hashFormatFactory
来对密码进行Hash和匹配。
4.4 Realm 其他子类
重新贴一下这张图,以便解读。
4.4.1 AuthenticatingRealm 身份认证Realm
Realm接口的顶级抽象实现,仅实现 身份验证支持(登录)操作,并将授权(访问控制)行为留给子类。这个类是基于CachingRealm
的,可以实现获取用户身份信息、校验用户身份信息是否匹配等功能。不过shiro是可以不开启缓存的,如果没有缓存,则要通过子类去实现doGetAuthenticationInfo
方法获取用户的认证信息。
4.4.2 AuthorizingRealm 权限校验Realm
主要负责权限控制,包括获取角色、权限、匹配用户是否具有相关权限、角色等功能。这个类在前面的UML中我们应该看到很多次,他实现了Authorizer
、AuthenticatingRealm
、PermissionResolverAware
、RolePermissionResolverAware
等接口,是比较核心的一个类,我们在进行shiro配置时,如果要自己定义 Realm,一般都是继承这个类来重写逻辑。这个类也是可以启用缓存和关闭缓存的,如果关闭缓存,则需要通过子类重写的doGetAuthorizationInfo
方法获取用户的角色和权限。
4.4.3 AuthorizingRealm 子类
-
SimpleAccountRealm: Realm接口的简单实现,使用一组已配置的用户帐户和角色来支持身份验证和授权。 每个帐户条目为用户指定用户名、密码和角色。 角色也可以映射到权限,并与用户关联。这个类及其子类
TextConfigurationRealm
、PropertiesRealm
、IniRealm
主要是根据固定格式来获取用户信息的类,可以通过字符串格式、properties格式、INI格式三种方式进行配置。 - DefaultLdapRealm:使用Sun /Oracle的JNDI API作为LDAP API的LDAP Realm实现。
-
JdbcRealm:通过JDBC查询数据库获取身份、角色和权限信息。在这个类里是定义了默认sql的,如果我们使用这个类作为Bean注入spring容器中,就需要按照shiro的要求去建表,当然这个类也提供了set方法,我们可以将符合自己数据库表的sql设置进来。其子类
SaltAwareJdbcRealm
是为支持加盐凭证而存在的Realm。不过需要在未来的Shiro版本中更新JdbcRealm实现来处理这个问题。
/*--------------------------------------------
| C O N S T A N T S |
============================================*/
/**
* The default query used to retrieve account data for the user.
*/
protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
/**
* The default query used to retrieve account data for the user when {@link #saltStyle} is COLUMN.
*/
protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";
/**
* The default query used to retrieve the roles that apply to a user.
*/
protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?";
/**
* The default query used to retrieve permissions that apply to a particular role.
*/
protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";
-
SampleRealm:这个类是基于一个
UserDAO
来进行信息查询的,这个UserDao
是利用Hibernate
进行查询的 -
AbstractLdapRealm:使用LDAP服务器进行身份验证以为用户构建Subject的org.apache.shiro.realm。 这个实现只返回特定用户的角色,而其子类
ActiveDirectoryRealm
实现查询用户的组,然后使用groupRolesMap将组名映射到角色。
五. SecurityManager 大总管
5.1 RememberMeManager 身份记录管理器
RememberMeManager负责在应用程序的Subject会话中记住Subject的身份。
RememberMeManager
接口提供以上方法,主要作用分别是 获取记住的账户信息、忘记主体、登录成功时要做的事情、登录失败要做的事情、退出时要做的事情。
-
AbstractRememberMeManager:
RememberMeManager
接口的抽象实现,该接口处理记忆用户身份的序列化和加密。记住的标识存储位置和细节留给子类。在记住用户身份信息的时候,会利用private Serializer<PrincipalCollection> serializer;
和private CipherService cipherService;
来进行序列化和加解密。默认情况下,这个类使用的是对称的AesCipherService
,而AesCipherService
默认使用的是对称密码,而shiro是开源代码,所以最好自己设置加解密的方式(如果用户的账户信息你觉得并不敏感,也可以使用shiro默认配置)。 - CookieRememberMeManager:通过将Subject的principals保存到Cookie中以备以后检索,记住主体的身份。
5.2 SessionStorageEvaluator 是否使用Session存储用户信息判定器
评估Shiro是否可以使用Subject的Session来持久化该Subject的内部状态。这是一个常见的Shiro实现策略,使用Subject的会话来持久化Subject的身份和身份验证状态(例如登录后),这样信息就不需要在任何进一步的请求/调用中传递。 这有效地允许会话id被用于任何请求或调用,作为Shiro需要的唯一“指针”,Shiro可以根据引用的会话重新创建Subject实例。
DefaultSessionStorageEvaluator:内部维护一个boolean值存储是否启用session
DefaultWebSessionStorageEvaluator:一个特定于web的SessionStorageEvaluator,它执行与父类DefaultSessionStorageEvaluator相同的逻辑,但额外检查一个特定于请求的标志,可以启用或禁用会话访问。
5.3 SecurityManager 大总管
SecurityManager执行跨单个应用程序的所有subject(也就是用户)的所有安全操作。 接口本身主要是作为一种方便而存在的—它扩展了Authenticator, Authorizer, 和 SessionManager接口,从而将这些行为合并到单个参考点中。 对于大多数Shiro用法来说,这简化了配置,比单独引用Authenticator、Authorizer和SessionManager实例更方便; 相反,只需要与单个SecurityManager实例进行交互。 除了上述三个接口外,该接口还提供了许多支持Subject行为的方法。 Subject对单个用户执行身份验证、授权和会话操作,因此只能由SecurityManager管理,它知道所有这三个功能。 另一方面,三个父接口不“知道” Subjects,以确保关注点的清晰分离。 使用注意:事实上,绝大多数应用程序程序员不会经常与SecurityManager交互,如果有的话。 大多数应用程序程序员只关心当前执行用户的安全操作,通常通过调用SecurityUtils.getSubject()来实现。 另一方面,框架开发人员可能会发现使用一个实际的SecurityManager非常有用。
这个类是整个Shiro的核心,也是整个Shiro执行的入口,他实现了之前我们讨论的认证、鉴权、登录、注销、Session管理等等功能,是把之前的细分领域做了一个统筹。
-
SecurityManager:超类,提供了
login
、logout
、createSubject
三个接口-
CachingSecurityManager:这是SecurityManager接口的一个非常基本的起点,它只提供日志记录和缓存支持。 所有实际的SecurityManager方法实现都留给子类。内部维护了一个
cacheManager
缓存管理器和一个eventBus
消息总线,主要功能是依赖这二者,并把EventBus
设置进CacheManager
以便后续CacheMangaer
进行消息通知 -
RealmSecurityManager:内部维护一个
Realm
集合,提供后置方法,并将父类中的CacheManager
设置到每个Realm
中,具体的实现留给子类 -
AuthenticatingSecurityManager:内部维护一个
Authenticator
,提供后置方法,并把父类中的Ream
集合设置到Authenticator
中。具体实现留给子类 -
AuthorizingSecurityManager:内部维护一个
Authorizer
,所有鉴权的方法都是依赖Authorizer
,提供后置方法,并把父类中的Ream
集合设置到Authenticator
中。 -
SessionsSecurityManager:内部维护一个
SessionManager
,提供开启session和获取session的操作,提供后置方法,并把父类中的CacheManager
和EventBus
设置到SessionManager
中。 -
DefaultSecurityManager: Shiro框架默认的SecurityManager接口的具体实现,基于一组Realms。 此实现通过超类实现将其身份验证、授权和会话操作分别委托给包装的Authenticator, Authorizer, 和 SessionManager实例。自己封装了
login
、和logOut
方法 - WebSecurityManager:这个接口代表了一个SecurityManager实现,可以在支持web的应用程序中使用。
- DefaultWebSecurityManager: 默认的WebSecurityManager实现用于基于web的应用程序或任何需要HTTP连接的应用程序(SOAP、HTTP远程处理等)。
-
CachingSecurityManager:这是SecurityManager接口的一个非常基本的起点,它只提供日志记录和缓存支持。 所有实际的SecurityManager方法实现都留给子类。内部维护了一个
六. Subject 用户直接使用的操作器
Subject表示单个应用程序用户的状态和安全操作。 这些操作包括身份验证(登录/注销)、授权(访问控制)和会话访问。 它是Shiro实现单用户安全功能的主要机制。
-
Subject:超类,提供用户的状态和安全操作。内含一个内部类
Builder
,是利用构造者模式,构建一个Subject
,不过需要注意的是,返回的Subject实例不会自动绑定到应用程序(线程)以供进一步使用。 也就是说,SecurityUtils.getSubject()
不会自动返回与构建器返回的相同实例。-
WebSubject:WebSubject表示作为传入ServletRequest的结果而获得的Subject实例。内部封装了request和response,并且重写了
Subject
的Buider
类,在父类的基础上进行了加了request和response的get/set方法 -
DelegatingSubject: Subject接口的实现,该接口将方法调用委托给底层SecurityManager实例进行安全检查。 它本质上是一个SecurityManager代理。此实现不维护角色和权限等状态(仅维护Subject principals,如用户名或用户主键),以在无状态体系结构中获得更好的性能。 相反,它每次都要求底层SecurityManager执行授权检查。
- WebDelegatingSubject:附加的默认WebSubject实现确保了保留servlet请求/响应对的能力,以便在请求执行期间内部shiro组件在必要时使用。
-
WebSubject:WebSubject表示作为传入ServletRequest的结果而获得的Subject实例。内部封装了request和response,并且重写了
6.1 SubjectFactory 工厂类
Subject制造工厂
-
DefaultSubjectFactory:利用SubjectContext的一系列resolve*方法获取相关数据并返回一个
DelegatingSubject
public Subject createSubject(SubjectContext context) {
SecurityManager securityManager = context.resolveSecurityManager();
Session session = context.resolveSession();
boolean sessionCreationEnabled = context.isSessionCreationEnabled();
PrincipalCollection principals = context.resolvePrincipals();
boolean authenticated = context.resolveAuthenticated();
String host = context.resolveHost();
return new DelegatingSubject(principals, authenticated, host, session, sessionCreationEnabled, securityManager);
}
- DefaultWebSubjectFactory:比父类多传递request和response
七. SecurityUtils
这个工具类是可以用来设置SecurityManager
获取SecurityManager
、Subject
的,是我们在真正使用Shiro
的时候最先使用到的工具
7.1 ThreadContext
SecurityUtils
是利用ThreadContext
来存储和获取SecurityManager
、Subject
的,而ThreadContext
是基于ThreadLocal
来实现的。
八. 主要方法
8.1 login
/**
* Performs a login attempt for this Subject/user. If unsuccessful,
* an {@link AuthenticationException} is thrown, the subclass of which identifies why the attempt failed.
* If successful, the account data associated with the submitted principals/credentials will be
* associated with this {@code Subject} and the method will return quietly.
* <p/>
* Upon returning quietly, this {@code Subject} instance can be considered
* authenticated and {@link #getPrincipal() getPrincipal()} will be non-null and
* {@link #isAuthenticated() isAuthenticated()} will be {@code true}.
*
* <pre>
*
* 为这个主题/用户执行登录尝试。 如果不成功,则抛出{@link AuthenticationException},其子类标识尝试失败的原因。 如果成功,与提交的主体/凭据相关联的帐户数据将与这个Subject相关联,方法将安静地返回。
*
* 在安静返回时,这个Subject实例可以被认为是经过身份验证的,{@link #getPrincipal() getPrincipal()}将为非空,{@link #isAuthenticated() isAuthenticated()}将为true。
* </pre>
*
* @param token the token encapsulating the subject's principals and credentials to be passed to the
* Authentication subsystem for verification.
* @throws org.apache.shiro.authc.AuthenticationException
* if the authentication attempt fails.
* @since 0.9
*/
void login(AuthenticationToken token) throws AuthenticationException;
根据方法的时序图我们可以发现:
实际调用
DelegatingSubject.login(AuthenticationToken token)
-
第一步:清除
runAs
状态。之前介绍
Subject
接口时,曾看到Subject
有个void runAs(PrincipalCollection principals) throws NullPointerException, IllegalStateException;
方法,是允许这个Subject无限期地“作为”或“假定”另一个身份。而登录方法首先去除了这种状态。可以发现去除这种状态是利用的Session.removeAttribute(Object key)
方法实现的。 -
第二步:执行登录操作。
-
首先调用
DefaultSecurityManager
的login
方法,而,SecurityManager
实际上是去调用了Authenticator
接口的authenticate(AuthenticationToken authenticationToken)
方法进行身份认证- 认证主要是调用了
AbstractAuthenticator.doAuthenticate(AuthenticationToken token)
,而这个方法是留给子类去实现的
- 认证主要是调用了
-
2. `ModularRealmAuthenticator`的`doAuthenticate`方法是其默认实现,其首先根据判断了注入的Realms不能为空,然后根据realms的个数,决定了是执行单个认证还是多个认证的逻辑。
![doAuthenticate时序图](https://upload-images.jianshu.io/upload_images/26562427-9ba8816d431b6d2d?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
1. 单个Realm的认证是我们在使用Realm中比较常用的。`ModularRealmAuthenticator`中的单个Realm认证流程主要依赖`AuthenticatingRealm.getAuthenticationInfo(AuthenticationToken token)`来完成。而这个方法是先从缓存中获取`AuthenticationInfo`,即用户信息,这里的缓存是使用的`Shiro`的`Cache`来实现。如果缓存中没有,就去持久层获取,这里的持久层获取逻辑是留给子类实现的,而子类实现的方法有很多种,像JDBC、LDAP等等,也可以自己去实现Realm重写这个逻辑,都是可以的。获取到账户信息后,开始校验账户是否匹配,而这个过程 是通过`CredentialsMatcher`实现的。而这里的匹配方式主要包括对账户Hash或者加盐等方式保证安全性后再校验等。
![getAuthenticationInfo时序图](https://upload-images.jianshu.io/upload_images/26562427-7831cdb9106898a6?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
2. 多Realm的场景在平时使用较少,认证逻辑跟上面单个Realm是一样的。区别就在于策略,这里是会根据策略来判断最后是否认证成功,策略配置是基于`AuthenticationStrategy`,可以配置的策略包括至少一个Realm匹配、所有Realm都匹配、第一个Realm匹配等几个策略,也可以自己进行扩展。
![doMultiRealmAuthentication时序图](https://upload-images.jianshu.io/upload_images/26562427-0884392ddf65bf33?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
-
然后
SecurityMangaer
进行了createSubject
(即创建Subject
)。- 创建
Subject
利用了DefaultSubjectContext
- 创建
Subject
时对SubjectContext
中的Session
、SecurityMangaer
、Principals
进行了填充,以确保后续创建Subject
时数据的完整性 -
Subject
最后实际封装了Session
、SecurityMangaer
、Principals
等信息 - 对生成的
Subject
进行了持久化(利用SubjectDao
)
- 创建
执行登录成功后的逻辑,在
DefaultSecurityManager
中执行的是rememberMe
的逻辑(登录失败会执行rememberMe的忘记逻辑,,注意,这里不是删除数据。而是将标识改为删除,拿CookieRememberMeManager为例,只是把cookie的value设置为deleteMe
),以CookieRememberMeManager
为例,是将用户的身份信息序列化为字节数组并加密,然后存储在cookie中将登录后的
principals
、host
、装饰后的session
保存在Subject
中。
8.2 getPrincipal系列方法:获取用户唯一标识(比如id)
通过登录接口记录的PrincipalCollection
来获取。主要逻辑是根据用户配置的Realm数量,认证生成了多个Principal
,即身份唯一标识(例如用户的ID),shiro将这些唯一标识存在PrincipalCollection
中,用户可以根据自己的需求去获取里面的身份
8.3 isPermitted系列方法: 鉴权
鉴权是调用SecurityManager
的鉴权方法,而其又调用authorizer
的鉴权方法。ModularRealmAuthorizer
是默认实现。以isPermitted
方法为例,
public boolean isPermitted(PrincipalCollection principals, String permission) {
assertRealmsConfigured();
for (Realm realm : getRealms()) {
if (!(realm instanceof Authorizer)) continue;
if (((Authorizer) realm).isPermitted(principals, permission)) {
return true;
}
}
return false;
}
shiro会去判断用户注入的所有的realm,来看有没有能鉴权成功的,只要有一个鉴权成功,那就认为用户有这个权限。这里可以认为是组合模式的应用,``ModularRealmAuthorizer就是那个集合。而真正执行鉴权逻辑的是
AuthorizingRealm,下面是
AuthorizingRealm`的具体执行逻辑
public boolean isPermitted(PrincipalCollection principals, String permission) {
//解析传入的用户需要的权限,并解析为`Permission`类
Permission p = getPermissionResolver().resolvePermission(permission);
return isPermitted(principals, p);
}
public boolean isPermitted(PrincipalCollection principals, Permission permission) {
//根据用户的唯一标识获取用户的权限信息(同样,这里是先从缓存中获取,再从持久化的地方获取,而这里的持久化方法是根据配置的具体的类或自己实现的 // //Realm的doGetAuthorizationInfo来获取的)
AuthorizationInfo info = getAuthorizationInfo(principals);
return isPermitted(permission, info);
}
//visibility changed from private to protected per SHIRO-332
protected boolean isPermitted(Permission permission, AuthorizationInfo info) {
//对用户有的权限和所执行方法需要的权限进行一个对比,看用户是否具有权限
Collection<Permission> perms = getPermissions(info);
if (perms != null && !perms.isEmpty()) {
for (Permission perm : perms) {
//还记得这个`implies`方法吗,可以去翻看之前的解析,看是如果进行权限匹配的
if (perm.implies(permission)) {
return true;
}
}
}
return false;
}
8.4 hasRole系列方法: 角色判断
角色判断的逻辑跟权限判断大体一致。以hasRole
方法为例,ModularRealmAuthorizer
遍历Realm并判断用户是否有use,如果有一个Realm有,那便认为用户有这个角色。
public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
assertRealmsConfigured();
for (Realm realm : getRealms()) {
if (!(realm instanceof Authorizer)) continue;
if (((Authorizer) realm).hasRole(principals, roleIdentifier)) {
return true;
}
}
return false;
}
然后去AuthorizingRealm
执行具体逻辑
public boolean hasRole(PrincipalCollection principal, String roleIdentifier) {
//获取用户的权限信息
AuthorizationInfo info = getAuthorizationInfo(principal);
return hasRole(roleIdentifier, info);
}
protected boolean hasRole(String roleIdentifier, AuthorizationInfo info) {
//判断用户已有的角色是否包含方法需要的角色
return info != null && info.getRoles() != null && info.getRoles().contains(roleIdentifier);
}
8.5 getSession 获取Session
具体执行的是DelegatingSubject
->SecurityManger
->SessionManger
的start
方法,最后默认生成的是一个SimpleSession
,并且在生成后,初始化了session的一些参数,然后通过SessionListener
进行了通知
//这个是AbstractNativeSessionManager的方法
public Session start(SessionContext context) {
Session session = createSession(context);
applyGlobalSessionTimeout(session);
onStart(session, context);
notifyStart(session);
//Don't expose the EIS-tier Session object to the client-tier:
return createExposedSession(session, context);
}
protected void applyGlobalSessionTimeout(Session session) {
session.setTimeout(getGlobalSessionTimeout());
onChange(session);
}
//这个是AbstractValidatingSessionManager的方法
protected Session createSession(SessionContext context) throws AuthorizationException {
//启用session定时调度器(调度器可以定时清理过期session)
enableSessionValidationIfNecessary();
//真正的创建session的方法
return doCreateSession(context);
}
//这个是DefaultSessionManager的方法
protected Session doCreateSession(SessionContext context) {
Session s = newSessionInstance(context);
if (log.isTraceEnabled()) {
log.trace("Creating session for host {}", s.getHost());
}
create(s);
return s;
}
//这个是DefaultSessionManager的方法
protected Session newSessionInstance(SessionContext context) {
return getSessionFactory().createSession(context);
}
//这个是SimpleSessionFactory的方法
public Session createSession(SessionContext initData) {
if (initData != null) {
String host = initData.getHost();
if (host != null) {
return new SimpleSession(host);
}
}
return new SimpleSession();
}
8.6 logOut 登出
清除了用户缓存的信息。