Shiro原理讲解

Shiro原理讲解,第1张

Shiro原理讲解

文章目录
  • 前言
  • 源码下载
  • 模块讲解
  • Authenticator 认证器
    • AuthenticationToken 认证标识
    • AuthenticationInfo 认证信息
    • AuthenticatorRealm 认证Realm
  • Authorizer 授权器
    • AuthorizerRealm 授权Realm
  • SessionManager 会话管理器
    • NativeSessionManager 本地会话管理器
    • ServletContainerSessionManager 容器会话管理器
  • SecurityManager 安全管理器
    • 总结
  • Shiro过滤器

前言

本文假设读者对Shiro有部分了解,并且有使用经历。对于一个框架首先要会用它,再来看原理就透彻多了。如果没用过可以看看这篇SpringBoot使用Shiro。

源码下载

下载地址,本文讲解的是1.7.1,我下载的也是这个版本。版本最好和我相同,要不然Shiro新增或修改的一些功能不会再这篇文章中体现出来。

模块讲解

下载下来,用开发工具打开

可以看到Shiro的模块非常的多。但其实主要模块就3个,被我选中标记了

  • core:核心模块,权限认证实现都在这个模块
  • web:对web支持的模块
  • support:对其他框架的支持模块

其他模块

  • all:实现打包shiro-all
  • cahce:shiro缓存,默认实现了本地缓存
  • config:shiro配置支持,支持ini文件配置和ogdl结构化纯文本格式
  • crypto:shiro加密工具包
  • event:shiro事件处理
  • integration-test:集成测试
  • lang:提供一些类和工具类
  • samples:示例
  • test-coverage:覆盖率统计
  • tools:提供工具类

Shiro使用外观模式,SecurityManager包含系统的所有功能,他的实现树如下:

我将依次讲解:

  • Authenticator:认证器
  • Authorizer:授权器
  • SessionManager:会话管理器
  • SecurityManager:安全管理器
Authenticator 认证

认证器位于code包下的authc下。对于安全系统,最主要的功能是如何获取访问者的信息,并与我们所设立的白名单中的用户相匹配。Authenticator对于Shiro来说就是定义这样一个功能,他的方法如下:


只有一个认证方法,接收一个AuthenticationToken,返回一个认证信息。他的实现树如下:


忽略AbstractAuthenticator,因为他将authenticate(),委托给了内部的抽象方法doAuthenticate(),最终由ModularRealmAuthenticator实现,具体如下:
如上图注释所说,ModularRealmAuthenticator又将认证器具体获取的信息交给了Realm实现。

doSingleRealmAuthentication(Realm,AuthenticationToken)具体实现如下:

  1. !realm.supports(token)
    判断Realm是否支持此Token,不支持则抛出异常
  2. AuthenticationInfo info = realm.getAuthenticationInfo(token);
    如果支持则使用realm获取认证信息

doMultiRealmAuthentication()实现与之类似,只不过增加了多Realm策略:

  • AllSuccessfulStrategy:所有Realm成功获取才通过
  • AtLeastOneSuccessfulStrategy:只要有一个Realm成功就通过
  • FirstSuccessfulStrategy:必须第一个Realm成功才通过

默认使用 AtLeastOneSuccessfulStrategy

AuthenticationToken 认证标识

接下来我们来了解认证器使用的Token到底是什么,有什么功能,下面是shiro内置的Token:

  • AuthenticationToken
    认证Token的基本接口,包括下面两个方法
    • getCredentials() :获取认证凭证,一般来说是密码
    • getPrincipal() :获取认证主体,一般来说是用户名
  • RememberMeAuthenticationToken extends AuthenticationToken
    记住我认证Token,只是标识一个Token认证后的用户是否要被记住。继承AuthenticationToken
    • isRememberMe() :是否记住我
  • HostAuthenticationToken extends AuthenticationToken
    主机名或ip认证Token。继承AuthenticationToken
    • getHost() :获取主机名或 ip
  • UserNamePasswordToken implements RememberMeAuthenticationToken, HostAuthenticationToken
    用户名密码Token
  • BearerToken implements HostAuthenticationToken
    凭证Token,持有一个字符串类型的认证码。实现 HostAuthenticationToken

