原文:http://lengyun3566.iteye.com/category/153689?page=2
最近阅读了《Spring Security3》一书,颇有收获(封面见图片)。因此将其部分内容翻译成中文,对于电子版内容,本人放弃一切权利。
在翻译之前,本人曾发邮件征询原作者的意见,如今已是半月有余,未见其回复。因为非盈利为目的,所以斗胆将内容发布于博客之上。
这是我第一次翻译东西,有很多地方觉得翻译的很生硬,希望阅读到的朋友们踊跃拍砖。
我的新浪微博:http://weibo.com/1920428940
本书源代码的地址:http://www.packtpub.com/support?nid=4435
全文下载地址:http://lengyun3566.iteye.com/blog/pdf
毫无疑问,安全是任何一个写于21世纪的web工程中最重要的架构组件之一。在这样一个时代,计算机病毒、犯罪以及不合法的员工一直存在并且持续考验软件的安全性试图有所收益,因此对你负责的项目综合合理地使用安全是至关重要的一个元素。
本书的写作遵循了这样的一个开发模式,这个模式我们感觉提供了一个有用的前提来解决复杂的话题——即使用一个基于Spring3的web工程作为基础,以理解使用Spring Security3使其保证安全的概念和策略。
不管你是不是已经使用Spring Security还是只是对这个软件有兴趣,就都会在本书中得到有用的信息。
在本节的内容中,你能够:
l 检查一个虚拟安全审计的结果
l 讨论web应用通常的一些安全问题
l 学习软件安全中的几个核心词汇和概念
如果你已经熟悉基本的安全术语,你可以直接跳到第二章:Spring Security起步,在哪里我们将涉及这个框架的细节。
这是你作为软件开发人员在Jim Bob Circle Pant Online Pet Store(JBCPPets.com)工作的一个清晨,你正在喝你的第一杯咖啡的时候,收到了你主管的以下邮件:
To: Star Developer <[email protected]>
From: Super Visor <[email protected]>
Subject: 安全审计
Star,
今天,有一个第三方的安全公司要审计我们的电子商务网站。尽管我知道你在设计网站的时候已经把安全考虑在内了,但请随时解决他们可能发现的问题。
Super Visor
什么?你在设计应用的时候并没有过多考虑安全的问题?似乎你有许多的东西要向安全审计人员学习。首先,让我们花一点时间检查一下要审计的应用吧。
尽管在本书的后续内容中我们要模拟虚拟的场景,但这个应用的设计以及我们对其进行的改造都是基于现实世界中真实使用Spring的工程。
这个应用的设计很简单,使得我们能够关注于重要的安全方面而不会过多关注ORM的细节和复杂的UI技术。我们期望你能够参考其他的资料来掌握示例代码中所涉及功能的技术。
代码是基于Spring和Spring Security3编写而成的,但是它很容易改变为Spring Security2的例子。关于Spring Security 2和3的细节差异,可以参照第十三章:迁移至Spring Security,可以作为将示例代码转换成Spring Security2的帮助材料。
不要以本应用作为基础去构建真实的Pet Store在线应用。本应用已经有意识的构建地简单并关注于本书所要阐述的概念和配置。
本应用遵循标准的三层结构,包括web层、服务层和数据访问层,如下图所示:
web层封装了MVC的代码和功能。在示例代码中,我们使用了Spring MVC框架,但是我们可以一样容易的使用Spring Web Flow,Struts甚至是一个对Spring友好的web stack如Apache Wicket。
在一个典型使用Spring Security的web应用中,大量配置和参数代码位于web层。所以,如果你没有web应用开发,尤其是Spring MVC的经验,在我们进入更复杂的话题前,你最好仔细看一下基础代码并确保你能理解。再次强调,我们已经尽力让我们的应用简单,把它构建成一个pet store只是为了给它一个合理的名字和轻量级的结构。可以将其与复杂的Java EE Pet Clinic示例作为对比,那个示例代码展现了很多技术的使用指导。
服务层封装了应用的业务逻辑。在示例应用中,我们在数据访问层前做了一个很薄的façade用来描述如何在特殊的点周围保护应用的服务方法。
在典型的web工程中,这一层将会包括业务规则校验,组装和分解BO以及交叉的关注点如审计等。
数据访问层封装了操作数据库表的代码。在很多基于Spring的工程中,这将会在这里发现使用了ORM技术如hibernate或JPA。它为服务层暴露了基于对象的API。在示例代码中,我们使用基本的JDBC功能完成到内存数据库HSQL的持久化。
在典型的web工程中,将会使用更为复杂的数据访问方式。因为ORM,即数据访问,开发人员对其很迷惑。所以为了更清晰,这一部分我们尽可能的对其进行了简化。
我们使用了一些每个Spring程序员都会遇到的技术和工具,以使得示例应用很容易的运行起来。尽管如此,我们还是提供了补充的起步资料信息在附录:参考资料。
我们建立使用如下的IDE以提高开发的效率并使用本书的示例代码:
<!--[if !supportLists]-->l <!--[endif]-->Eclipse 3.4或3.5 Java EE版本可以在以下地址获得:
http://www.eclipse.org/downloads/
<!--[if !supportLists]-->l <!--[endif]-->Spring IDE2.2(2.2.2或更高)可以在以下地址获得:http://springide.org/blog
在本书的示例和截图中,你会看到Eclipse和Spring IDE,所以我们建议你使用它们。
你可能希望使用免费的Spring Tool Suite(STS)Eclipse插件,它作为Eclipse的一个插件由Spring Source开发,其包含了下一代的Spring IDE功能(可以在以下地址下载:http://www.springsource.com/products/springsource-tool-suite-download)。一些用户不喜欢它的侵入性和SpringSource的标示,但是你如果从事Spring相关的开发,它提供了很多有用的功能。
我们提供了Eclipse3.4兼容的工程以允许你在Eclipse中构建和部署代码到Tomcat6.x的服务器上。鉴于大多数开发人员熟悉Eclipse,所以我们觉得这是最直接的方式来打包示例代码。我们为这些例子提供了Apache Ant的脚本以及Apache Maven的modules。不管你熟悉开发环境,我们希望你能够在阅读本书的时候能够运行示例代码。
另外,在阅读过程中,你可能会愿意去下载Spring 3和Spring Security 3的源码版本。如果你有不明白的地方或想获取更多的信息,他们的JavaDoc和源码是最好的参考资料,他们提供的示例也能够提供额外的帮助并消除你的疑惑。
让我们回到e-mail并看一下审计的进展。哦,结果貌似并不好啊:
To: Star Developer <[email protected]>
From: Super Visor <[email protected]>
Subject: FW: Security Audit Results
Star,
看一下审计结果并制定一个计划来解决这些问题。
Super Visor
审计结果:
本应用存在如下的不安全隐患:
<!--[if !supportLists]--> l <!--[endif]-->缺少URL保护和统一的认证造成的权限扩散;
<!--[if !supportLists]--> l <!--[endif]-->授权不合理甚至缺失;
<!--[if !supportLists]--> l <!--[endif]-->数据库认证信息不安全且很容易获取;
<!--[if !supportLists]--> l <!--[endif]-->个人的识别信息和敏感数据很容易获取或没有加密;
<!--[if !supportLists]--> l <!--[endif]-->不安全的传输层保护,没有使用SSL加密;
<!--[if !supportLists]--> l <!--[endif]-->危险等级:高
我们希望本应用在解决这些问题前能够下线。
哦,这些结果看起来对我们公司很不利,我们最好尽快将这些问题解决。
公司(或他们的合作伙伴、顾客)会雇佣第三方的安全专家来审计他们软件的安全性。审计过程会联合使用破解,代码检查以及与开发人员和架构师的正式或非正式交流。
安全审计的通常目标是保证基本的安全开发措施被遵守以实现客户数据和系统功能的完整性和安全性。依靠软件业所追求的工业化目标,审计人员可能会使用工业化的标准或特定的方式来进行这些测试。
收到这样的安全审计结果可能是一件令人很吃惊的事情,但是,如果你按照被要求去做,这将会是一个绝佳的机会来学习和提升软件质量,同时,这将会引领你去实现一些常规的策略来保证软件的安全性。
让我们来看一下审计人员发现的问题并制定一个计划去解决他们。
缺少URL保护和统一的认证造成的权限扩散
认证是在开发安全应用中,你必须记住的两个关键词之一(另外一个是授权)。认证识别系统中的某一个用户,并将其与一个可信任的(即安全的)实体关联。一般来讲,软件系统会被分为两个层次的访问范围,如未认证通过的(或匿名的)和认证通过的,如下图所示:
匿名可访问的应用功能是用户无关的(如一个在线商店的产品列表)。
匿名区域不会:
<!--[if !supportLists]-->l <!--[endif]-->要求用户登录系统;
<!--[if !supportLists]-->l <!--[endif]-->展示敏感信息如人名、地址、信用卡号、订单等;
<!--[if !supportLists]-->l <!--[endif]-->提供管理系统全局的状态或数据。
未认证的系统区域可供任何人使用,即使该用户没有被系统所识别。但是,有可能对于已认证的用户会看到更多的信息(如随处可见的欢迎{名字}文本)。Spring Security的标签库可以完全支持对登录用户进行有区别的显示,这部分内容将在第五章:精确的访问控制中介绍。
我们将在第二章中解决这个发现的问题并使用Spring Security的自动配置功能实现一个基于表单的认证。更复杂的表单认证(通常用来用在与企业系统集成或外部的认证存储)将在本书的第二部分进行讲解,这部分从第八章:对OpenID开放开始。
授权不合理甚至缺失
授权是两个重要安全概念中的第二个,它对实现和理解应用安全很重要。授权保证授权过的用户能够对功能和数据进行恰当的访问。构建应用的安全模块的主要任务是拆分应用的功能和数据并将权限、功能和数据、用户结合起来,以实现对这些内容的访问能够被很好的控制。在审计中,我们的应用在这一点的失败表明应用的功能没有按照用户角色进行限制。试想一下,你正在运行一个在线购物系统,查看、取消或修改订单以及用户的信息对所有的用户可见!(这将是多么恐怖的事情)
我们将会在第二章通过使用Spring Security的授权模块解决基本的授权问题,接着会有关于授权更高级的知识介绍,其中在第五章介绍web层,在第六章:高级配置和扩展中介绍业务层。
数据库认证信息不安全且很容易获取
通过检查应用的源码和配置文件,审计人员指出用户的密码在配置文件中以明文的显示存储,这会导致恶意的用户很容易通过访问到服务器进而访问应用。
因为应用中包含了个人和财务的信息,一个恶意的用户如果能够非法访问任何数据都会导致公司信息的泄露。保护能够进入系统的凭证信息对我们来时是最重要的事情,最重要的第一步是保证在安全上一个点的失败不会连累整个系统。
我们将会在第四章:凭证安全存储中检查Spring Security的数据访问层以实现凭证存储的安全配置,那里将会用到JDBC连接。在第四章中,我们同时也会学习一些内置的技术以增强保存在数据库中密码的安全。
个人的识别信息和敏感数据很容易获取或没有加密
审计人员指出一些重要和敏感的数据,包括信用卡号,在系统中的任何地方都没有加密或混淆。这除了是缺乏数据设计的一种典型表现外,对信用卡号缺少保护也是违反PCI标准的。
幸运的是,基于Spring Security的注解式AOP支持,我们可以使用一些简单的设计模式和工具来安全的保护这些信息。我们将会在第五章中阐述在数据访问层保护这种数据的一些技术。
不安全的传输层保护,没有使用SSL加密
在现实世界中,很难想象一个在线的商业网站不使用SSL保护;不幸的是,我们的JBCP Pet正是这样。SSL保护能够保证在客户端浏览器和web应用服务器之间的通信是安全的,可以防护多种的信息篡改和窥探。
在第五章中,我们将会介绍如何在应用的安全架构中使用传输层安全配置,包括规划应用的那些部分要使用强制的SSL加密。
Spring Security提供了丰富的资源,许多安全的通用问题都可以用非常简单的声明或配置来解决。在接下来的章节中,我们将会通过源代码和应用配置的不断改进来解决安全审计人员提出的所有问题(甚至更多),同时会对我们应用的安全性树立信心。
使用Spring Security 3,我们将会通过以下的变化来提高应用的安全性:
<!--[if !supportLists]-->l <!--[endif]-->细分系统的用户类别;
<!--[if !supportLists]-->l <!--[endif]-->为用户的角色授予不同级别的权限;
<!--[if !supportLists]-->l <!--[endif]-->为不同的用户类别授予不同的角色;
<!--[if !supportLists]-->l <!--[endif]-->为应用全局的资源使用认证规则;
<!--[if !supportLists]-->l <!--[endif]-->为应用所有层的结构使用授权规则;
<!--[if !supportLists]-->l <!--[endif]-->阻止常用的攻击手段以控制或窃取一个用户的session。
Spring Security的存在填补了众多java第三方类库的一个空白,正如Spring框架第一次出现时那样。一些业界的标注如JAAS或Java EE Security确实提供了一些方式来解决认证和授权问题,但是Spring Security是一个胜出者,因为它以一种简明和合理的方式封装了各个层的安全解决方案。
除此以外,Spring Security提供了与很多通用企业认证系统的内置集成支持。所以,对开发者来说,它通过很少的努力就能适应绝大多数的场景。
因为没有任何一款主流的框架与之匹敌,所以它被广泛的使用。
在本章中,我们:
<!--[if !supportLists]-->l <!--[endif]-->检查了一个不安全的web工程的风险点;
<!--[if !supportLists]-->l <!--[endif]-->查看了示例在线商务应用的基本架构;
<!--[if !supportLists]-->l <!--[endif]-->讨论了使示例工程安全的策略。
在第二章中,我们将会介绍Spring Security的整体架构,主要侧重于其如何扩展和配置以支持我们的应用的需要。
在本章中,我们将要学习Spring Security背后的核心理念,包括重要的术语和产品架构。我们将会关注配置Spring Security的一些方式以及对应用的作用。
最重要的是为了解决工作中的问题,我们要开始使得JBCP Pets的在线商店系统变得安全。我们将会通过分析和理解认证如何保护在线商店的适当区域来解决在第一章:一个不安全应用的剖析中审计人员发现的第一个问题,即缺少URL保护和统一的认证造成的权限扩散。
在本章的内容中,我们将会涉及:
l了解应用中安全的重要概念;
l使用Spring Security的快速配置功能,为JBCP Pets在线商店实现基本层次的安全;
l理解Spring Security的全貌;
l探讨认证和授权的标准配置和选项;
l在Spring Security访问控制中使用Spring的表达式语言(Spring Expression Language)
由于安全审计结果的启示作用,你研究了Spring Security并确定它能够提供一个坚实的基础,以此可以构建一个安全的系统来解决在安全审计JBCP Pet在线商店中发现的问题,而那个系统是基于Spring Web MVC开发的。
为了Spring Security的使用更高效,在开始评估和提高我们应用的安全状况之前,先了解一些关键的概念和术语是很重要的。
正如我们在第一章所讨论的那样,认证是鉴别我们应用中的用户是他们所声明的那个人。你可能在在线或线下的日常生活中,遇到不同场景的认证:
l 凭据为基础的认证:当你登录e-mail账号时,你可能提供你的用户名和密码。E-mail的提供商会将你的用户名与数据中的记录进行匹配,并验证你提供的密码与对应的记录是不是匹配。这些凭证(用户名和密码,译者注)就是e-mail系统用来鉴别你是一个合法用户的。首先,我们将首先使用这种类型的认证来保护我们JBCP Pet在线商店的敏感区域。技术上来说,e-mail系统能够检查凭证信息不一定非要使用数据库而是各种方式,如一个企业级的目录服务器如Microsoft Active Directory。一些这种类型的集成方式将在本书的第二部分讲解。
l 两要素认证:当你想从自动柜员机取钱的时候,你在被允许取钱和做其他业务前,你必须先插卡并输入你的密码。这种方式的认证与用户名和密码的认证方式很类似,与之不同的是用户名信息被编码到卡的磁条上了。联合使用物理磁卡和用户输入密码能是银行确认你可能有使用这个账号的权限。联合使用密码和物理设备(你的ATM卡)是一种普遍存在的两要素认证形式。专业来看,在安全领域,这种类型的设备在安全性要求高的系统中很常见,尤其是处理财务或个人识别信息时。硬件设备如RSA的SecurId联合使用了基于时间的硬件和服务端的认证软件,使得这样的环境极难被破坏。
l 硬件认证:早上当你启动汽车时,你插入钥匙并打火。尽管和其他的两个例子很类似,但是你的钥匙和打火装置的匹配是一种硬件认证的方式。
其实会有很多种的认证方式来解决硬件和软件的安全问题,它们各自也有其优缺点。我们将会在本书的后面章节中介绍它们中的一些,因为它们适用于Spring Security。事实上,本书的后半部分基本上都是原来介绍很多通用的认证方式用Spring Security的实现。
Spring Security扩展了java标准概念中的已认证安全实体(对应单词principal)(java.security.Principal),它被用来唯一标识一个认证过的实体。尽管一个典型的安全实体通常一对一的指向了系统中的一个用户,但它也可能对应系统的各种客户端,如web service的客户端、自动运行的feed聚合器(automated batch feed)等等。在大多数场景下,在你使用Spring Security的过程中,一个安全实体(Principal)只是简单地代表一个用户(user),所以我当我们说一个安全实体的时候,你可以将其等同于说用户。
授权通常涉及到两个不同的方面,他们共同描述对安全系统的可访问性。
第一个是已经认证的安全实体与一个或多个权限(authorities)的匹配关系(通常称为角色)。例如,一个非正式的用户访问你的网站将被视为只有访问的权限而一个网站的管理员将会被分配管理的权限。
第二个是分配权限检查给系统中要进行安全保护的资源。通常这将会在系统的开发过程中进行,有可能会通过代码进行明确的声明也可能通过参数进行设置。例如,在我们应用中管理宠物商店详细目录的界面只能对具有管理权限的用户开放。
【要进行安全保护的资源可以是系统的任何内容,它们会根据用户的权限进行有选择的可访问控制。web应用中的受保护资源可以是单个的页面、网站的一个完整部分或者一部分界面。相反的,受保护的业务资源可能会是业务对象的一个方法调用或者单个的业务对象。】
你可能想象的出对一个安全实体的权限检查过程,查找它的用户账号并确定它是不是真的为一个管理员。如果权限检查确定这个试图访问受保护区区域的安全实体实际上是管理员,那么这个请求将会成功,否则,这个安全实体的请求将会因为它缺少足够的权限而被拒绝。
我们更近距离的看一个特定的受保护资源——产品目录的编辑界面。目录的编辑界面需要管理员才能访问(毕竟,我们不希望普通的用户能够调整我们的目录层次),因此当一个安全实体访问它的时候会要求特定等级的权限。
当我们思考一个网站的管理员试图访问受保护的资源时,权限控制决定是如何做出的时候,我们猜想对受保护资源的权限的检查过程可以用集合理论很简明的进行表述。我们将会使用维恩图来展现对管理用户的这个决策过程:
<!--[endif]-->
对这个页面来说,在用户权限(普通用户和管理员)和需要权限(管理员)之间有一个交集,所以在交集中的用户将能够进行访问。
可以与没有授权的访问者进行对比:
权限集合没有交集,没有公共的元素。所以,用户将会被拒绝访问这个界面。至此,我们已经介绍了对资源授权的简单原理。
实际上,会有真正的代码来决定用户是允许还是被拒绝访问受保护的资源。下面的图片在整体上描述了这个过程,正如Spring Security所使用的那样:
我们可以看到,有一个名为访问决策管理器(access decision manager)的组件来负责决定一个安全实体是不是有适当的访问权限,判断基于安全实体具备的权限与被请求资源所要求资源的匹配情况。
安全访问控制器对访问是否被允许的判断过程可能会很简单,就像查看安全实体所拥有的权限集合与被访问资源所要求的资源集合是不是有交集。
让我们在JBCP Pets应用中简单使用Spring Security,并将会更详细的阐述认证和授权。
尽管Spring Security的配置可能会很难,但是它的作者是相当为我们着想的,因为他们为我们提供了一种简单的机制来使用它很多的功能并可以此作为起点。以这个为起点,额外的配置能够实现应用的分层次详细的安全控制。
我们将从我们不安全的在线商店开始,并且使用三步操作将它变成一个拥有基本用户名和密码安全认证的站点。这个认证仅仅是为了阐述使用Spring Security使我们应用变得安全的步骤,所以你可能会发现这样的方式会有明显不足,这将会引领我们在以后的配置中不断进行改良。
起点配置的第一步是创建一个XML的配置文件,用来描述所有需要的Spring Security组件,这些组件将会控制标准的web请求。
在WEB-INFO目录下建立一个名为dogstore-security.xml的XML文件。文件的内容如下所示:
【讨厌Spring XML配置的用户可能会失望了,因为Spring Security没有像Spring那样提供可替代的注解机制。仔细想一下也是可以理解的,因为Spring Security关注的是整个系统而不是单个对象或类。但未来,我们可能能够在Spring MVC的控制器上使用安全注解,而不是在一个配置文件中指明URL模式!】
尽管在Spring Security中注解并不普遍,但正如你所预料的那样,对类或方法进行的配置是可以通过注解来完成的。我们将在第五章:精确的访问控制中介绍。
Spring Security对我们应用的影响是通过一系列的ServletRequest过滤器实现的(我们将会在介绍Spring Security架构的时候进行阐述)。可以把这些过滤器想成位于每个到我们应用的请求周围的具有安全功能的三明治。(这个比喻够新鲜,不过Spring Security的核心确实就是一系列介于真正请求之间的过滤器,译者注)。
Spring Security使用了o.s.web.filter.DelegatingFilterProxy这个servlet过滤器来包装所有的应用请求,从而保证它们是安全的。
【DelegatingFilterProxy实际上是Spring框架提供的,而不是安全特有的。这个过滤器一般在Spring构建的web应用工程中使用,并将依赖于servlet过滤器的Spring Bean与Servle过滤器的生命周期结合起来。】
通过在web.xml部署描述文件中添加如下的代码,就可以配置这样一个过滤器。这段代码位于Spring MVC的<servlet-mapping>之后:
如果你有心的话,可能会发现这与我们的Spring Security配置文件即dogstore-security.xml没有任何的关系。为了实现这个,我们需要添加一个XML配置文件的应用到web应用的部署描述文件web.xml中。
取决于你如何配置你的Spring web应用,不知你是否已经在web.xml中有了对XML 配置文件的明确引用。Spring web的ContextLoaderListener的默认行为是寻找与你的Spring web servlet同名的XML配置文件。让我们看一下如何添加这个新的Spring Security XML配置文件到已经存在的JBCP Pet商店站点中。
首先,我们需要看一下这个应用是否使用了Spring MVC的自动查找XML配置文件的功能。我们看一下在web.xml中servlet的名字,以<servlet-name>原始进行标识:
over Configuration,CoC)将会在WEB-INF目录下寻找名为dogstore-servelt.xml的配置文件。我们没有覆盖这种默认行为,你能在WEB-INF目录下找到这个文件,它包含了一些Spring MVC相关的配置。
【很多Spring Web Flow和Spring MVC的用户并不明白这些CoC规则是如何生效的以及Spring的代码中在何处声明了这些规则。o.s.web.context.support.XmlWebApplicationContext和它的父类是了解这些的一个很好的起点。JavaDoc在讲解基于Spring MVC框架的web工程的一些参数配置方面也做得不错。】
也可以声明额外的Spring ApplicationContext配置文件,它将会先于Spring MVC servle关联的配置文件加载。这通过Spring的o.s.web.context.ContextLoaderListener创建一个独立于Spring MVC ApplicationContext的另一个ApplicationContext来实现。这是通常的非Spring MVC beans配置的方式,也为Spring Security相关的配置提供了一个好地方。
在web应用的部署描述文件中,用来配置ContextLoaderListener的XML文件地址是在<context-param>元素中给出的:
dogstore-base.xml文件中包含了一些标准的Spring bean的配置,如数据源、服务层bean等。现在,我们能够添加一个包含Spring Security的XML配置文件了,下面是更新后的<context-param>元素:
在我们添加完新的Spring Security配置文件到web部署文件并重启web工程后,尝试访问应用的首页地址:http://localhost:8080/JBCPPets/home.do,你将会看到如下的界面:
漂亮!我们已经使用Spring Security在三步之内实现了一个简单的安全层。在这里,你可以使用guest作为用户名和密码进行登录。接着你将能够看到JBCP Pets应用的一个简单首页。
为了简便起见,本章的源码只包含了全部JBCP Pets整个应用的很小一部分(只有一个页面)。我们这么做是为了更简洁,同时也能让构建和部署应用时不必考虑我们将在后续章节中涉及到的附加功能。这也提供了一个很好的起点让你快速的体验参数的配置并重新部署以查看它们是否生效。
到此为止,思考一下我们所做的事情。你可能已经意识到在产品化之前应用存在很多很明显的问题,这需要很多后续的工作和对Spring Security产品的了解。尝试列举一下在将这个实现安全功能的站点上线前还需要什么样的完善。
实现Spring Security起点级别的配置是相当迅速的,它已经提供了一个登录界面,用户名密码认证以及自动拦截我们在线商店的URL。但是在自动配置给我们提供的功能与我们的最终目标之间有很大的差距:
<!--[if !supportLists]-->l <!--[endif]-->我们将用户名、密码以及角色信息硬编码到XML配置文件中。你是否还记得我们添加的这部分XML内容:
<!--[if !supportLists]-->l <!--[endif]-->我们对所有的页面都进行了安全控制,包括一些潜在客户可能想匿名访问的界面。我们需要完善系统的角色以适应匿名的、认证过的以及管理权限的用户(这也将在第四章中讨论)。
<!--[if !supportLists]-->l <!--[endif]-->登录界面非常应用,但是它太基础了而且与我们JBCP商店风格一点也不一致。我们需要添加一个登录的form界面,并与我们应用的外观和风格一致(我们将在下一章解决这个问题)。
很多用户在初次使用Spring Security时会遇到一些问题。我们列出了几个常见的问题和建议。我们希望你能够一直跟随着本书的讲解,运行我们示例代码。
<!--[if !supportLists]-->l <!--[endif]-->确保你的应用在添加Spring Security之前是可以编译和部署的。必要的时候看一些你所使用的servlet容器的入门级例子和文档。
<!--[if !supportLists]-->l <!--[endif]-->通常使用一个IDE如Eclipse会极大地简化你使用的servlet容器。不仅能够保证部署准确无误,它所提供的控制台日志也很易读可用来检查错误。你还可以在关键的位置添加断点,运行的时候会触发从而简化分析错误的过程。
<!--[if !supportLists]-->l <!--[endif]-->如果你的XML配置文件不正确,你会得到这样的提示信息(或类似的):org.xml.sax.SAXParseException: cvc-elt.1: Cannot find the declaration of element 'beans'。为实现Spring Security的正确配置需要各种各样的XML命名空间引用,这可能会使很多用户感到迷惑。重新检查一下这个例子,仔细查看一下schame声明的部分,并使用XML校验器来保证你没有不合法的XML。
<!--[if !supportLists]-->l <!--[endif]-->确保你所使用的Spring和Spring Security版本匹配,并确保没有你的应用中不存在没用的Spring jar包。
借助于Spring Security的强大基础配置功能以及内置的认证功能,我们在前面讲述的三步配置是很快就能完成的;它们的使用是通过添加auto-config属性和http元素实现的。
但不幸的是,应用实现的考量、架构的限制以及基础设施集成的要求可能使你的Spring Security实现远较这个简单的配置所提供的复杂。很多用户一使用比基本配置复杂的功能就会遇到麻烦,那是因为他们不了解这个产品的架构以及各个元素是如何协同工作以实现一个整体的。
理解web请求的整体流程以及它们是如何穿越实现功能的拦截器链,对我们成功了解Spring Security的高级话题至关重要。记住认证和授权的基本概念,因为它们贯穿我们要保护的系统架构的始终。
Spring Security的架构在很大程度上依赖代理(delegates)和servlet过滤器,来实现环绕在web应用请求前后的功能层。
Servlet过滤器(Servlet Filter,实现javax.servlet.Filter接口的类)被用来拦截用户请求来进行请求之前或之后的处理,或者干脆重定向这个请求,这取决于servlet过滤器的功能。在JBCP Pets在线商店中,最终的目标servlet是Spring MVC 分发servlet,但是在理论上它可能是任何一个web servlet。下面的图描述了一个servlet过滤器是如何封装一个用户请求的:
Spring Security在XML配置文件中的自动配置属性,建立了十个servlet过滤器,它们通过使用Java EE的servlet过滤器链按顺序组合起来。Filter chain是Java EE Servlet API的一个概念,通过接口javax.servlet.FilterChain进行定义,它允许在web应用中的一系列的servlet过滤器能够应用于任何给定的请求。
与生活中金属制定的链类似,每一个servelt过滤器代表链上的一环,它会进行方法的调用以处理用户的请求。请求穿过整个过滤器链,按顺序调用每个过滤器。
正如你能从链这个词汇中推断出的那样,servlet请求按照一定的顺序从一个过滤器到下一个穿过整个过滤器链,最终到达目标servlet。与之相对的是,当servelt处理完请求并返回一个response时,过滤器链按照相反的顺序再次穿过所有的过滤器。
Spring Security使用了过滤器链的概念并实现了自己抽象,提供了VirtualFilterChain,它可以根据Spring Security XML配置文件中设置的URL模式动态的创建过滤器链(可以将它与标准的Java EE过滤器链进行对比,后者需要在web应用的部署描述文件中进行设置)。
【Servlet过滤器除了能够如它的名字所描述的那样进行过滤功能(或阻止请求)以外,还可以用于很多其他的目的。实际上,很多的servlet过滤器的功能类似于在web运行的环境中对请求进行AOP式的代理拦截,因为它们可以允许一些功能在任何发往servelt容器的请求处理之前或之后执行。过滤器能实现的多功能在Spring Security中页得到了体现,因为很多过滤器实际上并不直接影响用户的请求。】
自动配置的选项为你建立了十个Spring Security的过滤器。理解这些过滤器的默认行为以及它们在哪里以及如何配置的,对使用Spring Security的高级功能至关重要。
这些过滤器以及它们使用的顺序,在下面的表格中进行了描述。大多数这些过滤器在我们完善JBCP Pets在线商店的过程中都会被再次提到,所以如果你现在不明白它们的确切功能也不必担心。
过滤器名称 |
描述 |
o.s.s.web.context.SecurityContextPersistenceFilter |
负责从SecurityContextRepository获取或存储SecurityContext。SecurityContext代表了用户安全和认证过的session。 |
o.s.s.web.authentication.logout.LogoutFilter |
监控一个实际为退出功能的URL(默认为/j_spring_security_logout),并且在匹配的时候完成用户的退出功能。 |
o.s.s.web.authentication.UsernamePasswordAuthenticationFilter |
监控一个使用用户名和密码基于form认证的URL(默认为/j_spring_security_check),并在URL匹配的情况下尝试认证该用户。 |
o.s.s.web.authentication.ui.DefaultLoginPageGeneratingFilter |
监控一个要进行基于forn或OpenID认证的URL(默认为/spring_security_login),并生成展现登录form的HTML |
o.s.s.web.authentication.www.BasicAuthenticationFilter |
监控HTTP 基础认证的头信息并进行处理 |
o.s.s.web.savedrequest. RequestCacheAwareFilter |
用于用户登录成功后,重新恢复因为登录被打断的请求。 |
o.s.s.web.servletapi. SecurityContextHolderAwareRequest Filter |
用一个扩展了HttpServletRequestWrapper的子类(o.s.s.web. servletapi.SecurityContextHolderAwareRequestWrapper)包装HttpServletRequest。它为请求处理器提供了额外的上下文信息。 |
o.s.s.web.authentication. AnonymousAuthenticationFilter |
如果用户到这一步还没有经过认证,将会为这个请求关联一个认证的token,标识此用户是匿名的。 |
o.s.s.web.session. SessionManagementFilter |
根据认证的安全实体信息跟踪session,保证所有关联一个安全实体的session都能被跟踪到。 |
o.s.s.web.access. ExceptionTranslationFilter |
解决在处理一个请求时产生的指定异常 |
o.s.s.web.access.intercept. FilterSecurityInterceptor |
简化授权和访问控制决定,委托一个AccessDecisionManager完成授权的判断 |
Spring Security拥有总共大约25个过滤器,它们能够根据你的需要进行适当的应用以改变用户请求的行为。当然,如果需要的话,你也可以添加你自己实现了javax.servlet.Filter接口的过滤器。
请记住,如果你在XML配置文件中使用了auto-config属性,以上表格中列出的过滤器自动添加的。通过使用一些额外的配置指令,以上列表中的过滤器能够精确的控制是否被包含,在后续的章节章将会进行介绍。
你可能会完全从头做起来配置过滤器链。尽管这会很单调乏味,因为有很多的依赖关系要配置,但是它为配置和应用场景的匹配方面提供了更高层次的灵活性。我们将在第六章讲述在启动的过程中所依赖的Spring Bean的声明。
你可能想知道DelegatingFilterProxy是怎样找到Spring Security配置的过滤器链的。让我们回忆一下,在web.xml文件中,我们需要给DelegatingFilterProxy一个过滤器的名字:
<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class> org.springframework.web.filter.DelegatingFilterProxy </filter-class> </filter> |
这个过滤器的名字并不是随意配置的,实际上会跟根据这个名字把Spring Security织入到DelegatingFilterProxy。除非明确配置,否则DelegatingFilterProxy会在Spring WebApplicationContext中寻找同名的配置bean(名字是在filter-name中指明的)。更多配置DelegatingFilterProxy的细节可以在这个类对应的Javadoc中找到。
在Spring Security 3中,使用auto-config会自动提供以下三个认证相关的功能:
<!--[if !supportLists]-->l <!--[endif]-->HTTP基本认证
<!--[if !supportLists]-->l <!--[endif]-->Form登录认证
<!--[if !supportLists]-->l <!--[endif]-->退出
值得注意的是,也可以使用配置元素实现这三个功能,能够实现比使用auto-config提供的功能更精确。我们将在随后的章节中看到它们的使用以提供更高级的功能。
【auto-config和以前不一样了!在Spring Security3之前的版本中,auto-config属性提供了比现在更多的启动项。在Spring Security2中通过auto-config配置的功能,现在可以使用security命名空间样式的配置很容易的实现。请参考第13章:迁移至Spring Security 3来获取更多从Spring Security2迁移到3的详细信息。】
除了以上认证相关的功能,其它过滤器链的配置是通过使用<http>元素来实现的。
在我们的安全系统中,当一个用户在我们的登录form中提供凭证后,这些凭证信息必须与凭证存储中的数据进行校验以确定下一步的行为。凭证的校验涉及到一系列的逻辑组件,它们封装了认证过程。
我们将会深入讲解我们例子中的用户名和密码登录form,与之对应的接口和实现都是特定于用户名和密码认证的。但是,请记住,整体的认证是相同的,不管你是使用基于form的登录请求,或者使用一个外部的认证提供者如集中认证服务(CAS),抑或用户的凭证信息存在一个数据库或在一个LDAP目录中。在本书的第二部分,我们将会看到在基于form登录中学到的概念是如何应用到更高级的认证机制中。
涉及到认证功能的重要接口在下边的图标中有一个概览性的描述:
站在一个较高层次上看,你可以看到有三个主要的组件负责这项重要的事情:
接口名 |
描述/角色 |
AbstractAuthenticationProcessingFilter |
它在基于web的认证请求中使用。处理包含认证信息的请求,如认证信息可能是form POST提交的、SSO信息或者其他用户提供的。创建一个部分完整的Authentication对象以在链中传递凭证信息。 |
AuthenticationManager |
它用来校验用户的凭证信息,或者会抛出一个特定的异常(校验失败的情况)或者完整填充Authentication对象,将会包含了权限信息。 |
AuthenticationProvider |
它为AuthenticationManager提供凭证校验。一些AuthenticationProvider的实现基于凭证信息的存储,如数据库,来判定凭证信息是否可以被认可。 |
有两个重要接口的实现是在认证链中被这些参与的类初始化的,它们用来封装一个认证过(或还没有认证过的)的用户的详细信息和权限。
o.s.s.core.Authentication是你以后要经常接触到的接口,因为它存储了用户的详细信息,包括唯一标识(如用户名)、凭证信息(如密码)以及本用户被授予的一个或多个权限(o.s.s.core.
GrantedAuthority)。开发人员通常会使用Authentication对象来获取用户的详细信息,或者使用自定义的认证实现以便在Authentication对象中增加应用依赖的额外信息。
以下列出了Authentication接口可以实现的方法:
方法签名 |
描述 |
Object getPrincipal() |
返回安全实体的唯一标识(如,一个用户名) |
Object getCredentials() |
返回安全实体的凭证信息 |
List<GrantedAuthority> getAuthorities() |
得到安全实体的权限集合,根据认证信息的存储决定的。 |
Object getDetails() |
返回一个跟认证提供者相关的安全实体细节信息 |
你可能会担心的发现,Authentication接口有好几个方法的返回值是简单的java.lang.Object。这可能会导致在编译阶段很难知道调用Authentication对象的方法返回值是什么类型的对象。
需要注意的一点是AuthenticationProvider并不是直接被AuthenticationManager接口使用或引用的。但是Spring Security只提供了AuthenticationManager的一个具体实现类,即o.s.s.authentication.ProviderManager,它会使用一个或更多以上描述的AuthenticationProvider实现类。因为AuthenticationProvider的使用非常普遍并且被很好的集成在ProviderManager中,所以理解它在最常见的基本配置下是如何工作的就非常重要了。
让我们更仔细的看看在基于web用户名和密码认证的请求下,这些类的处理过程:
你可能已经发现,当你试图访问我们JBCP Pets商店的主页时,你被重定向到http://localhost:8080/JBCPPets/spring_security_login:
URL的spring_security_login部分表明这是一个默认的登录的页面并且是在DefaultLoginPageGeneratingFilter中命名的。我们可以使用配置属性来修改这个页面的名字从而使得它对于我们应用来说是唯一的。
【建议修改登录页URL的默认值。修改后不仅能够对应用或搜索引擎更友好,而且能够隐藏你使用Spring Security作为安全实现的事实。通过这种方式来掩盖Spring Security能够使得万一Spring Security被发现存在安全漏洞时,恶意黑客寻找你应用漏洞的难度。尽管通过这种方式的安全掩盖不会降低你应用的脆弱性,但是它确实能够使得一些传统的黑客工具很难确定你的应用能够承担的住什么类型的攻击。需要注意的是,这里并不是“spring”名称在URL中出现的唯一地方。我们将在后面的章节详细阐述。】
让我们看一下这个form的HTML源码(忽略布局信息),来看一下UsernamePasswordAuthenticationFilter期望得到的信息:
<form name='f' action='/JBCPPets/j_spring_security_check' method='POST'> User:<input type='text' name='j_username' value=''> Password:<input type='password' name='j_password'/> <input name="submit" type="submit"/> <input name="reset" type="reset"/> </form> |
你可以看到用户名和密码对应的form文本域有独特的名字((j_username和j_password),并且form的action地址j_spring_security_check也并不是我们配置的。它们是怎么来的呢?
文本域的名字是UsernamePasswordAuthenticationFilter规定的,并借鉴了Java EE Servlet 2.x的规范(在SRV.12.5.3章节中),规范要求登录的form使用特定的名字并且form的action要为特定的j_security_check值。这样的实际模式目标是允许基于Java EE servlet-based的应用能够与servlet容器的安全设施以标准的方式连接起来。
因为我们的应用没有使用到servlet容器的安全组件,所以可以明确设置UsernamePasswordAuthenticationFilter以使得文本域有不同的名字。这种特定的配置变化可能会比你想象的复杂。现在,我们将要回顾一下UsernamePasswordAuthenticationFilter的生命周期,看一下它是如何进入我们配置的(尽管我们将会在第六章再次讲述这个配置)。
UsernamePasswordAuthenticationFilter是通过<http>配置指令的<form-login>子元素来进行配置的。正如在本章前面讲述的,我们设置的auto-config元素将会在你没有明确添加的情况下包含了<form-login>功能。正如你可能猜测的那样,j_spring_security_check并不对应任何应用中的物理资源。它只是UsernamePasswordAuthenticationFilter监视的一个基于form登录的URL。实际上,在Spring Security中有好几个这样的特殊的URL来实现特定的全局功能。你能在附录:参考资料中找到这些URL的一个列表。
在我们的简单的三步配置文件中,我们使用了一个基于内存的凭证存储实现快速的部署和运行:
<authentication-manager alias="authenticationManager"> <authentication-provider> <user-service> <user authorities="ROLE_USER" name="guest" password="guest"/> </user-service> </authentication-provider> </authentication-manager> |
我们没有将AuthenticationProvider与任何具体的实现相关联,在这里我们再次看到了security命名空间默认为我们做了许多机械的配置工作。但是需要记住的是AuthenticationManager支持配置一个或多个AuthenticationProvider。<authentication-provider>声明默认谁实例化一个内置的实现,即o.s.s.authentication.dao.DaoAuthenticationProvider。<authentication-provider>声明会自动的将这个AuthenticationProvider对象织入到配置的AuthenticationManager中,当然在我们这个场景中AuthenticationManager是自动配置的。
DaoAuthenticationProvider是AuthenticationProvider的简单封装实现并委托o.s.s.core.userdetails.UserDetailsService接口的实现类进行处理。UserDetailsService负责返回o.s.s.core.userdetails.UserDetails的一个实现类。
如果你查看UserDetails的Javadoc,你会发现它与我们前面讨论的Authentication接口非常类似。尽管它们在方法名和功能上有些重叠的部分,但是请不要混淆,它们有着截然不同的目的:
接口 |
目的 |
Authentication |
它存储安全实体的标识、密码以及认证请求的上下文信息。它还包含用户认证后的信息(可能会包含一个UserDetails的实例)。通常不会被扩展,除非是为了支持某种特定类型的认证。 |
UserDetails |
为了存储一个安全实体的概况信息,包含名字、e-mail、电话号码等。通常会被扩展以支持业务需求。 |
我们对<user-service>子元素的声明将会触发对o.s.s.core.userdetails.memory.InMemoryDaoImpl的配置,它是UserDetailsService的一个实现。正如你可能期望的那样,这个实现将在安全XML文件中配置的用户信息放在一个内存的数据存储中。这个service的实现支持其它属性的设置,从而实现账户的禁用和锁定。
让我们更直观的看一下DaoAuthenticationProvider是如何交互的,从而AuthenticationManager提供认证支持:
正如你可能想象的那样,认证是相当可配置化的。大多数的Spring Security例子要么使用基于内存的用户凭证存储要么使用JDBC(在数据库中)的用户凭证存储。我们已经意识到修改JBCP Pets应用以实现数据库存储用户凭证是一个好主意,我们将会在第四章来处理这个配置变化。
Spring Security很好的使用应用级异常(expected exceptions)来表示处理各种的结果情况。你可能在使用Spring Security的日常工作中不会与这些异常打交道,但是了解它们以及它们为何被抛出将会在调试问题或理解应用流程中非常有用。
所有认证相关的异常都继承自o.s.s.core.AuthenticationException基类。除了支持标准的异常功能,AuthenticationException包含两个域,可能在提供调试失败信息以及报告信息给用户方面很有用处。
<!--[if !supportLists]-->l <!--[endif]-->authentication:存储关联认证请求的Authentication实例;
<!--[if !supportLists]-->l <!--[endif]-->extraInformation:根据特定的异常可以存储额外的信息。如UsernameNotFoundException在这个域上存储了用户名。
我们在下面的表格中,列出了常见的异常。完整的认证异常列表可以在附录:参考资料中找到:
异常类 |
何时抛出 |
extraInformation内容 |
BadCredentialsException |
如何没有提供用户名或者密码与认证存储中用户名对应的密码不匹配 |
UserDetails |
LockedException |
如果用户的账号被发现锁定了 |
UserDetails |
UsernameNotFoundException |
如果用户名不存在或者用户没有被授予的GrantedAuthority |
String(包含用户名) |
这些以及其他的异常将会传递到过滤器链上,通常将会被request请求的过滤器捕获并处理,要么将用户重定向到一个合适的界面(登录或访问拒绝),要么返回一个特殊的HTTP状态码,如HTTP 403(访问被拒绝)。
在Spring Security的默认过滤器链中,最后一个servelt过滤器是FilterSecurityInterceptor,它的作用是判断一个特定的请求是被允许还是被拒绝。在FilterSecurityInterceptor被触发的时候,安全实体已经经过了认证,所以系统知道他们是合法的用户。(其实也有可能是匿名的用户,译者注)。请记住的一点是,Authentication提供了一个方法((List<GrantedAuthority>
getAuthorities()),将会返回当前安全实体的一系列权限列表。授权的过程将使用这个方法提供的信息来决定一个特定的请求是否会被允许。
需要记住的是授权是一个二进制的决策——一个用户要么有要么没有访问一个受保护资源的权限。在授权中,没有模棱两可的情景。
在Spring Security中,良好的面向对象设计随处可见,在授权决策管理中也不例外。回忆一下我们在本章前面的讨论,一个名为访问控制决策器(access decision manager)的组件负责作出授权决策。
在Spring Security中,o.s.s.access.AccessDecisionManager接口定义了两个简单而合理的方法,它们能够用于请求的决策判断流程:
<!--[if !supportLists]-->l <!--[endif]-->supports:这个逻辑操作实际上包含两个方法,它们允许AccessDecisionManager的实现类判断是否支持当前的请求。
<!--[if !supportLists]-->l <!--[endif]-->decide:基于请求的上下文和安全配置,允许AccessDecisionManager去核实访问是否被允许以及请求是否能够被接受。decide方法实际上没有返回值,通过抛出异常来表明对请求访问的拒绝。
与AuthenticationException及其子类在认证过程中的使用很类似,特定类型的异常能够表明应用在授权决策中的不同处理结果。o.s.s.access.AccessDeniedException是在授权领域里最常见的异常,因此值得过滤器链进行特殊的处理。我们将在第六章中详细介绍它的高级配置。
AccessDecisionManager是能够通过标准的Spring bean绑定和引用实现完全的自定义配置。AccessDecisionManager的默认实现提供了一个基于AccessDecisionVoter接口和投票集合的授权机制。
投票器(voter)是在授权过程中的一个重要角色,它的作用是评估以下的内容:
<!--[if !supportLists]-->l <!--[endif]-->要访问受保护资源的请求所对应上下文(如URL请求的IP地址);
<!--[if !supportLists]-->l <!--[endif]-->用户的凭证信息(如果存在的话);
<!--[if !supportLists]-->l <!--[endif]-->要试图访问的受保护资源;
<!--[if !supportLists]-->l <!--[endif]-->系统的配置以及要访问资源本身的配置参数。
AccessDecisionManager还会负责传递要请求资源的访问声明信息(在代码中为ConfigAttribute接口的实现类)给投票器。在web URL的请求中,投票器将会得到资源的访问声明信息。如果看一下我们配置文件中非常基础的拦截声明,我们能够看到ROLE_USER被设置为访问配置并用于用户试图访问的资源:
<intercept-url pattern="/*" access="ROLE_USER"/> |
投票器将会对用户是否能够访问指定的资源做出一个判断。Spring Security允许过滤器在三种决策结果中做出一种选择,它们的逻辑定义在o.s.s.access.AccessDecisionVoter接口中通过常量进行了定义。
决策类型 |
描述 |
Grant (ACCESS_GRANTED) |
投票器允许对资源的访问 |
Deny (ACCESS_DENIED) |
投票器拒绝对资源的访问 |
Abstain (ACCESS_ABSTAIN) |
投票器对是否能够访问做了弃权处理(即没有做出决定)。可能在多种原因下发生,如: <!--[if !supportLists]-->l <!--[endif]-->投票器没有确凿的判断信息; <!--[if !supportLists]-->l <!--[endif]-->投票器不能对这种类型的请求做出决策。 |
正如你从访问决策相关类和接口的设计中可以猜到的那样,Spring Security的这部分被精心设计,所以认证和访问控制的使用场景并不仅仅限于web领域。我们将会在:精确的访问控制中关于方法级别的安全时,再次讲解投票器和访问控制管理。
当将他们组合在一起,“对web请求的默认认证检查”的整体流程将如下图所示:
我们可以看到ConfigAttribute能够从配置声明(在DefaultFilterInvocationSecurityMetadataSource类中保存)中传递数据到投票器,投票器并不需要其他的类来理解ConfigAttribute的内容。这种分离能够为新类型的安全声明(例如我们将要看到的方法安全声明)使用相同的访问决策模式提供基础。
实际上Spring Security允许通过security命名空间来配置AccessDecisionManager。<http>元素的access-decision-manager-ref属性来指明一个实现了AccessDecisionManager的Spring Bean。Spring Security提供了这个接口的三个实现类,都在o.s.s.access.vote包中:
类名 |
描述 |
AffirmativeBased |
如果有任何一个投票器允许访问,请求将被立刻允许,而不管之前可能有的拒绝决定。 |
ConsensusBased |
多数票(允许或拒绝)决定了AccessDecisionManager的结果。平局的投票和空票(全是弃权的)的结果是可配置的。 |
UnanimousBased |
所有的投票器必须全是允许的,否则访问将被拒绝。 |
如果你想修改我们的应用来使用UnanimousBased访问决策管理器,我们需要修改两个地方。首先让我们在<http>元素上添加access-decision-manager-ref属性:
<http auto-config="true" access-decision-manager-ref="unanimousBased" > |
这是一个标准的Spring Bean的引用,所以这需要对应一个bean的id属性。接下来,我们要定义这个bean(在dogstore-base.xml中),并与我们引用的有相同的id:
<bean class="org.springframework.security.access.vote.UnanimousBased" id="unanimousBased"> <property name="decisionVoters"> <list> <ref bean="roleVoter"/> <ref bean="authenticatedVoter"/> </list> </property> </bean> <bean class="org.springframework.security.access.vote.RoleVoter" id="roleVoter"/> <bean class="org.springframework.security.access.vote. AuthenticatedVoter" id="authenticatedVoter"/> |
你可能象知道decisionVoters属性是什么。这个属性在我们不声明AccessDecisionManager时,是自动配置的。默认的AccessDecisionManager要求我们配置投票器的一个列表,它们将会在认证决策时用到。这里列出的两个投票器是security命名空间配置默认提供的。
遗憾的是,Spring Security没有为我们提供太多的投票器,但是实现AccessDecisionVoter接口并在配置中添加我们的实现并不是一件困难的事情。我们将在第六章看一个例子。
我们引用的两个投票器介绍如下:
类名 |
描述 |
例子 |
o.s.s.access. vote.RoleVoter |
检查用户是否拥有声明角色的权限(GrantedAuthority)。access属性定义了GrantedAuthority的一个列表。预期会有ROLE_前缀,但这也是可配置的。 |
access="ROLE_USER,ROLE_ADMIN" |
o.s.s.access. vote.AuthenticatedVoter |
支持特定类型的声明,允许使用通配符: <!--[if !supportLists]-->l <!--[endif]-->IS_AUTHENTICATED_FULLY——允许提供完整的用户名和密码的用户访问; <!--[if !supportLists]-->l <!--[endif]-->IS_AUTHENTICATED_REMEMBERED——如果用户是通过remember me功能认证的则允许访问; <!--[if !supportLists]-->l <!--[endif]-->IS_AUTHENTICATED_ANONYMOUSLY——允许匿名用户访问。 |
access=" IS_AUTHENTICATED_ANO |
关注这个系列的同学们有福了,我把前两章翻译的doc文档上传了,欢迎传播。有谬误之处,请不吝指正。
基于角色标准投票机制的标准实现是使用 RoleVoter ,还有一种替代方法可用来定义语法复杂的投票规则即使用Spring 表达式语言( SpEL )。要实现这一功能的直接方式是在 <http> 配置元素上添加 use-expressions 属性:
<http auto-config="true" use-expressions="true"> |
添加后将要修改用来进行拦截器规则声明的 access 属性,改为 SpEL 表达式。 SpEL 允许使用特定的访问控制规则表达式语言。与简单的字符串如 ROLE_USER 不同,配置文件可以指明表达式语言触发方法调用、引用系统属性、计算机值等等。
SpEL 的语法与其他的表达式语言很类似,如在 Tapestry 等框架中用到的 Object Graph Notation Language (OGNL) ,以及用于 JSP 和 JSF 的 Unified Expression Language 。它的语法面很广,已经超出了本书的覆盖范围,我们将会通过几个例子为你构建表达式提供一些确切的帮助。
需要注意的重要一点是,如果你通过使用 use-expressions 属性启用了 SpEL 表达式访问控制,将会使得自动配置的 RoleVoter 实效,后者能够使用角色的声明,正如在前面的例子所见到的那样:
<intercept-url pattern="/*" access="ROLE_USER"/> |
这意味着如果你仅仅想通过角色来过滤请求的话,访问控制声明必要要进行修改。幸运的的是,这已经被充分考虑过了,一个 SpEL 绑定的方法 hasRole 能够检查角色。如果我们要使用表达式来重写例子的配置,它可能看起来如下所示:
<http auto-config="true" use-expressions="true"> <intercept-url pattern="/*" access="hasRole('ROLE_USER')"/> </http> |
正如你可能预料的那样, SpEL 使用了一个不同的 Voter 实现类,即o.s.s.web.access.expression.WebExpressionVoter ,它能理解怎样解析 SpEL 表达式。 WebExpressionVoter 借助于 o.s.s.web.access.expression.WebSecurityExpressionHandler 接口的一个实现类来达到这个目的。WebSecurityExpressionHandler 同时负责评估表达式的执行结果以及提供在表达式中应用的安全相关的方法。这个接口的默认实现对外暴露了 o.s.s.web.access.expression.WebSecurityExpressionRoot 类中定义的方法。
这些类的流程以及关系如下图所示:
为实现 SpEL 访问控制表达式的方法和伪属性( pseudo-property )在类 WebSecurityExpessionRoot 及其父类的公共方法中进行了声明。
【伪属性( pseudo-property )是指没有传入参数并符合 JavaBeans 的 getters 命名格式的方法。这允许 SpEL表达式能够省略方法的圆括号以及 is 或 get 的前缀。如 isAnonymous() 方法可以通过 anonymous 伪属性来访问。】
Spring Security 3 提供的 SpEL 方法和伪属性在以下的表格中进行了描述。要注意的是没有被标明“ web only ”的方法和属性可以在保护其他类型的资源中使用,如在保护方法调用时。示例表示的方法和属性是使用在<intercept-url> 的 access 声明中。
方法 |
Web only? |
描述 |
示例 |
hasIpAddress (ipAddress) |
Yes |
用于匹配一个请求的 IP 地址或一个地址的网络掩码 |
access="hasIpAddress(' 162.79.8.30')" access="hasIpAddress(' 162.0.0.0/224')" |
hasRole(role) |
No |
用于匹配一个使用GrantedAuthority 的角色(类似于 RoleVoter ) |
access="hasRole('ROLE USER')" |
hasAnyRole(role) |
No |
用于匹配一个使用GrantedAuthority 的角色列表。用户匹配其中的任何一个均可放行。 |
access="hasRole('ROLE_ USER','ROLE_ADMIN')" |
除了以上表格中的方法,在 SpEL 表达式中还有一系列的方法可以作为属性。它们不需要圆括号或方法参数。
属性 |
Web only? |
描述 |
例子 |
permitAll |
No |
任何用户均可访问 |
access="permitAll" |
denyAll |
NO |
任何用户均不可访问 |
access="denyAll" |
anonymous |
NO |
匿名用户可访问 |
access="anonymous" |
authenticated |
NO |
检查用户是否认证过 |
access="authenticated" |
rememberMe |
No |
检查用户是否通过remember me 功能认证的 |
access="rememberMe" |
fullyAuthenticated |
No |
检查用户是否通过提供完整的凭证信息来认证的 |
access="fullyAuthenticated" |
需要记住的是, voter 的实现类必须基于请求的上下文返回一个投票的结果(允许、拒绝或者弃权)。你可能会认为 hasRole 会返回一个 Boolean 值,实际上正是如此。基于 SpEL 的访问控制声明必须是返回 Boolean 类型的表达式。返回值为 true 意味着投票器的结果是允许访问, false 的结果意味着投票器拒绝访问。
【如果一个表达式的值不是 Boolean 类型的,你将会得到如下的一个异常信息:org.springframework.expression.spel.SpelException:
EL1001E:Type conversion problem, cannot convert from
class java.lang.Integer to java.lang.Boolean 】
另外,表达式不能返回一个弃权类型的结果,除非访问控制声明不是一个合法 SpEL 表达式,在这种情况下投票器将会放弃投票。
如果你不在乎这些细小的约束, SpEL 访问控制声明能够提供一种灵活的配置访问控制决策的方式。
在本章中,我们提供了安全领域两个重要概念即认证和授权的介绍。
l 在总体上了解我们要进行安全保护的系统;
l 使用 Spring Security 的自动配置在三步之内实现了我们应用的安全配置;
l 了解了在 Spring Security 中 servlet 过滤器的使用及重要性;
l 了解了认证和授权过程中重要的角色,包括一些重要类实现的详细介绍如 Authentication 和 UserDetails
l 体验了与访问控制规则有关的 SpEL 表达式的配置。
在接下来的一章中,我们将通过添加一些增强用户体验的功能,把基于用户名和密码的认证提高一个新的水平。
在本章中,我们将对JBCP Pets在线商店增加一些功能,这些新功能能够为用户提供更愉悦和可用的用户体验,同时提供一些对安全系统很重要的功能。
在本章中,我们将要:
l 按照你的意愿自定义登录和退出页面,并将它们与标准的Spring web MVC的控制器相关联;
l 使用remember me功能为用户提供便利,并理解其背后的安全含义;
l 构建用户账号管理功能,包括修改密码以及密码遗忘找回功能。
你可能还记得在前一章中,我们使用了Spring Security的security命名空间的基本配置功能。这为我们提供了基本的登录、认证和授权功能,但是这肯定没有到达产品质量的要求。在我们向老板汇报进度前,要添加的一个很重要的增强功能就是使得登录界面在展现和行为上与我们在线应用的其他地方保持一致。
回忆一下现在的登录界面大致如下所示:
自动配置并没有为我们提供很多其他的功能,如为登录页面添加样式。我们要为站点增加以下的功能:
l 拥有页头、页脚以及与JBCP Pets样式一致的登录页;
l 允许用户退出的链接
l 允许用户修改密码的页面。
登录和退出的流程应该如下图所示:
我们将会通过一系列的练习来开发完善这个站点的结构。当开发登录和退出功能时,我们将会讲解所做的内容,所以当我们需要扩展站点的基本功能时,能够对于我们构建的内容有一个清晰的理解。
首先,我们需要一个集成于我们系统的登录页来替代默认的Spring Security登录页。需要的登录流程如下图所示:
我们需要添加一个Spring MVC的控制器来实现登录功能,以及以后的退出功能。JBCP Pets站点使用Spring MVC基于注解的机制来实现控制器与站点路径和资源的配置。让我们在包下com.packtpub.springsecurity.web.controller创建一个名为LoginLogoutController的controller,并包含以下的内容:
// imports omitted @Controller public class LoginLogoutController extends BaseController{ @RequestMapping(method=RequestMethod.GET,value="/login.do") public void home() { } } |
可以看到,我们添加了一个非常简单的controller,并将其唯一的方法匹配至/login.do这个URL地址。这是我们编写简单的自定义登录页所要做的全部事情,这将替代Spring Security基本配置中为我们添加的登录页。BaseController基类在第二章:Spring Security起步的代码中已经添加,它提供了一个便利的地方我们可以添加应用中所有controller都能用到的方法。
/login.do引用将会导致我们在WEB-INF/dogstore-servlet.xml配置的Spring MVC view resolver去/WEB-INF/views目录下寻找名为login.jsp的JSP文件。让我们添加一个包含登录form的简单JSP,它将被Spring Security识别和使用。在第二章中我们已经学到,为了保证接下来的行为能够被正确的执行,登录的form中有两个重要的元素必须要被正确的设置:
l Form action必须与UsernamePasswordAuthenticationFilter过滤器的action的配置相一致。默认的form action是j_spring_security_check;
l 用户名和密码的表单域要与servlet的标准相一致。默认j_username和j_password是文本域的名字。
我们同时会在这个JSP中包含站点的页头和页脚(本章的示例代码中添加了这部分,但是在本书的内容中没有进行罗列,因为它们在这里并不是阐述的重点所在)。这些完成后,得到一个简单的JSP:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <jsp:include page="common/header.jsp"> <jsp:param name="pageTitle" value="Login"/> </jsp:include> <h1>Please Log In to Your Account</h1> <p> Please use the form below to log in to your account. </p> <form action="j_spring_security_check" method="post"> <label for="j_username">Login</label>: <input id="j_username" name="j_username" size="20" maxlength="50" type="text"/> <br /> <label for="j_password">Password</label>: <input id="j_password" name="j_password" size="20" maxlength="50" type="password"/> <br /> <input type="submit" value="Login"/> </form> <jsp:include page="common/footer.jsp"/> |
需要注意的是,必须使用post方式的form提交,否则UsernamePasswordAuthenticationFilter会拒绝登录请求。
最后,我们还需要Spring Security的自动配置来引用我们新的登录页面。如果你在此时迫不及待想看一下效果的话,我们实际上只是为应用增加了一个新的工作页面。按照上面的流程并输入以下的地址http://localhost:8080/JBCPPets/login.do,看看发生了什么。
什么?你是否发现你的请求首先被Spring Security拦截了(被重定向到spring_security_login)并且能够看见那个登录的form?这是因为Spring Security依旧指向了DefaultLoginPageGeneratingFilter生成的默认登录页。一旦你通过了这个过滤器生成的默认登录页,你才能够看到新的自定义登录页。最后一步就是要移除默认页并使用我们的登录form作为登录页。
按照第一感觉,貌似我们只需要配置Spring Security的配置文件中的<form-login>元素并添加login-page命令,大致如下所示:
<http auto-config="true" use-expressions="true"> <intercept-url pattern="/*" access="hasRole('ROLE_USER')"/> <form-login login-page="/login.do" /> </http> |
现在,启动应用并输入首页地址(http://localhost:8080/JBCPPets/home.do)。如果你使用的IE浏览器,你会发现页面根本没有渲染,但是页面的似乎在不停的加载。让我们切换到Mozilla Firefox并访问同样的地址。在Firefox下,你能够看到更多的信息,如下所示:
产生这样的问题是因为我们的URL拦截规则:
<intercept-url pattern="/*" access="hasRole('ROLE_USER')"/> |
这将要求访问所有匹配/*的URL(这将匹配应用的所有页面,包括我们新的登录页)都需要拥有ROLE_USER角色。下面的图展现了发生了什么事情:
(其实上面发生了反复请求登录页的情况,死循环了——译者注)
我们需要修改认证规则来允许匿名用户能够访问登录页。
【对于所有给定的URL请求,Spring Security按照自顶向下的顺序评估认证规则。第一个匹配URL模式的规则将会被使用。这意味着你的授权规则将要按照最特殊的到最不特殊的规则来进行排列。这在开发复杂的规则集合时将会非常重要,因为开发人员经常会感到迷惑,因为他们有时会搞不清到底哪个规则会生效。记住自顶向下顺序,你将能够很容易地在任何场景下找到正确的对应规则。】
因为我们是要添加一个更特殊的规则,所以我们需要将其添加在列表的顶部。我们最终会得到如下的规则设置:
<intercept-url pattern="/login.do" access="permitAll"/> <intercept-url pattern="/*" access="hasRole('ROLE_USER')"/> |
这将能够达到我们想要的效果:允许任何用户访问登录页而限制站点的其他部分只能是认证用户才能访问。到此为止,已经完成了登录功能。让我们看一下要添加退出功能都需要做些什么。
术语退出(Logout)指的是用户使其安全session失效的一种操作。一般来说,用户在退出后,将会被重定向到站点的非安全保护的界面。让我们在站点的页头部分添加一个“Log Out”的链接,并再次访问站点以了解其如何实现功能的。
正如我们在第二章中讨论的那样,Spring Security将会监视一些特殊的URL,这些URL将会触发过滤器链中的一个或多个过滤器。用户实现退出的URL标识为/j_spring_security_logout。在header.jsp中添加一个退出的链接与为一个锚标签(即a标签)添加适合的href属性:
如果你重新加载站点并点击“Log Out”链接,你会发现被重置到登录form的界面。现在看来,登录和退出是很有趣的!
【使用JSTL URL标签来处理相对URL。我们使用了JSTL核心库中的url标签来保证提供的URL在部署的web应用中能够被正确处理。url标签将提供的URL按照相对路径(以/开始)进行处理。你可能会见过其他类似的实现技术如使用JSP的代码((<%= request.getContextPath() %>)),但是JSTL的url标签能够使得你免于编写内联的代码。】
让我们看一下在这个简单操作的背后都发生了什么。
当我们点击退出链接时,到底发生了什么?
需要记住的一点是任何URL请求在被servlet处理之前,都会经过Spring Security的过滤器链。所以,/j_spring_security_logout这个URL请求并非对应系统中的一个JSP,也不必有一个真正的JSP或者Spring MVC的目标来对其进行处理。这种类型的URL通常被称为虚拟URL。
请求/j_spring_security_logout的URL被o.s.s.web.authentication.logout.LogoutFilter过滤器所拦截。在Spring Security的众多默认过滤器中,LogoutFilter专门匹配这个虚拟URL并执行相应的操作。
让我们快速地查看一下Spring Security的security命名空间提供的默认退出功能:
基于这个基本配置,系统将会寻找在logout-url属性配置的URL并实现用户的退出。使得用户退出系统将会涉及如下的三个步骤:
1. 使得HTTP session失效(如果invalidate-session属性被设置为true);
2. 清除SecurityContex(真正使得用户退出);
3. 将页面重定向至logout-success-url指明的URL。
以下的图片阐述了退出的过程:
o.s.s.web.authentication.logout.LogoutHandler接口的实现类可以在用户通过LogoutFilter退出时被调用。你可以实现自己的LogoutHandler(尽管比较复杂)并将其关联到LogoutFilter的生命周期中。通过LogoutFilter默认设置的LogoutHandler将会清除session以及remember me相关的功能,所以用户的session中不会再持有认证相关的信息。最后,通过一个o.s.s.web.
authentication.logout.LogoutSuccessHandler接口的默认实现,页面得以重定向到一个URL。默认实现中会将页面重定向到我们配置的成功退出URL地址(默认为/),但是我们自定义任何系统在用户退出时想要的操作。值得注意的是,退出的处理不应该抛出异常,因为很重要的一点是要在用户的安全session中避免可能出现的潜在不一致性。所以在实现自己的安全处理时要保证异常被正确的处理和记录。
让我们尝试重写默认的logout URL来提供一个修改自动设置的简单例子。我们将会修改logout URL为/logout。
修改dogstore-security.xml配置文件来包含<logout>元素如下的代码所示:
修改/common/header.jsp文件来改变logout链接的herf属性以匹配新的URL:
重新启动应用并进行尝试。你可以发现使用/logout URL取代了/j_spring_security_logout实现用户的退出。你可能也会发现当你尝试/j_spring_security_logout这个地址时,你会得到一个Page not Found(404)的错误,是因为这个URL不与任何一个实际的servlet资源相对应并且不会被过滤器所处理。
<logout>元素包含其他的配置指令以实现更复杂的退出功能,介绍如下:
属性 |
描述 |
invalidate-session |
如果被设置为true,用户的HTTP session将会在退出时被失效。在一些场景下,这是必要的(如用户拥有一个购物车时) |
logout-success-url |
用户在退出后将要被重定向到的URL。默认为/。将会通过HttpServletResponse.redirect来处理。 |
logout-url |
LogoutFilter要读取的URL(在例子中,我们改变了它的设置)。 |
success-handler-ref |
对一个LogoutSuccessHandler实现的引用。 |
对于经常访问站点的用户有一个便利的功能就是remember me。这个功能允许一个再次访问的用户能够被记住,它通过在用户的浏览器上存储一个加密的cookie来实现的。如果Spring Security能够识别出用户提供的remember me cookie,用户将不必填写用户名和密码,而是直接登录进入系统。
与到目前为止我们介绍的其它功能不同,remember me功能并不是在使用security命名空间方式的配置中自动添加的。让我们尝试这个功能并查看它是怎样影响登录流程的。
完成这个练习后,将会为用户访问pet store应用提供一个简单的记忆功能。
修改dogstore-security.xml配置文件,添加<remember-me>声明。设置key属性为jbcpPetStore:
如果我们现在尝试使用应用,在流程上将不会看到有任何的变化。这是因为还需要在登录form上添加一个输入域,以允许用户选择使用这个功能。编辑login.jsp文件,添加一个checkbox,代码如下:
当我们再次登录时,如果Remember Me被选中,一个Remember Me的cookie将会设置在用户的浏览器中。
如果用户关闭浏览器并重新打开访问一个JBCP Pets站点上需要认证的页面,他将不会再看到登录页了。请亲自试一下——登录并将Remember Me选项选中,收藏首页,然后重启浏览器并再次访问首页。你能发现你直接登录成功并不再需要提供凭证。
一些高级的用户在体验本功能时也可以使用浏览器插件如Firecookie(http://www.
softwareishard.com/blog/firecookie/),来管理(移除)会话session。这将会在你开发或校验这种类型功能时,节省你时间和提高效率。
Remember me功能设置了一个cookie在用户的浏览器上,它包含一个Base64编码的字符串,包含以下内容:
l 用户的名字;
l 过期的日期/时间;
l 一个MD5的散列值包括过期日期/时间、用户名和密码;
l 应用的key值,是在<remember-me>元素的key属性中定义的。
这些内容将被组合成一个cookie的值存储在浏览器中以备后用。
MD5是一种知名的加密哈希算法。加密哈希算法将输入的数据进行压缩并生成唯一的任意长度的文字,这叫做摘要。摘要能够在以后使用,以校验不明的输入是否与生成hash的输入内容完全一致,此时并不需要使用原来的输入内容本身。下面的图片展示了这个过程:
你可以看到,未知的输入可以与存储的MD5哈希进行校验,并能够得出未知的输入与存储的已知输入是否匹配的结论。摘要和加密算法(encryption algorithms)的一个重要不同在于,它很难从反向工程从摘要值得到初始的数据。这是因为摘要仅仅是原始内容的一个概述或指纹(fingerprint,),并不是全部的数据本身(它可能会很大)。
尽管对加密的数据不可能进行解码,但是MD5对一个类型的国际却很脆弱,包括挖掘算法本身的弱点以及彩虹表攻击(rainbow table attacks)。彩虹表通常会包括数百万输入值计算出来的哈希值。这使得攻击者能够寻找彩虹表中的哈希值从而确定实际值(未经过hash的值)。我们将会在第四章:凭证安全存储中讲解密码安全的时,介绍防范这种攻击的一种方法。
关于remember me的cookie,我们能够看到这个cookie的组成足够复杂,所以对攻击者来说很难造出一个仿冒的cookie。在第四章中,我们将会学习另一种技术来使得remember me功能更加安全,免受恶意攻击。
Cookie的失效时间基于一个配置的过期时间段的长度。如果用户在cookie失效之前重新访问我们的站点,这个cookie将和应用设置的其它cookie一起提交到应用上。
如果存在remember me cookie,o.s.s.web.authentication.rememberme.RememberMeAuthenticationFilter过滤器将会检查cookie的内容并通过检查是否为一个认证过的remember me cookie来认证用户(查看本章后面的Remember me是否安全?章节,将会讲述这样做的原因),这个过滤器是通过<remember-me>配置指令添加到过滤器链中的。
下面的图表阐述了校验remember me cookie过程中涉及到的不同组件:
RememberMeAuthenticationFilter在过滤器链中,位于SecurityContextHolderAwareRequestFilter之后,而在AnonymousProcessingFilter之前。正如链中的其它过滤器那样,RememberMeAuthenticationFilter也会检查request,如果是其关注的,对应的操作将会被执行。
按图中所述,过滤器负责检查用户的过滤器是否remember me cookie作为它们请求的一部分。如果remember me被发现,它会是一个Base64的编码,期望的MD5哈希值通过cookie中的用户名和密码进行计算获得。(这里感觉有些问题,因为期望的MD5值应该是通过应用来进行获取,而不是提供前台过来的cookie计算出来的。?)如果cookie通过这一层的校验,用户就已经登录成功了。
【你可能已经意识到如果用户修改了用户名或密码,任何的remember me token都将失效。请确保给用户提供适当的信息,如果允许它们修改账号的那些信息。在第四章中,我们将会看到一个替代的remember me实现,它只依赖用户名并不依赖密码。】
我们看到RememberMeAuthenticationFilter依赖一个o.s.s.web.authentication.RememberMeServices的实现来校验cookie。如果登录请求的request包含一个名为_spring_security_remember_me 的form参数,相同的实现类也会于form登录成功时使用。这个cookie用上面提到的信息进行编码,以Base64编码存储在浏览器中,包含了时间戳和用户密码等信息形成的MD5哈希值。
需要记住的是,可以区分通过remember me认证的用户和提供用户名和密码(或相当凭证)认证的用户。我们将会在审查remember me功能的安全性的时,对其进行简单的实验。
RememberMeServices的实现在用户的生命周期中(一个认证用户session的生命周期)在好几个地方被调用。为了使你理解remember me功能,了解remember me service完成生命周期功能的时间点将会有所帮助:
行为 |
应该做什么? |
登录成功 |
实现类设置remember me cookie(如果设置了对应的form参数) |
登录失败 |
如果存在的话,实现类应该删掉这个cookie |
用户退出 |
如果存在的话,实现类应该删掉这个cookie |
知道RememberMeServices在何时以及怎样与用户的生命周期关联对于创建自定义的认证处理至关重要,因为我们需要保证任何的自定义认证处理对待RememberMeServices保持一致性,以保证这个功能的有效性和安全性。
可以修改两个常用的配置来改变remember me功能的默认行为:
属性 |
描述 |
Key |
为remember mecookie定义一个唯一的key值,以与我们的应用关联 |
token-validity-seconds |
定义时间的长度(以秒计)。Remember me的cookie将在将被视为认证合法,并且也将用于设置cookie的过期时间。 |
通过对cookie哈希内容生成的讲解你可以推断出,Key属性对与remember me功能的安全性很重要。要保证你选择的key值是你的应用唯一使用的,并且足够长以至于不能很容易的被猜出。
请记住本书的目的何在,我们让key的值相对很简单,但是如果你要使用remember me功能在你的应用中,建议key值包含应用的唯一名称以及至少36位长度的随机字符。密码生成工具(在google中搜索“online password generator”)是一个好办法来得到包含文字数字以及特殊字符组成的伪随机混合内容,以作为你的remember me key值。
还要记住的是应用处于不同的环境中(如开发、测试、产品级等),remember me cookie的值也要考虑这些情况。这能够避免remember me cookie在测试阶段被无意中的错误使用。
一个产品环境的应用中,示例的key值如下:
jbcpPets-rmkey-paLLwApsifs24THosE62scabWow78PEaCh99Jus
token-validity-seconds属性被用来设置remember me token被接受作为自动登录功能(即使要校验的token不合法)的秒数。本属性还会设置用户浏览器中登录cookie能够被保存的最长时间。
【设置remember me的会话cookie:如果token-validity-seconds属性被设置成-1,登录cookie将被设置为会话cookie,即在用户关闭浏览器后不会被保存。Token的有效时间是一个不可配置的值为2周(假设用户不关闭浏览器)。不要将这个cookie与存储用户的session ID的cookie相混淆——它们是不同的事情却有着类似的名字。】
关于remember me功能的高级自定义功能,还有几个其它的配置指令。我们将会在接下来的练习中包含部分,另一部分在第六章:高级配置与扩展中包含,那时会介绍高级的授权技术。
对我们精心保护的站点来说,为了用户体验而添加的任何与安全相关的功能,都有增加安全风险的潜在可能。按照其默认方式,Remember me功能存在用户的cookie被拦截并被恶意用户重用的风险。下图展现了这种情况是如何发生的:
使用SSL(第四章进行讨论)以及其他的网络安全技术能缓解这种类型攻击的风险,但是要注意的是还有其他技术如跨站脚本攻击(XSS)能够窃取或损害一个remembered user session。为了照顾用户的易用性,我们不会愿意让用户的财产信息或个人信息因为remembered session的不合理使用而遭到篡改或窃取。
【尽管我们不会涉及恶意用户行为的细节,但是当你实现安全系统时,了解恶意用户所使用的攻击技术是很重要的。XSS是其中的一种技术,当然还有其他的很多种。强烈建议你了解OWASP Top Ten(http://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project)作为一个入门列表并参考一本web应用安全参考书,里面介绍了各种的技术使用。】
平衡用户易用性和应用安全性的一种通用方法是识别出站点中与个人或敏感信息相关的功能点。确保这些功能点在进行授权校验时不仅要判断用户的角色,还要保证用户进行了完整的用户名和密码认证。这可以通过使用SpEL表达式语言的fullyAuthenticated伪属性来实现,关于授权规则的SpEL表达式语言我们在第二章中已经有所介绍。
我们将在随后的第五章:精确的访问控制中介绍高级的认证技术,但是,了解能够辨别认证session是否为remembered并以此建立访问规则也是很重要的。
我们可以设想一个使用remembered session登录的用户要查看和修改他的“wish list”。这与其他的客户在线站点很类似,并不会出现与用户信息或财务信息相关的风险(要注意的是每个站点各不相同,不能盲目的将这些规则应用与你的站点)。相反的,我们将会重点保护用户的账号以及订单功能。我们要确保即使是remembered的用户,如果试图访问账号信息或定购产品,都需要对他们进行认证。以下为我们如何设置授权规则:
<intercept-url pattern="/login.do" access="permitAll"/>
已经存在的登录页和ROLE_USER设置没有变化。但我们添加了一条规则,要求用户具有GrantedAuthority的ROLE_USER角色同时还要求用户被完全的认证,即这个认证的session确实是通过提供用户名、密码或等同的凭证来进行认证的。注意这里的SpEL逻辑操作语法——在SpEL中,使用and,or以及not作为逻辑操作符。这是SpEL的设计者充分考虑的结果,因为&&操作符在XML中很难被使用。
如果你在应用中尝试运行,如果以remember me功能登录并试图访问“My Account”链接,你将会得到一个403访问拒绝的提示,这说明这个地址已经被适当地保护了。出现错误界面是因为我们应用的配置还是使用默认的AccessDeniedHandler,这个类负责捕获和响应AccessDeniedException的信息。我们将会在第六章学习AccessDeniedException怎样被处理时,自定义这个行为。
【不使用表达式来实现完全认证的检查。如果你的应用不使用SpEL表达式来进行访问控制声明,你可以通过使用IS_AUTHENTICATED_FULLY访问规则来检查用户是不是进行了完整的认证(如:access=" IS_AUTHENTICATED_FULLY")。但要注意的是,这种标准的角色设置声明并没有SpEL那样强的表现力,所以如果要处理复杂的boolean表达式的时候,可能会比较困难。】
错误处理尚没有添加,但是你可以看到通过这种方式将remember me的易用性与更高层次的安全性结合了起来,用户访问敏感的信息时就会被要求提供完整的凭证信息。
有一种让remember me功能更安全的方式就是将用户的IP地址绑定到cookie的内容上。让我们通过一个例子来描述怎样构建RememberMeServices的实现类来完成这个功能。
基本的实现方式是扩展o.s.s.web.authentication.rememberme.TokenBasedRememberMeServices基类,以添加请求者的IP地址到cookie本身和其他的MD5哈希元素中。
扩展这个基类涉及到重写两个主要方法,并重写或实现几个小的帮助方法。还有一个要注意的是我们需要临时存储HttpServletRequest(将使用它来得到用户的IP地址)到一个ThreadLocal中,因为基类中的一些方法并没有将HttpServletRequest作为一个参数。
首先,我们要扩展TokenBasedRememberMeServices类并重写父类的特定行为。尽管父类是非常易于重写,但是我们不想去重复一些重要的处理流程,所以能使这个类非常简明却有点不好理解。在com.packtpub.springsecurity.security包下创建这个类:
还有一些简单的方法来设置和获取ThreadLocal HttpServletRequest:
我们还需要添加一个工具方法以从HttpServletRequest中获取IP地址:
我们要重写的第一个有趣的方法是onLoginSuccess,它用来为remember me处理设置cookie的值。在这个方法中,我们需要设置ThreadLocal并在完成处理后将其清除。需要记住的是父类方法的处理流程——收集用户的所有认证请求信息并将其合成到cookie中。
父类的onLoginSuccess方法将会触发makeTokenSignature方法来创建认证凭证的MD5哈希值。我们将要重写此方法,以实现从request中获取IP地址并使用Spring框架的一个工具类编码要返回的cookie值。(这个方法在进行remember me校验时还会被调用到,以判断前台传递过来的cookie值与后台根据用户名、密码、IP地址等信息生成的MD5值是否一致。——译者注)
与之类似的,我们还重写了setCookie方法以添加包含IP地址的附加编码信息:
这就得到了生成新cookie所有需要的信息。
最后,我们要重写processAutoLoginCookie方法,它用来校验用户端提供的remember me cookie的内容。父类已经为我们解决了大部分有意思的工作,但是,为了避免调用父类冗长的代码,我们在调用它之前先进行了一次IP地址的校验。
我们的自定义的RememberMeServices编码已经完成了。现在我们要进行一些微小的配置。这个类的完整源代码(包括附加的注释)都在本章的源码中能够找到。
配置自定义的RememberMeServices实现需要两步来完成。第一步是修改dogstore-base.xml Spring配置文件,以添加我们刚刚完成类的Spring Bean声明:
第二个要进行的修改是Spring Security的XML配置文件。修改<remember-me>元素来引用自定义的Spring Bean,如下所示:
最后为<user-service>声明添加一个id属性,如果它还没有添加的话:
重启web应用,你将能看到新的IP过滤功能已经生效了。
因为remember me cookie是Base64编码的,我们能够使用一个Base64解码的工具得到cookie的值以证实我们的新增功能是否生效。如果我们这样做的话,我们能够看到一个名为SPRING_SECURITY_REMEMBER_ME_COOKIE的cookie的内容大致如下所示:
guest:1251695034322:776f8ad44034f77d13218a5c431b7b34:127.0.0.1 |
正如我们所料,你能够看到IP地址确实存在于cookie的结尾处。在IP地址之前,你还能够分别看到用户名、时间戳以及MD5的哈希值。
【调试remember me cookie。在尝试调试remember me功能时,会有两个难点。第一个就是得到cookie的值本身!Spring Security并没有提供记录我们设置的cookie值的日志级别。我们推荐使用基于浏览器的工具如Mozilla Firefox下的Chris Pederick's Web Developer插件(http://chrispederick.com/work/web-developer/)。基于浏览器的开发工具一般允许查看(甚至编辑)cookie的值。第二个困难(相对来说较小)就是解码cookie的值。你能使用在线或离线的Base64解码工具来对cookie的值进行解码(需要记住的是添加一个等号符(=)结尾,以使其成为一个合法的Base64编码值)。】
如果用户是在一个共享的或负载均衡的网络设施下,如multi-WAN公司环境,基于IP的remember me tokens可能会出现问题。但是在大多数场景下,添加IP地址到remember me功能能够为用户提供功能更强、更好的安全层。
好奇的读者可能会关心remember me form的checkbox名(_spring_security_remember_me)以及cookie的名(SPRING_SECURITY_REMEMBER_ME_COOKIE),是否能够修改。<remember-me>声明是不支持这种扩展性的,但是现在我们作为一个Spring Bean声明了自己的RememberMeServices实现,那我们能够定义更多的属性来改变checkbox和cookie的名字:
不要忘记的是,还需要修改login.jsp页面中的checkbox form域以与我们声明的parameter值相匹配。我们建议你进行一下实验以确保理解这些设置之间的关联。
(如果想更好的理解本章节内容,建议阅读一下Spring Security的源码——译者注)
现在我们将要对基于内存的UserDetailsService进行简单的扩展以使其支持用户修改密码。因为这个功能对用户名和密码存于数据库的场景更有用,所以基于o.s.s.core.userdetails.memory.InMemoryDaoImpl扩展的实现不会关注存储机制,而是关注框架对这种方式扩展的整体流程和设计。在第四章中,我们将通过将其转移到数据库后台存储来进一步扩展我们的基本功能。
Spring Security框架提供的InMemoryDaoImpl内存凭证存储使用了一个简单的map来存储用户名以及关联的UserDetails。InMemoryDaoImpl使用的UserDetails实现类是o.s.s.core.userdetails.User,这个实现类将会在Spring Security API中还会看到。
这个扩展的设计有意的进行了简化并省略了一些重要的细节,如需要用户在修改密码前提供他们的旧密码。添加这些功能将作为练习留给读者。
我们要首先写自定义的类来扩展基本的InMemoryDaoImpl,并提供允许用户修改密码的方法。因为用户是不可改变的对象,所以我们copy已经存在的User对象,只是将密码替换为用户提交的值。在这里我们定义一个接口在后面的章节中将会重用,这个接口提供了修改密码功能的一个方法:
以下的代码为基于内存的用户数据存储提供了修改密码功能:
比较幸运的是,只有一点代码就能将这个简单的功能加到自定义的子类中了。我们接下来看看添加自定义UserDetailsService到pet store应用中会需要什么样的配置。
现在,我们需要重新配置Spring Security的XML配置文件以使用新的UserDetailsService实现。这可能比我们预想的要困难一些,因为<user-service>元素在Spring Security的处理过程中有特殊的处理。需要明确声明我们的自定义bean并移除我们先前声明的<user-service>元素。我们需要把:
修改为:
在这里我们看到的user-service-ref属性,引用的是一个id为userService的Spring Bean。所以在dogstore-base.xml Spring Beans配置文件中,声明了如下的bean:
你可能会发现,这里声明用户的语法不如<user-service>包含的<user>元素更易读。遗憾的是,<user>元素只能使用在默认的InMemoryDaoImpl实现类中,我们不能在自定义的UserDetailsService中使用了。在这里例子中,这个限制使得事情稍微复杂了一点,但是在实际中,没有人会愿意长期的将用户定义信息放在配置文件中。对于感兴趣的读者,Spring Security 3参考文档中的6.2节详细描述了以逗号分隔的提供用户信息的语法。
【高效使用基于内存的UserDetailsService。有一个常见的场景使用基于内存的UserDetailsService和硬编码的用户列表,那就是编写安全组件的单元测试。编写单元测试的人员经常编码或配置最简单的场景来测试组件的功能。使用基于内存的UserDetailsService以及定义良好的用户和GrantedAuthority值为测试编写人员提供了很可控的测试环境。】
到现在,你可以重启JBCP Pets应用,应该没有任何的配置错误报告。我们将在这个练习的最后的两步中,完成UI的功能。
我们接下来将会建立一个允许用户修改密码的简单页面。
这个页面将会通过一个简单的链接添加到“My Account”页面。首先,我们在/account/home.jsp文件中添加一个链接:
接下来,在/account/ changePassword.jsp文件中建立“Change Password”页面本身:
最后我们还要添加基于Spring MVC的AccountController来处理密码修改的请求(在前面的章节中我们没有介绍AccountController,它是账号信息主页的简单处理类)。
我们需要将对自定义UserDetailsService的应用注入到com.packtpub.springsecurity.web.controller.AccountController,这样我们就能使用修改密码的功能了。Spring的@Autowired注解实现了这一功能:
两个接受请求的方法分别对应渲染form以及处理POST提交的form数据:
完成这些配置后,重启应用,并在站点的“My Account”下找到“Change Password”功能。
比较精细的读者可能意识到这个修改密码的form相对于现实世界的应用来说太简单了。确实,很多的修改密码实现要复杂的多,并可能包含如下的功能:
l 密码确认——通过两个文本框,确保用户输入的密码是正确的;
l 旧密码确认——通过要求用户提供要修改的旧密码,增加安全性(这对使用remember me功能的场景特别重要);
l 密码规则校验——检查密码的复杂性以及密码是否安全。
你可能也会注意到当你使用这个功能的时,会被自动退出。这是因为SecurityContextHolder.clearContext()调用导致的,它会移除用户的SecurityContext并要求他们重新认证。在练习中,我们需要给用户做出提示或者找到方法让用户免于再次认证。
在本章中,我们更细节的了解了认证用户的生命周期并对JBCP Pet Store进行了结构性的修改。我们通过添加真正的登录和退出功能,进一步的满足了安全审计的要求,并提升了用户的体验。我们也学到了如下的技术:
l 配置并使用基于Spring MVC的自定义用户登录界面;
l 配置Spring Security的退出功能;
l 使用remember me功能;
l 通过记录IP地址,实现自定义的remember me功能;
l 实现修改密码功能;
l 自定义UserDetailsService和InMemoryDaoImpl。
在第四章中,我们将会使用基于数据库的认证信息存储并学习怎样保证数据库中的密码和其他敏感数据的安全。