详细架构
下图显示了Shiro的核心架构概念,并简要概述了每个架构:
下面我们对除了核心组件的部分做一下简单的介绍:
Authentication(身份验证)
身份验证是验证用户身份的过程。
也就是说,当用户通过应用程序进行身份验证时,他们在证明自己实际上就是他们所说的身份。有时也称为“登录”。
这通常是一个三步过程。
收集用户的识别信息(称为主体)和支持身份证明的凭据(称为凭据)。
将主体和凭据提交到系统。
如果提交的凭据与系统对该用户身份(本金)的期望匹配,则认为该用户已通过身份验证。如果不匹配,则认为该用户未通过身份验证。
每个人都熟悉的此过程的一个常见示例是用户名/密码组合。当大多数用户登录软件应用程序时,通常会提供其用户名(主体)和支持密码(凭据)。如果存储在系统中的密码(或密码表示形式)与用户指定的密码匹配,则认为它们已通过身份验证。
Shiro以简单直观的方式支持相同的工作流程。
正如我们所说,Shiro具有以主题为中心的API-在运行时,您与Shiro所做的几乎所有事情都是通过与当前正在执行的Subject进行交互来实现的。
因此,要登录主题,您只需调用其登录方法,并传递一个AuthenticationToken实例,该实例代表所提交的主体和凭据(在本例中为用户名和密码)。
此示例在下面的清单5中显示。
- 列表5 主题登录
//1. Acquire submitted principals and credentials:AuthenticationToken token =new UsernamePasswordToken(username, password);//2. Get the current Subject:Subject currentUser = SecurityUtils.getSubject();//3. Login:currentUser.login(token);
如您所见,Shiro的API可以轻松反映常见的工作流程。
您会继续将这种简单性视为主题操作的所有操作的主题。
当调用登录方法时,SecurityManager将接收AuthenticationToken并将其分发给一个或多个配置的领域,以允许每个领域根据需要执行身份验证检查。
每个领域都可以根据需要对提交的AuthenticationToken做出反应。
但是,如果登录尝试失败会怎样?
如果用户指定了错误的密码怎么办?
您可以通过对Shiro的运行时AuthenticationException做出反应来处理故障,如清单6所示。
- 列表6-处理失败的场景
//3. Login:try {currentUser.login(token);} catch (IncorrectCredentialsException ice) { …} catch (LockedAccountException lae) { …}…catch (AuthenticationException ae) {…}
您可以选择捕获AuthenticationException子类之一并作出具体反应,或者一般性地处理任何AuthenticationException(例如,向用户显示通用的“用户名或密码错误”消息)。
选择取决于您,具体取决于您的应用程序需求。
主题成功登录后,它们被认为已通过身份验证,通常您允许他们使用您的应用程序。
但是,仅仅因为用户证明了自己的身份并不意味着他们可以在应用程序中做任何想做的事情。
这就引出了下一个问题:“如何控制允许用户执行或不执行的操作?” 确定允许用户执行的操作称为授权。
接下来,我们将介绍Shiro如何启用授权。
Authorization(授权)
授权本质上是访问控制-控制用户可以在应用程序中访问的内容(例如资源,网页等)。
大多数用户通过使用角色和权限等概念来执行访问控制。
也就是说,通常根据分配给他们的角色和/或权限,允许用户执行某项操作或不执行某项操作。
然后,您的应用程序可以根据对这些角色和权限的检查来控制公开哪些功能。
如您所料,主题API使您可以非常轻松地执行角色和权限检查。
例如,清单7中的代码片段显示了如何检查Subject是否被分配了特定角色。
- 清单7-角色检查
if ( subject.hasRole("administrator") ) {//show the ‘Create User’ button} else {//grey-out the button?}
如您所见,您的应用程序可以基于访问控制检查来启用或禁用功能。
权限检查是执行授权的另一种方法。
如上例所示,检查角色存在一个重大缺陷:您无法在运行时添加或删除角色。您的代码使用角色名称进行了硬编码,因此,如果更改了角色名称和/或配置,则代码将被破坏!如果您需要能够在运行时更改角色的含义,或者根据需要添加或删除角色,则必须依靠其他方式。
为此,Shiro支持其权限概念。
权限是对功能的原始说明,例如“开门”,“创建博客条目”,“删除’jsmith’用户”等。
通过使权限反映您的应用程序的原始功能,您只需更改权限检查何时更改应用程序的功能。反过来,您可以在运行时根据需要向角色或用户分配权限。
作为一个示例,如下面的清单8所示,我们可以重写之前的角色检查,而使用权限检查。
- 清单8-权限检测
if (subject.isPermitted("user:create") ) {//show the ‘Create User’ button} else {//grey-out the button?}
这样,分配了 user:create
权限的任何角色或用户都可以单击“Create User”按钮,并且这些角色和分配甚至可以在运行时更改,从而为您提供了非常灵活的安全模型。
“usercreate”字符串是遵循某些解析约定的权限字符串的示例。
Shiro的WildcardPermission支持此约定。
尽管不在本介绍文章的讨论范围之内,但您会发现WildcardPermission在创建安全策略时可以非常灵活,甚至还支持实例级访问控制之类的功能。
- 清单9-实例级别权限校验
if ( subject.isPermitted(“user:delete:jsmith”) ) {//delete the ‘jsmith’ user} else {//don’t delete ‘jsmith’}
此示例表明,如果需要,您可以控制甚至访问非常细致的实例级别。
如果愿意,您甚至可以发明自己的权限语法。
有关更多信息,请参见 Shiro权限文档。
最后,与身份验证一样,上述调用最终也进入了SecurityManager,后者将咨询一个或多个Realms来做出访问控制决策。 这使领域可以根据需要响应身份验证和授权操作。
这就是Shiro授权功能的简要概述。
尽管大多数安全框架都停止了身份验证和授权,但Shiro提供了更多功能。
接下来,我们将讨论Shiro的高级会话管理功能。
会话管理
Apache Shiro在安全框架领域提供了一些独特的功能:可在任何应用程序和任何体系结构层中使用的一致的Session API。
也就是说,Shiro为任何应用程序启用了会话编程范例-从小型守护程序独立应用程序到最大的群集Web应用程序。
这意味着希望使用会话的应用程序开发人员不再需要,则不再需要使用Servlet或EJB容器。或者,如果使用这些容器,则开发人员现在可以选择在任何层中使用统一且一致的会话API,而不是使用servlet或EJB特定的机制。
但是Shiro会话的最重要好处之一就是它们与容器无关。
这具有微妙但极其强大的含义。
例如,让我们考虑会话集群。有多少种特定于容器的方式可以将会话群集在一起以实现容错和故障转移?
Tomcat与Jetty的功能不同,而Jetty与Websphere的功能不同。但是,通过Shiro会话,您可以获得独立于容器的集群解决方案。
Shiro的体系结构允许可插入的Session数据存储,例如企业缓存,关系数据库,NoSQL系统等等。
这意味着您只需配置一次会话集群,无论部署环境如何(Tomcat,Jetty,JEE Server或独立应用程序),它都将以相同的方式工作。
无需根据部署应用程序的方式重新配置应用程序。
Shiro会话的另一个好处是,如果需要,会话数据可以跨客户端技术共享。
例如,如果需要,Swing桌面客户端可以参加相同的Web应用程序会话-如果最终用户同时使用这两个客户端,则很有用。
那么,您如何在任何环境中访问主题的会话?
如下面的示例所示,有两种Subject方法。
- Listing 10. Subject’s Session
Session session = subject.getSession();Session session = subject.getSession(boolean create);
如您所见,这些方法在概念上与HttpServletRequest API相同。
第一种方法将返回主题的现有会话,或者如果没有,则创建一个新的会话并返回。
第二种方法接受一个布尔参数,该参数确定是否将创建一个新的会话(如果尚不存在)。
获取主题的会话后,就可以将其几乎与HttpSession一样使用。
Shiro团队认为HttpSession API最适合Java开发人员,因此我们保留了大部分感觉。
当然,最大的区别是您可以在任何应用程序中使用Shiro Sessions,而不仅仅是Web应用程序。
清单11显示了这种熟悉程度。
- Listing 11. Session methods
Session session = subject.getSession();session.getAttribute("key", someValue);Date start = session.getStartTimestamp();Date timestamp = session.getLastAccessTime();session.setTimeout(millis);