每一个Realm都有他支持的Token,通过support(AuthenticationToken)就可以知道他是否支持此token。

AuthenticationInfo 认证信息

AuthenticationInfo接口 是 authenticate() 的返回值,他的结构非常简单:

  • PrincipalCollection getPrincipals();
    返回一个主体集合,只会在全部成功策略并拥有多个Realm时才会有多个主体。
  • Object getCredentials();
    获取主体的凭证,通俗点就是密码

他的几个实现类也非常简单,不做过多赘述,有兴趣的可以自己看看。一般使用 SimpleAuthenticationInfo 作为返回值

AuthenticatorRealm 认证Realm

之前我们说 获取用户的信息会委托到Realm,AuthenticatorRealm 是委托的具体类,他是一个抽象类,实现他来完成自己的自定义认证Realm,下面是他的实现树和方法

Realm.getAuthenticationInfo()只有一个实现:AuthenticatingRealm
进入到方法体内

我们逐句分析

  1. AuthenticationInfo info = getCachedAuthenticationInfo(token);

    首先,获取缓存中的认证信息。什么意思呢?
    就是说你用 admin+123456 创建了一个token登录,他在缓存里用 admin 找一下,看看你有这个认证信息没有,有就直接返回了。没有就进入下一步

  2. info = doGetAuthenticationInfo(token);

    这个获取就是要委托子类,也就是抽象方法。是要委托给我们自己写的realm,当然shiro他自己也有实现很多realm,但是我们一般不用

  3. cacheAuthenticationInfoIfPossible(token, info);

    缓存认证信息,这个方法他有个判断,判断你是否开启缓存,默认是关闭的。也就是说你没具体设置过第一步是不能获取到任何信息的,还是需要进入到第 2 步。开启缓存有利有弊,利就是重复登录不再需要进入第 2 步,弊是编码要复杂了,修改密码了,你还要把这个缓存同步

  4. assertCredentialsMatch(token, info);

    认证信息中的密码和token中的是否匹配,不匹配抛出一个 IncorrectCredentialsException 异常。默认的密码匹配器是很简单的字符匹配,就是匹配两个字符是否相同,所以说如果你数据库中密码是 HASH 过的,你在构建 info 时密码就使用用户的输入,判断在Realm中做好。或者你可以使用 HashedCredentialsMatcher来进行密码匹配

Authorizer 授权器

授权器的实现和认证器非常相似,也是将授权信息的获取委托给Realm。下面是Authorizer接口的方法列表:

别看方法非常多,其实就是两个功能:查看这个主体也就是登录的用户有没有这个角色,查看这个主体有没有这个权限
接口的实现树:

是不是和Authemticator非常相似,但他的实现下面多了一个AuthorizingRealm,这个我们之后再讲。我们看一下 isPermitted(PrincipalCollection,String)

  1. assertRealmsConfigured();
    断言Realm有没有,没有就报错
  2. !(realm instanceof Authorizer)
    判断这个Realm是不是一个授权Realm
  3. ((Authorizer) realm).isPermitted(principals, permission)
    使用Realm判断有没有此权限

可以看到,和认证器不同,授权器对多个授权Realm使用的策略是只要有一个可以通过授权,就算做成功。

AuthorizerRealm 授权Realm

Authorizer使用的Realm 为 AuthorizerRealm ,他继承于AuthenticatorRealm 。也就是说,实现授权Realm 也需要实现认证Realm。看一看他的关键方法 AuthorizationInfo getAuthorizationInfo(PrincipalCollection)

  1. Cache cache = getAvailableAuthorizationCache();
    获取授权的缓存,如果没有开启为null。
  2. Object key = getAuthorizationCacheKey(principals);
    获取缓存的key,默认实现是返回主体,也就是 PrincipalCollection 类
  3. info = doGetAuthorizationInfo(principals);
    doGetAuthorizationInfo(principals); 是一个抽象方法,委托到子类实现的。是我们自定义Realm需要实现的
  4. cache.put(key, info);
    缓存不为空的情况下添加到缓存。值得注意的是,当你修改了用户的权限,你需要更新缓存

