2021SC@SDUSC
一. Apache Shiro是什么
Apache Shiro是一个强大、易用的Java安全框架。Apache Shiro的应用场景很广泛,从简单的命令行应用程序、移动App乃至企业级的网站或者应用程序都可以使用。
Shiro从以下四个方面提供API,这四个方面可以被认为是应用程序安全性的四大基石:
- 验证(Authentication):提供用户验证,也就是所谓的登录
- 鉴权(Authorization):访问控制
- 加密(Cryptography):保护数据的安全
- 会话管理(Session Management):每个用户有自己的会话时间状态
Shiro还支持一些辅助性的功能,例如网页应用安全管理、单元测试、多线程支持等等,但这些辅助功能只是为了增强上面四个方面的基础功能。
二. Apache Shiro的由来
对于一款框架来说,它的存在以及被选择的理由一定是它满足了其他选择无法提供的需求。
Shiro的前身是在2003年启动的JSecurity项目,2008年JSecurity项目加入了Apache软件基金会,此后改名为Shiro。
在2003年,Java开发者们几乎没有通用的安全框架,大家对于Java的认证鉴权服务(也就是JAAS,Java Authentication and Authorization Service)非常头疼。JAAS有很多缺点,虽然它的身份验证功能还说得过去,但是在鉴权方面的功能晦涩难懂、使用起来十分麻烦。而且JAAS和虚拟机层面的安全机制捆绑地很紧密,举个例子,它会决定一个类是否可以加载到JVM中。作为应用程序开发者,大家更关心的是程序在用户终端的功能,而不是代码在JVM中的行为。
最后需要解决的是加密问题。我们时常需要确保数据的安全性,但如果不具备相应的密码学知识,当时的Java加密架构简直是劝退,里面提供的API涉及到大量的异常处理,总之就是用起来让人恼火。
所以说基于2003年当时安全方面的发展形势,不难得知那时候根本没有一款能够满足这些所有需求的框架。因此JSecurity也就应运而生,发展成了现在的Shiro。
三. Apache Shiro的优势
- 易用(Easy To Use):易用性是这个项目的终极目标。维护应用程序的安全性通常十分复杂让人头大,但这又是很有必要的。如果框架可以简单到连萌新程序员都可以上手使用的话,那么维护程序的安全性也就不再是一件痛苦的事情。
- 全面(Comprehensive):Apache Shiro拥有其他安全框架无可比拟的全面功能,可以成为应用程序安全需求的一站式解决方案。
- 灵活(Flexible):Apache Shiro可以在任何环境下工作,如web、EJB、IoC。但Shiro的运行并不依赖这些环境,而且Shiro也没有强制的规范或者很繁琐的依赖。
- 支持Web(Web Capable):Apache Shiro可以很好地支持Web应用,可以基于应用的URL和网页协议(如REST)创建灵活的安全策略。同时提供JSP库来控制页面输出。
- 可扩展(Pluggable):Shiro简介的API和设计模式使得其能够很好地和其他框架或应用融合。Shiro可以很丝滑地和Spring、Grails、Wicket、Tapestry、Mule、Apache Camel、Vaadin等框架进行融合。
- 持续维护(Supported):Apache Shiro是Apache软件基金会的一个项目。项目的开发者和用户都十分友好,乐于助人。同时像Katasoft这样的商业公司也会向项目提供专业的支持和服务。
四. Shiro的三个核心概念:Subject、SecurityManager、Realms
-
Subject
在维护应用安全性的过程中,最经常涉及到的问题就是:“谁是当前用户?”或者“这个用户允许做某某事情吗?”。在写代码或者设计用户接口的时候,通常会考虑到应用程序是基于用户情景构建的,而且也会基于每个用户来制定功能。所以,为应用程序考虑安全问题时最自然的方法就是基于当前用户来考虑。Shiro的API在Subject这个概念上体现了
这种思维方式。Subject是一个安全术语,意思是“当前正在执行的用户”。不用User作为名字的原因是User通常和一个具体的人有关。在安全领域,Subject这个术语可以指代人,也可以指代第三方进程、守护进程或者其他类似的事物。简而言之,就是目前正在和软件交互的东西。在大多数情况下,可以将Subject理解为用户。我们可以在代码中的任何位置获取Subject。
import org.apache.shiro.subject.Subject; import org.apache.shiro.SecurityUtils; ... Subject currentUser = SecurityUtils.getSubject();
获取了Subject对象后就可以对当前用户进行大部分操作了,例如登录、登出、访问session、进行权限检查等等。这里的关键是Shiro的API很直观,因为它符合开发者按用户进行安全管理的直觉。
-
SecurityManager
Subject在背后对应的是SecurityManager。Subject对应对当前用户进行的安全操作,SecurityManager则是为所有用户管理安全选项。SecurityManager是Shiro架构的核心,充当一种“伞形”的对象,它引用了许多内部嵌套的安全组件,这些组件形成了一个对象图。然而,一旦SecurityManager和它内部的对象图被定义完成,它就会被晾在一旁,然后程序员会将大部分时间花在Subject下面的API中。
如何配置SecurityManager取决于应用程序的环境。比方说web应用通常在web.xml中指定一个Shiro Servlet过滤器,他会在其中将SecurityManager实例配置好。如果是运行一个独立的应用程序,则需要通过另一种途径进行配置。
通常每个应用只有一个SecurityManager实例。本质上就是单例模式(虽然无需是一个静态单例)。和Shiro中的其余部分一样,SecurityManager的默认实现方式是简单Java对象(POJO),可以通过任何与POJO兼容的配置机制来进行配置,如:普通的Java代码、Spring XML、YAML、.properties和.ini文件等等。基本上能够实例化一个类或者调用JavaBeans兼容的方法都可以使用。
为此,Shiro通过基于文本的ini文件配置提供了默认的配置方法。ini文件易于阅读和编写,而且只需要少量的依赖。
一个最简单基于ini文件来配置Shiro的例子:
[main] cm = org.apache.shiro.authc.credential.HashedCredentialsMatcher cm.hashAlgorithm = SHA-512 cm.hashIterations = 1024 # Base64 encoding (less text): cm.storedCredentialsHexEncoded = false iniRealm.credentialsMatcher = $cm [users] jdoe = TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJpcyByZWFzb2 asmith = IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbXNoZWQsIG5vdCB
我们可以看到在这个配置SecurityManager实例的ini文件中有两个部分:[main] 和 [users]
[main]部分是配置SecurityManager对象以及任何被SecurityManager使用的对象的地方,在这个例子中,配置了两个对象:- cm对象:Shiro HashedCredentialsMatcher类的一个实例。cm实例的各种属性都是以‘nested dot’语法配置的,这是IniSecurityManagerFactory使用的一种约定,用于表示属性配置以及定位对象。
- iniRealm对象:SecurityManager用于表示以INI格式定义的用户账户的组件。
[users]部分可以指定一个用户账户的静态列表,一般在构建简单的应用或者测试时会这么做。
加载.ini配置文件的例子:
import org.apache.shiro.SecurityUtils; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.util.Factory; ... //1. Load the INI configuration Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); //2. Create the SecurityManager SecurityManager securityManager = factory.getInstance(); //3. Make it accessible SecurityUtils.setSecurityManager(securityManager);
在这个例子中,我们可以看到大致的三个步骤:
- 加载将要配置SecurityManager及其组件的ini配置
- 基于配置创建SecurityManager实例,这个过程使用了Shiro的工厂方法设计模式。
- 让应用程序可以访问SecurityManager单例。在这个例子中,它被设置为了静态单例,但一般不需要这样。应用程序配置机制可以确定是否需要使用静态内存。
-
Realms
Realm充当着Shiro和应用中安全数据的桥梁。也就是说当需要与安全相关的数据进行交互时(例如验证和鉴权时需要和用户账户数据进行交互),Shiro通过配置好的一个或多个Realms进行查找。
在这个意义上,一个Realm本质上是一个安全专用的DAO(Data Access Object)。它封装了连接数据源的细节同时根据需要将相关数据提供给Shiro。在配置Shiro时,必须指定至少一个Realm来进行认证以及鉴权,当然也可以根据需要配置多个。
Shiro提供了开箱即用的Realm来连接各种数据源(目录)例如LDAP、关系型数据库(JDBC)、文本配置(如INI或配置文件)等等。当默认的Realm无法满足需求时还可以自定义Realm的实现。
通过ini配置使用LDAP目录作为Realms的例子:[main] ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm ldapRealm.userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com ldapRealm.contextFactory.url = ldap://ldapHost:389 ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5
五. 验证
验证是核对用户身份的过程,也就是我们常说的“登录”。这通常包括三个步骤:
- 收集用户的身份信息(称为principals-主体)以及支持身份信息的证明(称为credentials-凭据)
- 向系统提交主体和凭据
- 如果提交的凭据和系统中对应这个用户的身份信息(主体)相匹配,那么就通过验证,否则就验证失败。
验证过程一个常见而且被大家熟知的例子是用户名/密码组合。大多数用户在进行登录时都会提供用户名(主体)和密码(凭据)。如果系统中存储的密码(或是他的表现形式)与用户指定的密码匹配,那么就认为他们通过身份验证。
Shiro以一种直观的方式支持同样的工作流程。如前文所说,Shiro的API是以Subject为中心的。在运行时,几乎所有与Shiro相关的操作都是通过与当前正在执行的Subject交互来实现的。所以说一个Subject要登录,只需要简单地调用它的登录方法并向其中传递一个表示所提交的主体和凭据的AuthenticationToken实例,在下面的例子中主体和凭据分别为用户名和密码。
Subject登录的例子,非常简单直接:
//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);
一旦登录方法被调用,SecurityManager就会收到AuthenticationToken然后将其发送到配置好的Realm中根据每个用户的需要进行身份验证。每个Realm都能够按需对提交的AuthenticationToken作出反应。可以通过下面例子所示的方式来处理登录失败的情况。
处理登录失败:
//3. Login:
try {
currentUser.login(token);
} catch (IncorrectCredentialsException ice) {
…
} catch (LockedAccountException lae) {
…
}
…
catch (AuthenticationException ae) {
…
}
可以选择捕获AuthenticationException的子类来对每种错误进行不同的反应,或者笼统地捕获一个AuthenticationException异常。
Subject登录成功后,用户就被验明正身,可以开始使用应用程序了。但登录成功并不意味着用户可以在应用中为所欲为,这就引出了下一个问题:“如何控制用户能做的事情呢?”决定用户能做什么这件事就称之为鉴权。
六. 鉴权
鉴权的本质是访问控制——控制用户能访问应用的那些部分,如资源、网页等等。大多数用户使用角色、权限这样的概念进行访问控制。也就是说一个用户允许在应用中做什么是基于他们被分配的角色和权限。Subject的API可以很轻松地进行角色和权限检查。
一个检查角色的例子:
if ( subject.hasRole(“administrator”) ) {
//显示“创建用户”按钮
} else {
//让按钮变灰?
}
应用可以根据访问控制检查来启用或者禁用某些功能。
权限检查是另外一种鉴权的方式。像上面例子一样检查角色存在一个很致命的缺陷:无法在运行时增删角色。角色名被硬编码在了代码当中,所以如果需要改变角色名或者相关的配置,代码就会被破坏。
为此Shiro支持权限的概念。权限就是对功能直白的描述,比如“开门”、“创建博客的入口”、“删除a用户”等等。有了和应用的功能对应的权限后,改变应用程序功能时只需要更改权限检查即可。相反地,可以在运行时按需给用户或者角色分配不同权限。
我们可以将上一个检查角色的例子改为检查权限:
if ( subject.isPermitted(“user:create”) ) {
//显示“创建用户”按钮
} else {
//让按钮变灰?
}
这样,被分配了“user:create”权限的用户和角色就可以点击“创建用户”按钮了。这些角色和分配情况可以在运行时进行修改,提供灵活的安全模型。
“user:create”是一个遵守了解析约定的权限字符串,Shiro通过其通配符权限(WildcardPermission)支持这种开箱即用的约定。在创建安全策略时,WildcardPermission可以非常灵活,甚至支持实例级别的访问控制。
一个实例级别访问控制的例子:
if ( subject.isPermitted(“user:delete:jsmith”) ) {
//delete the ‘jsmith’ user
} else {
//don’t delete ‘jsmith’
}
例子展示了在需要的时候可以控制对单个资源的访问,甚至可以在实例级别进行细粒度的控制,也可以自定义一种权限控制的语法。最后,和验证一样,上面的调用会到达SecurityManager,由SecurityManager去和Realm打交道从而得到具体的结果。这允许Realm根据需要响应身份验证和授权的操作。
七. 会话管理
Apache Shiro提供了一些和别的安全框架不一样的功能:可以在任何应用以及任何架构层使用的一致的会话API,从一个小的独立守护进程到大型网页集群为任何应用程序提供了会话编程范例。意味着希望使用session的开发者不再强制使用Servlet或者EJB容器了,使用这些容器的话也可以选择在任何一层中使用这些统一、一致的会话API而不是拘泥于特定的Servlet或EJB的机制。
Shiro的session最重要的优点之一是它不依赖于容器,这有着微妙但及其重大的含义。例如,考虑一个会话集群,有多少种特定于容器的方法来集群会话以实现容错和故障切换?Tomcat、Jetty、Websphere都有不同的方法。但在Shiro的session中,可以得到独立于容器的集群解决方案。Shiro的架构支持可扩展的Session数据存储,例如企业级缓存、关系型数据库、NoSQL系统等等。这意味着只需一次性配置好会话集群,他就可以在任何环境下进行部署(如Tomcat, Jetty, JEE Server或独立的应用),不会因为环境的问题而产生不一致的表现,无需再根据不同环境重新进行配置。
Shiro session另一个好处是会话数据可以跨客户端进行共享。例如,Swing桌面客户端可以参与到一个相同的web应用的会话当中,这在终端用户同时使用这两种客户端时很有用。所以如何访问Subject的session呢?有两种方法:
Session session = subject.getSession();
Session session = subject.getSession(boolean create);
这和HttpServletRequest中的API一模一样。第一个方法会返回Subject中存在的会话,没有的时候就会新建一个然后返回。第二个方法接收一个布尔型的参数,这个参数用于指定当没有会话的时候是否需要新建一个然后返回。一旦得到了Subject的session,它的用法几乎和HttpSession一模一样。Shiro团队认为HttpSession API用起来是最舒服的,所以保留了这种风格。最大的不同就是可以在任何应用中使用Shiro Session。
和Session相关的一些方法:
Session session = subject.getSession();
session.getAttribute(“key”, someValue);
Date start = session.getStartTimestamp();
Date timestamp = session.getLastAccessTime();
session.setTimeout(millis);
...