然后让我们回到AuthorizingRealm中,他实现了Authorzing ,我们来看一下isPermitted(PrincipalCollection,Permission)这个方法是怎么做的

  1. AuthorizationInfo info = getAuthorizationInfo(principals);
    这个调用就是我们上面说的,根据主体获取权限信息
  2. return isPermitted(permission, info);
    这里直接调用了一个内部类,调到第 3 步
  3. Collection perms = getPermissions(info);
    从授权信息中获取 权限列表
  4. perm.implies(permission)
    判断权限列表是否包含要访问的权限

现在我们大概知道了认证的信息和权限是怎么获取的,也清楚我们的Realm是如何被调用生效的。接下来就是Shiro是如何将我们登录的状态保存。

SessionManager 会话管理器

shiro有两种类型的Session,一种是NativeSession本地会话,也就是Shiro自己实现的;一种是ServletContainerSession容器会话,也就是直接用容器的Session,比方说你是用Tomcat,shiro用的就是tomcat的Session。

NativeSessionManager 本地会话管理器

讲解之前先明确几个功能接口

  1. SessionFactory:会话工厂,专门使用此接口来创建Session
  2. SessionDao:会话储存,会话的保存和获取都是此接口的功能
  3. SessionListener:会话监听器,监听 会话的启动、停止、到期

请求时获取会话的流程:

  1. 如何获取的SessionId?

    使用客户端传输的cookie获取,shiro在创建Session后会将 set-cookie响应头添加到响应中去,下次请求客户端就会携带带有SessionId的cookie

  2. SessionDao如何保存Session?

    使用shiro Cache保存,shiro默认实现有 MapCache,EhCache。你可以自定义实现自己的缓存,如使用Redis,Mysql

  3. SessionListener有什么用?

    实现自己的监听器,在Shiro Session的创建、停止、到期时实现自己的逻辑。监听器只有在使用本地会话管理器的时候才会生效。

  4. Session如何过期?

    每次获取或生成Session时都会判断有没有启动 SessionValidationScheduler,没有启动则启动。他所做的就是周期性的监测Session有无过期,过期则删除。同样,此功能只有在使用本地Session才会开启,因为使用容器Session时,容器已经帮我们做到这一点了。

ServletContainerSessionManager 容器会话管理器

Shiro对容器会话并没有太多 *** 作,只是将其封装了一下。像会话的生成,到期这些功能全都由容器做好了。

SecurityManager 安全管理器

现在我们将上面串联起来,结合安全管理器。

shiro将每一个客户端的请求都抽象成了一个Subject,利用SecurityUtils.getSubject()可以获取当前的Subject,下面看看他的代码

  1. Subject subject = ThreadContext.getSubject();
    ThreadContext内使用ThreadLocal存储一个Map,这段代码就是获取当前线程的Subject
  2. subject = (new Subject.Builder()).buildSubject();
    如果当前线程Subject为空的话,那么使用SubjectBuilder构建一个,待会我们进入到 buildSubject()方法,看看到底是怎么实现的
  3. ThreadContext.bind(subject);
    将Subject绑定到当前的线程

进入到buildSubject():

subjectContext实现Map,是一个包含认证信息、是否认证、sessionId的信息类。最终创建Subject还是委托到了SecurityManager.createSubject(SubjectContext),进入:

  1. SubjectContext context = copy(subjectContext)
    上面说过SubjectContext实现Map,这里就是复制一份

  2. context = ensureSecurityManager(context);

    判断一下SecurityManager是不是空,是空就把自己赋值给context。

  3. context = resolveSession(context);

    这里我们重点看 红框标记的 Session session = resolveContextSession(context);

    先从context中获取一个SessionKey,也就是SessionId,注意这里使用的是SecurityManager的方法,他可以被子类继承重写。getSession(key),直接委托到了SessionManager,不细讲。

  4. context = resolvePrincipals(context);

    context.resolvePrincipals():这里是从上一步resolveSession中获取的Session里去取Principal,如果Session为空,这里就直接到下一步了。
    getRememberedIdentity(context):获取记住我的值,从web上来说,实现的是读取客户端传过来的cookie,然后进行解析

  5. Subject subject = doCreateSubject(context);

    获取一个工厂创建Subject,具体实现就是把context的值复制给Subject。非常简单,不过多讲述了

  6. save(subject);

    进入subjectDAO.save(subject);


    isSessionStorageEnabled(subject):判断一下subject是否要Session存储
    saveToSession(subject):保存到Session
    进入到saveToSession(subject);

    合并认证主体到Session,合并认证状态到Session。

总结

安全管理器本身逻辑非常少,只做一个整合功能。这也符合了外观模式的观点,将一个复杂系统的各类接口整合,提供一个统一的界面。上述功能实现其实忽略了一点东西,为了更好的结合使用体验,可以模糊了core模块和web模块之间的界限。想要得到更详细更正确的理解,还是得自己阅读源码,本文只提供一个大概脉络。

Shiro过滤器

我们在代码内部使用 SecurityUtils.getSubject();总能获取到完整的Subject。但根据上面的代码来看,SecurityUtils.getSubject();只能提供一个空的Subject,里面甚至都没有Session,这代表我们登录状态根本不会保存。这一步其实Shiro已经给我们做好了,它使用Web Filter在所有请求处理之前将Subject提供给当前线程,这样我们在代码中使用就无需自己创建。

下文的容器指实现 javax.servlet 包的程序,如Tomcat

我个人认为ShiroFilter分为两种:

  1. 拦截请求为Shiro生成环境,并执行第二类过滤器,此类Filter会暴露给容器
  2. Shiro的内部功能处理过滤器,如实现 认证过滤器、权限过滤器。这类Filter不会暴露给容器。

Shiro就是在第一种过滤器中生成Subject,并绑定到当前线程,使得SecurityUtils.getSubject();能返回带有Session的Subject。

转到AbstractShiroFilter.doFilterInternal()方法

createSubject(request, response)结合之前的,这里创建一个Subject并给予一个请求和响应,在这里shiro为我们创建好了一个Subject,并赋值了必须的参数,

接着,我们进入 subject.execute(Callable)看看它的源码


Callable可使用多线程调用并返回Futrue值,但这里直接调用没有使用多线程调用,说明shiro只是把Callable当做一个任务类。
接着我们转到associateWith(),看看是怎么构建这个Callable的。


把subject自身和传入的Callable通过构造函数构建一个SubjectCallable

继续进入SubjectCallable类

  1. 通过传入Subject构建一个ThreadState类、并把Callable存储到当前类

    我们转入ThreadState

    原来他就是将当前subject绑定到ThreadContext中,也就是当前线程的ThreadLocal。

  2. 在callable调用之前将Subject绑定,也就是call()方法内调用ThreadContext.getSubject()都能获取到传入的Subject。之后在执行完毕,调用ThreadState.restore(),清空ThreadContext并复原。

回到AbstractShiroFilter

  1. updateSessionLastAccessTime(request, response);
    更新Session最后访问时间,这个功能最后会委托到Session.touch()
  2. executeChain(request, response, chain);
    执行过滤链。shiro会将到此的链代理,只有在自己的功能性过滤器,也就是上面说的第2种过滤器执行完毕后,才会执行容器的过滤器链。在call方法体内的所有代码使用ThreadContext都能获取当前的Subject,这样Shiro在servlet开始之前就将Subject提供给了当前线程。同样的,如果你想要在自定义Filter中使用Subject的话,要么你自己创建,要么将自定义Filter执行顺序放到ShiroFilter之后。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5686502.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存