序言
译者:leondu ,作者保留版权,转载请注明出处。
Acegi Security为基于J2EE的企业应用软件提供全面的安全解决方案。正如你在本手册中看到的那样,我们尝试为您提供有用的,高可配置的安全系统。
安全是一个永无止境的目标,获取一个全面的,系统级的实现方式是至关重要的。在安全界,我们鼓励你采用"分层安全",这样每个层都确保自身尽可能的安全,另 外的层提供另外的安全。每个层自身越"紧密",你的系统就越鲁棒和安全。在底层,你要处理传输入安全和系统认证,减少"中间人攻击"(man-in-the-middle attacks)。接下来你要使用防火墙,结合VPN或者IP安全来确保只有认证过的系统能够尝试连接。在企业环境中,你可能部署DMZ(demilitarized zone,隔离区)来把面向公众的服务器和后端的数据和应用服务器分隔开。在以非授权用户运行进程和文件系统安全最大化上,你的操作系统也将扮演一个关键的角色。接下来你要防止针对系统的拒绝服务和暴力攻击。入侵检测系统在检测和应对攻击方面尤其有用,这些系统可以实时屏蔽恶意TCP/IP地址。在更高层上,你的Java虚拟机需要进行配置,将授予不同Java类型的权限最小化,然后,你的应用程序要对添加针对自身特定问题域的安全配置。Acegi Security使后者-应用程序安全变得容易。
当然,你要正确对待上述提到的每个安全层,以及包含于每个层的管理因素。这样的管理因素具体包括:安全公告监测,补丁,人工诊断,审计,变更管理,工程管理系统,数据备份,灾难恢复,性能评测,负载监测,集中日志,应急反应程序等等。
Acegi Security专注于在企业应用安全层为您提供帮助,你将会发现和各式各样的需求和商业问题领域一样多。银行系统的需求和电子商务应用的需求不同。电子商务应用和售卖军用自动工具的公司的需求不同。这些客户化的需求使得应用安全显得有趣,富有挑战性而且物有所值。
本手册为Acegi Security 1.0.0的发布而大规模重新组织。请先阅读Part I 架构概览。手册的其余部分按照传统的手册方式编排,需要一定的基础才能阅读。
我们希望您会觉得手册有用,并且欢迎您提供反馈意见和建议。
最后,欢迎加入Acegi Security社区。
Part I. 架构概览
象其他的软件一样,Acegi Security也有在整个框架中都会使用的特定核心接口,类,和概念抽象。在手册的这一部分,在检视这些规划和执行Acegi Security集成所必须的核心要素之前,我们先介绍Acegi Security。
第一章. 简介
1.1. Acegi Security是什么?
Acegi Security为基于J2EE的企业软件应用提供全面的安全服务。特别是使用领先的J2EE解决方案-Srping框架开发的项目。如果您不是使用Spring开发企业应用,我们温馨提醒您仔细研究一下。熟悉Spring,尤其是依赖注射原理,会极大的帮助你快速掌握Acegi Security。
人们使用Acegi Security有很多种原因,不过通常吸引他们到这个项目的原因是他们在J2EE的 Servlet Specification 或者 EJB Specification中找不到迫切需要的典型企业应用场景。提到这些规范,特别要提出的是他们不是在WAR或者EAR级别可移植的。这样,如果你切换服务器环境,一般来说你要在目标环境中花费很多工夫来重新配置你的应用安全。使用Acegi Security解决了这些问题,并且为你提供了很多其他有用的,完全可定制的安全特性。
如你所知,安全包含两个主要操作。第一个被称为"认证",是为用户建立一个它所声明的principal。Principal通常代表用户,设备,或者其他能在你的应用中执行操作的其他系统。"授权"指判定一个principal能否在你的系统中执行某个操作。在到达授权判断之前,principal的的身份认证已经由认证过程执行过了。这些概念是通用的,不是Acegi Security特有的。
在认证层面,Acegi Security广泛支持各种认证模块。这些认证模块绝大多数是第三方提供,或者相关的标准组织开发的,例如Internet Engineering Task Force。作为补充,Acegi Security自己也提供了一些认证功能。
Acegi Security当前支持如下的认证技术。
• HTTP BASIC authentication headers (an IEFT RFC-based standard)
• HTTP Digest authentication headers (an IEFT RFC-based standard)
• HTTP X.509 client certificate exchange (an IEFT RFC-based standard)
• LDAP (a very common approach to cross-platform authentication needs, especially in large environments)
• Form-based authentication (for simple user interface needs)
• Computer Associates Siteminder
• JA-SIG Central Authentication Service (otherwise known as CAS, which is a popular open source single sign on system)
• Transparent authentication context propagation for Remote Method Invocation (RMI) and HttpInvoker (a Spring remoting protocol)
• Automatic "remember-me" authentication (so you can tick a box to avoid re-authentication for a predetermined period of time)
• Anonymous authentication (allowing every call to automatically assume a particular security identity)
• Run-as authentication (which is useful if one call should proceed with a different security identity)
• Java Authentication and Authorization Service (JAAS)
• Container integration with JBoss, Jetty, Resin and Tomcat (so you can still use Container Manager Authentication if desired)
• 你自己的认证系统 (如下所示)
很多独立软件供应商(ISVs)选择Acegi Security是因为它具有丰富的认证模块。这样无论他们的终端客户需要什么,他们都可以快速集成到他们的系统中,不用花很多工夫或者让终端客户改变环境。如果Acegi Security System for Spring的7个认证模块还没有满足你的需求的话,Acegi Security是一个开放的系统,很容易写你自己的认证机制。许多Acegi Security的企业用户需要和"遗留"系统集成,这些遗留系统不遵循任何安全标准,Acegi Security能够和这样的系统"合作愉快"。
有时候基本的认证是不够的。有时候你需要根据principal和应用交互的方式来应用不同的安全措施。例如,你可能为了防止密码被窃取,或者防止终端用户受到"中间人"攻击,需要保证到达的是请求通过HTTPS的。或者,你要确保是一个真正的人而不是某种机器人或者自动进程在发送请求。这对于保护密码恢复不受暴力破解攻击,或者防止他人很容易的复制你应用的关键内容。为了帮助你实现这些目标,Acegi Security完全支持自动"通道安全"("channel security"),以及集成Jcaptcha来检测是否是真正人类用户。
Acegi Security不仅提供了认证功能,而且提供了完备的授权功能。在授权方面主要有三个领域,授权web请求,授权方法调用,授权存取单个领域对象实例。为了帮助你理解这些区别,对照考虑一下Servlet 规范中的web模式安全的授权功能,EJB容器管理安全以及文件系统安全。Acegi Security提供了所有这些重要领域的完备功能,我们将在本手册的后面介绍。
1.2. 历史
Acegi Security始于2003年晚期,当时在Spring Developers邮件列表中有人提问是否有人考虑提供一个基于Spring的安全实现。当时,Srping的社区是相对比较小的(尤其是和今天相比!),实际上Spring本身也是2003年早期才作为一个SourceForge项目出现的。对此问题的回应是它确实是一个值得研究的领域,虽然限于时间无法进行深入。
有鉴于此,这个简单的安全实现虽然构建了但是并没有发布。几周以后,Spring社区的其他成员询问了安全框架,代码就被提供给了他们。
随后又有人请求,到了2004年一月,大约有20人左右在使用这些代码。另外一些人加入到这些先行的用户中来,并建议建立一个SourceForge项目,这个项目在2004年3月建立起来。
在早期,该项目自身并不具备任何认证模块。认证过程依赖容器管理安全(Container Managed Security)而Acegi Security注重授权。在一开始这样是合适的,但是随着越来越多的用户要求额外的容器支持,基于容器的认证的限制就显示出来了。另外一个相关的问题是添加新的JAR文件到容器的classpath,通常会让最终用户感到困惑并且配置错误。
随后,Acegi Security加入了认证服务。大约一年后,Acegi Security成为了一个Spring Framework官方子项目。在2年半多的在多个软件项目中的活跃使用以及数以百计的改进和社区贡献,1.0.0最终版在2006年5月发布。
今天,Acegi Security成为一个强大而活跃的社区。在支持论坛上有数以千计的帖子。14个开发人员专职开发,一个活跃的社区也定期共享补丁并支持他们的同侪。
1.3. 发行版本号
理解Acegi Security的版本号是非常好处的,它可以帮助你判定升级的到新的版本是否需要花费很大精力。我们的正式发行版本使用Apache Portable Runtime Project版本指引,可以在下述网站查看http://apr.apache.org/versioning.html。为了您查看方便,我们引用该页的说明部分如下:
"版本号由三个部分的整数组成:主版本号(MAJOR)、副版本号(MINOR)、补丁版本号(PATCH)。主要的含义是主版本号(MAJOR)是不兼容的,API大规模升级。副版本号(MINOR)在源文件和可执行版和老版本保持兼容,补丁版本号(PATCH)则意味着向前和向后的完全兼容"。
第二章. 技术概览
2.1. 运行时环境
Acegi Security可以在JRE1.3中运行。这个发行版本中支持也Java 5.0,尽管对应的Java类型被分开打包到一个后缀是"tiger"的包中。因为Acegi Security致力于以一种自包含的方式运行,因此不需要在JRE中放置任何特殊的配置文件。特别无需配置Java Authentication and Authorization Service (JAAS)策略文件或者将Acegi Security放置到通用的classpath路径中。
同样的,如果你使用EJB容器或者Servlet容器,同样无需放置任何特别的配置文件或者将Acegi Security包含在服务器的类加载器(classloader)中。
上述的设计提供了最大的部署灵活性,你可以直接把目标工件(JAR, WAR 或者 EAR))直接从一个系统copy到另一个系统,它马上就可以运行起来。
2.2. 共享组件
让我们来看看Acegi Security中最重要的一些共享组件。所谓共享组件是指在框架中处于核心地位,系统脱离了它们之后就不能运行。这些Java类型代表了系统中其他部分的构建单元,因此理解它们是非常重要的,即使你不需要直接和它们打交道。
最基础的对象是SecurityContextHolder。在这里存储了当前应用的安全上下文(security context),包括正在使用应用程序的principal的详细信息。SecurityContextHolder默认使用ThreadLocal来存储这些详细信息,这意味着即便安全上下文(security context)没有被作为一个参数显式传入,它仍然是可用的。如果在当前principal的请求处理后清理线程,那么用这种方式使用ThreadLocal是非常安全的。当然, Acegi Security自动为你处理这些,所以你无需担心。
有些应用程序由于使用线程的方式而并不是完全适用ThreadLocal。例如,Swing客户端可能需要一个Java Virtual Machine中的所有线程都使用同样的安全上下文(security context)。在这种情况下你要使用SecurityContextHolder.MODE_GLOBAL模式。另外一些应用程序可能需要安全线程产生的线程拥有同样的安全标识符(security identity)。这可以通过SecurityContextHolder.MODE_INHERITABLETHREADLOCAL来实现。你可以通过两种方法来修改默认的SecurityContextHolder.MODE_THREADLOCAL。第一种是设置一个系统属性。或者,调用SecurityContextHolder的一个静态方法。大部分的应用程序不需要修改默认值,不过如果你需要,那么请查看SecurityContextHolder的JavaDocs获取更多信息。
我们在SecurityContextHolder中存储当前和应用程序交互的principal的详细信息。Acegi Security使用一个Authentication对象来代表这个信息。尽管你通常不需要自行创建一个Authentication对象,用户还是经常会查询Authentication对象。
你可以在你的应用程序中的任何地方使用下述的代码块:
Object obj =SecurityContextHolder.getContext().getAuthentication().getPrincipal();if (obj instanceof UserDetails) {String username =((UserDetails)obj).getUsername();} else {String username =obj.toString();}
上述的代码展示了一些有趣的联系和关键的对象。首先,你会注意到在SecurityContextHolder和Authentication之间有一个媒介对象。SecurityContextHolder.getContext() 方法实际上返回一个SecurityContext。Acegi Security使用若干个不同的SecurityContext实现,以备我们需要存储一些和principal无关的特殊信息。一个很好的例子就是我们的Jcaptcha集成,它需要知道一个需求是否是由人发起的。这样的判断和principal是否通过认证完全没有关系,因此我们将它保存在SecurityContext中。
从上述的代码片段可以看出的另一个问题是你可以从一个Authentication对象中获取一个principal。Principal只是一个对象。通常可以把它cast为一个UserDetails对象。UserDetails在Acegi Security中是一个核心接口,它以一种扩展以及应用相关的方式来展现一个principal。可以把UserDetails看作是你的用户数据库和Acegi Security在SecurityContextHolder所需要的东西之间的一个适配器(adapter)。作为你自己用户数据库的一个展现,你可能经常要把它cast到你应用程序提供的原始对象,这样你就可以调用业务相关的方法(例如 getEmail(), getEmployeeNumber())。
现在你可能已经开始疑惑,那我什么时候提供UserDetails对象呢?我要如何提供呢?
我想你告诉过我这个东西是声明式的,我不需要写任何Java代码-那怎么做到呢?对此的简短回答就是有一个叫做UserDetailsService的特殊接口。这个接口只有一个方法,接受一个Sring类型的参数并返回一个UserDetails。Acegi Security提供的大多数认证提供器将部分认证过程委派给UserDetailsService。UserDetailsService用来构建保存在SecurityContextHolder中的Authentication对象。好消息是我们提供若干个UserDetailsService的实现,包括一个使用in-memory map和另一个使用JDBC的。
大多数用户还是倾向于写自己的实现,这样的实现经常就是简单的构建于已有的Data Access Object (DAO)上,这些DAO展现了他们的雇员、客户、或者其他企业应用程序中的用户。要记得这样做的好处,不论你的UserDetailsService返回什么,它总是可以从SecurityContextHolder中获取,象上面的代码显示的那样。
除了principal,Authentication提供的另一个重要方法就是getAuthorities()。这个方法返回一个GrantedAuthority对象数组。GrantedAuthority,毫无疑问,就是授予principal的权限。这些权限通常是"角色",例如ROLE_ADMINISTRATOR 或者ROLE_HR_SUPERVISOR。这些角色稍后配置到web授权,方法授权和领域对象授权。Acegi Security的其他部分能够处理这些权限,并且期待他们被提供。通常你会从UserDetailsService中返回GrantedAuthority对象。
通常GrantedAuthority对象都是应用范围的权限。它们都不对应特定的领域对象。因此,你应该不会有一个代表54号员工对象的GrantedAuthority,因为这样会有数以千计的authority,你马上就会用光所有内存(或者,至少会让系统花太长时间来认证一个用户)。当然,Acegi Security会高效的处理这种普遍的需求,但是你不会使用领域对象安全功能来实现这个目的。
最后,但不是不重要,你有时候需要在HTTP 请求之间存储SecurityContext。另外有些时候你在每次请求的时候都会重新认证principal,不过大部分时候你会存储SecurityContext。HttpSessionContextIntegrationFilter在HTTP之间存储SecurityContext。正如类名字显示的那样,它使用HttpSession来进行存储。基于安全原因,你永远都不要直接和HttpSession交互。没有理由这么做,所以记得使用SecurityContextHolder来代替。
让我们回忆一下,Acegi Security的基本组成构件是:
• SecurityContextHolder,提供对SecurityContext的所有访问方式。
• SecurityContext, 存储Authentication以及可能的请求相关的安全信息。
• HttpSessionContextIntegrationFilter, 在web请求之间把SecurityContext存储在HttpSession中。
• Authentication, 以Acegi Security的方式表现principal。
• GrantedAuthority, 表示赋予一个principal的应用范围的权限。
• UserDetails, 为从你的应用程序DAO中获取必要的信息来构建一个Authentication 对象。
• UserDetailsService,用传入的String类型的username(或者认证ID,或类似)来创建一个UserDetails。
现在你已经理解了这些重复使用的组件,让我们仔细看看认证过程吧。
2.3. 认证
正如我们在手册开始部分所说的那样,Acegi Security适用于很多认证环境。虽然我们建议大家使用Acegi Security自身的认证功能而不是和容器管理认证(Container Managed Authentication)集成,但是我们仍然支持这种和你私有的认证系统集成的认证。让我们先从Acegi Security完全自行管理管理web安全的角度来探究一下认证,这也是最复杂和最通用的情形。
想象一个典型的web应用的认证过程:
1.你访问首页,点击一个链接。
2.一个请求被发送到服务器,服务器判断你是否请求一个被保护的资源。
3.因为你当前未被认证,服务器发回一个回应,表明你必须通过认证。这个回应可能是一个HTTP回应代码,或者重定向到一个特定的网页。
4.基于不同的认证机制,你的浏览器会重定向到一个网页好让你填写表单,或者浏览器会用某种方式获取你的身份认证(例如一个BASIC认证对话框,一个cookie,一个X509证书等等。)。
5.浏览器会发回给服务器一个回应。可能是一个包含了你填写的表单内容的HTTP POST,或者一个包含你认证详细信息的HTTP header。
6.接下来服务器会判断提供的认证信息是否有效。如果它们有效,你进入到下一步。如果它们无效,那么通常请求你的浏览器重试一次(你会回到上两步)。
7.你引发认证的那个请求会被重试。但愿你认证后有足够的权限访问那些被保护的资源。如果你有足够的访问权限,请求就会成功。否则,你将会受到一个意味"禁止"的HTTP403错误代码。
在Acegi Security中,对应上述的步骤,有对应的类。主要的参与者(按照被使用的顺序)是:ExceptionTranslationFilter, AuthenticationEntryPoint, 认证机制(authentication mechanism), 以及AuthenticationProvider。
ExceptionTranslationFilter是Acegi Security用来检测任何抛出的安全异常的过滤器(filter)。这种异常通常是由AbstractSecurityInterceptor抛出的,它是授权服务的主要提供者。我们将会在下一部分讨论AbstractSecurityInterceptor,现在我们只需要知道它产生Java异常,并且对于HTTP或者如何认证一个principal一无所知。反而是ExceptionTranslationFilter提供这样的服务,它负责要么返回403错误代码(如果principal通过了认证,只是缺少足够的权限,象上述第7步那样),要么加载一个AuthenticationEntryPoint (如果principal还没有被认证,那么我们要从第3步开始)。
AuthenticationEntryPoint负责上述的第3步。如你所想,每个web应用都有一个默认的认证策略(象Acegi Security中几乎所有的东西一样,它也是可配置的,不过我们现在还是还是从简单开始)。每个主流的认证系统都有它自己的AuthenticationEntryPoint实现,负责执行第3步中描述的动作。
当浏览器确定要发送你的认证信息(HTTP 表单或者HTTP header),服务器上需要有什么东西来"收集"这些认证信息。现在我们在上述的第6步。在Acegi Security中对从用户代理(通常是浏览器)收集认证信息有一个特定的名字,这个名字是"认证机制(authentication mechanism)"。当认证信息从客户代理收集过来以后,一个"认证请求(Authenticationrequest)"对象被创建,并发送到AuthenticationProvider。
Acegi Security中认证的最后一环是一个AuthenticationProvider。非常简单,它的职责是取用一个认证请求(Authentication request)对象,并且判断它是否有效。这个provider要么抛出一个异常,要么返回一个组装完毕的Authentication对象。还记得我们的好朋友UserDetails 和 UserDetailsService吧?如果没有,回到前一部分重新回忆一下。大部分的AuthenticationProviders都会要求UserDetailsService提供一个UserDetails对象。如前所述,大部分的应用程序会提供自己的UserDetailsService,尽管有些会使用Acegi Security提供的JDBC或者 in-memory实现。作为成品的UserDetails 对象,特别是其中的GrantedAuthority[]s,在构建完备的Authentication对象时会被使用。
当认证机制(authentication mechanism)取回组装完全的Authentication对象后,它将会相信请求是有效的,将Authentication放到SecurityContextHolder中,并且将原始请求取回(上述第7步)。反之,AuthenticationProvider则拒绝请求,认证机制(authentication mechanism)会请求用户重试(上述第2步)。
在讲述典型的认证流程的同时,有个好消息是Acegi Security不关心你是如何把Authentication放到SecurityContextHolder内的。唯一关键的是在AbstractSecurityInterceptor授权一个请求之前,在SecurityContextHolder中包含一个代表了principal的Authentication。
你可以(很多用户确实)实现自己的过滤器(filter)或者MVC控制器(controller)来提供和不是基于Acegi Security的认证系统交互。例如,你可能使用使用容器管理认证(Container Managed Authentication),从ThreadLocal或者JNDI中获取当前用户信息,使得它有效。或者,你工作的公司有一个遗留的私有认证系统,而它是公司"标准",你对它无能为力。在这种情况之下也是非常容易让Acegi Security运作起来,提供认证能力。你所需要做的是写一个过滤器(或等价物)从某处读取第三方用户信息,构建一个Acegi Security式的Authentication对象,把它放到SecurityContextHolder中。这非常容易做,也是一种广泛支持的集成方式。
2.4. 安全对象
如果你熟悉AOP,你会知道有很多种advice可用:before, after, throws 和 around。around advice非常有用,因为它能够选择是否选择是否执行一个方法调用,是否修改返回值,以及是否抛出异常。Acegi Security对方法调用和web请求都提供around advice。我们使用AOP联盟实现对方法调用的around advice,对于web请求的around advice则是使用标准的过滤器(Filter)。
对于那些不熟悉AOP的人来说,关键是要理解Acegi Security能够帮助你保护方法调用以及web请求。大多数人对保护他们服务层的方法调用感兴趣。这是因为在当前的J2EE应用中,服务层包含了大多数的业务逻辑(声明,作者不赞成这种设计,反而支持正确封装的领域模型以及DTO,assembly, facade 以及 transparent persistence patterns,而不是当前主流的贫血模型,我们将在这里讨论)。如果你需要保护service层的方法调用,使用标准的Spring AOP平台(或者被成为AOP 联盟(AOP Alliance))就足够了。如果你需要直接对领域模型进行保护,那么可以考虑使用AspectJ。
你可以选择对使用AspectJ 或者AOP联盟(AOP Alliance)对方法进行授权,或者你可以选择使用过滤器(filter)来对web请求进行授权。你将0个,1个,2个或者3个这些方法一起使用。主流的用法是执行一些web请求授权,以及在服务层使用AOP联盟(AOP Alliance)对一些方法调用授权。
Acegi Security使用"安全对象"(secure object)这个词来指任何能够将安全应用于其上的对象。每个Acegi Security支持的安全对象都有自己的类,它是AbstractSecurityInterceptor的子类。重要的一点是,如果一个principal通过认证,当AbstractSecurityInterceptor执行的时候,SecurityContextHolder中要包含一个有效的Authentication。
AbstractSecurityInterceptor提供一个固定的工作流程来处理安全对象请求。这个工作流程包括查找和当前请求相关联的"配置属性(configuration attributes)"。配置属性(configuration attributes)可以被认为是对被AbstractSecurityInterceptor使用的类有特殊含义的字符串。他们通常针对AbstractSecurityInterceptor使用XML进行配置。反正,AbstractSecurityInterceptor会询问AccessDecisionManager "这是配置属性(configuration attributes),这是当前的认证对象(Authentication object),这是当前请求的详细信息-那么这个特定的principal可以执行这个特定的操作吗?"。
假如AccessDecisionManager判定允许这个请求,那么AbstractSecurityInterceptor一般来说就继续执行请求。虽然这样,用户在少数情况之下可能需要替换SecurityContext中的Authentication,可以通过AccessDecisionManager调用一个RunAsManager来实现。在某些不常见的情形下这将非常有用,例如服务层的方法需要用另一种标识(身份)来调用远程系统。这可能有所帮助,因为Acegi Security自动在不同的服务器之间传播安全标识(假设你正确配置了RMI或者HttpInvoker remoting protocol client)。
随着安全对象处理和返回-意味着方法调用完毕或者过滤器链(filter chain)处理完毕-AbstractSecurityInterceptor有最后的机会来处理调用。这时,AbstractSecurityInterceptor可能会修改返回的对象。我们可能要这样做,因为授权判断不能在安全对象调用途中执行。由于高度的可插拔性,如果需要AfterInvocationManager将控制权交给AfterInvocationManager来实际修改对象。这个类甚至可以彻底替换对象,或者抛出异常,或者根本不修改它。
因为是AbstractSecurityInterceptor中心模版类,看起来第一副插图该献给它。(译注:原手册里的图画的太丑陋了,我用jude重新画了一遍)
图1 关键"安全对象"模型
只有那些希望实现全新的对请求进行截取截取和授权方式的开发者才需要直接使用安全对象。例如,可能构建一个新的安全对象安全调用一个消息系统。任何需要安全并且能够提供一种截取调用的方式(例如AOP around advice semantics)的东西都可以成为安全对象。虽然如此,大部分的Spring应用都会只是透明应用当前支持的三种安全对象类型(AOP Alliance MethodInvocation, AspectJ JoinPoint 和 web request FilterInterceptor)。
2.5. 结论
恭喜!你已经获取了Acegi Security足够的概括性的图景来开始着手你的项目。我们探究了共享组件,认证过程,以及对"安全对象"的通用授权概念。手册中的余下部分你可能用到也可能用不到,可以按照任意顺序阅读。
第三章. 协助系统
本章介绍一些Acegi Security使用的附加和协助系统。那些和安全无关,但是包含在Acegi Security项目中的部分,将会在本章中讨论
3.1. 本地化
Acegi Security支持对终端客户可能会看到的异常信息进行本地化。如果你的应用是为英文用户设计的,那么你什么都不用做,因为Acegi Security的所有消息默认都是英文的。如果你要支持其他区域用户,那么本节包含了你所需要了解的所有东西。
包括认证失败或者访问被拒绝(授权失败)的所有异常消息都可以被本地化。提供给开发者或者系统部署人员的异常或者日志信息(包括错误的属性、接口不符、构造器错误、debug级日志)没有被本地化,它们硬编码在Acegi Security的代码中。
在acegi-security-xx.jar(译注:xx代表版本号)的org.acegisecurity包中包含了一个messages.properties文件。这个文件会被你的application context引用,因为Acegi Security实现了Spring的MessageSourceAware接口,它期待在application context启动的时候注入一个message resolver。通常你所需要做的是在你的application context中注册一个引用这个消息的bean,如下所示:
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"><property name="basename"><value>org/acegisecurity/messages</value></property></bean>
messages.properties是按照资源包标准命名的,它代表了Acegi Securtiy支持的默认语言。文件默认是英文的。如果你不注册一个消息源,Acegi Security仍然可以正常工作,它会用回硬编码的英文消息。
如果你想定制messages.properties文件,或者支持其他语言,那么你应该copy这个文件,然后重命名,并在上述的bean定义中注册。因为文件中的key并不多,因此本地化花不了多少工夫。如果你针对消息文件进行了本地化,那么请和社区分享,你可以添加一个JIRA任务,将你正确命名的messages.properties本地化文件作为附件添加。
为了完善关于本地化的讨论需要知道Spring的ThreadLocal org.springframework.context.i18n.LocaleContextHolder。你应该为每个用户设置代表他区域的LocaleContextHolder。Acegi Security会尝试从这个ThreadLocal中获取的Locale来从消息源中获取消息。请参考Spring的文档以获取更多使用LocaleContextHolder和能够帮你自动设置它的辅助类(例如
AcceptHeaderLocaleResolver, CookieLocaleResolver, FixedLocaleResolver, SessionLocaleResolver 等)的详细信息。
3.2. Filters
正如你在整个手册中看到的那样,Acegi Security使用很多filter。你可以使用FilterToBeanProxy或者FilterChainProxy来确定这些是怎样加入到你的web应用中的,下面我们来看看。
大部分filter使用FilterToBeanProxy来配置。例如下面web.xml中配置所示:
<filter> <filter-name>Acegi HTTP Request Security Filter</filter-name> <filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class> <init-param> <param-name>targetClass</param-name> <param-value>org.acegisecurity.ClassThatImplementsFilter</param-value> </init-param></filter>
注意在web.xml中的filter实际上是一个FilterToBeanProxy,而不是真正实现filter逻辑的filter。FilterToBeanProxy所作的是代理Filter的方法到一个从Spring的application context 获取的bean。这使得这个bean可以享受Spring application context的生命周期支持以及配置灵活性。这个bean必须实现javax.servlet.Filter。
FilterToBeanProxy只需要一个简单的初始化参数,targetClass或者targetBean。targetClass会定位application context中指定的类的第一个对象,而FilterToBeanProxy按照bean的名字定位对象。象标准的Spring web应用一样,FilterToBeanProxy使用WebApplicationContextUtils.getWebApplicationContext(ServletContext)来访问application context,所以你应该在web.xml中配置一个ContextLoaderListener。
在IoC容器而不是servlet容器中部署Filter会有一个生命周期的问题。特别是,哪个容器应该负责调用Filter的"startup" 和 "shutdown"方法?注意到Filter的初始化和析构顺序随servlet容器不同而不同,如果一个Filter依赖于由另一个更早初始化的Filter的配置,这样就会出现问题。另一方面,Spring IoC具备更加完善的生命周期/IoC接口(例如InitializingBean, DisposableBean, BeanNameAware, ApplicationContextAware以及其他许多)以及一个容易理解的接口契约(interface contract),可预见的方法调用顺序,自动装配支持,以及可以避免实现Spring接口的选项(例如Spring XML中的destroy-method 属性)。因此,我们推荐尽可能使用Spring生命周期服务而不是servlet容器生命周期服务。FilterToBeanProxy默认不会将init(FilterConfig) 和 destroy()方法委派到被代理的bean。如果你需要这些调用被委派,那么将lifecycle初始化参数设置为servlet-container-managed。
我们强烈推荐你使用FilterChainProxy而不是FilterToBeanProxy。虽然FilterToBeanProxy是一个非常有用的类FilterToBeanProxy,问题是当web.xml中filter变多时,<filter> 和 <filter-mapping>项就会太多而变得臃肿不堪。为了解决这个问题,Acegi Security提供一个FilterChainProxy类。它在FilterToBeanProxy中被装配(正如上面例子中所示),但目标类(target class)是org.acegisecurity.util.FilterChainProxy。这样过滤器链(filter chain)可以在application context中按照如下代码配置:
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy"> <property name="filterInvocationDefinitionSource"> <value> CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON PATTERN_TYPE_APACHE_ANT /webServices/*=httpSessionContextIntegrationFilterWithASCFalse,basicProcessingFilter,exceptionTranslationFilter, /*=httpSessionContextIntegrationFilterWithASCTrue,authenticationProcessingFilter,exceptionTranslationFilter,filterSecurityInterceptor </value> </property></bean>
你可能注意到FilterSecurityInterceptor定义方式的相似之处。同时支持正则表达式和Ant Paths格式,越对应的URI越早出现。在运行时,FilterChainProxy会定位符合当前的web请求的第一个URI模式。每个对应的配置属性代表了在application context中定义的一个bean的名字。接着fiter会按照它们被指定的顺序,按照FilterChain的标准行为模式被调用(如果一个Filter决定停止处理,它可以不在chain中执行)。
如你所见,FilterChainProxy需要为不同的请求模式重复配置filter的名字(在上面的例子中,, exceptionTranslationFilter 和 filterSecurityInterceptor 是重复的)。这样的设计是为了让FilterChainProxy能够为不同的URI配置不同的filter调用顺序,同时也提高了表达力(针对正则表达式、Ant Paths、以及任何FilterInvocationDefinitionSource的特定实现)和清晰度,可以知道是哪个filter应该被调用。
你可能注意到了我们在filter chain定义了两个HttpSessionContextIntegrationFilter (ASC是allowSessionCreation的缩写,是HttpSessionContextIntegrationFilter的一个属性)。因为web服务不会为将来的请求提供一个jsessionid,为这样的用户创建HttpSessions是浪费的。如果你有一个需要最大限度的伸缩性的高容量的应用,我们建议你使用上述的方法。对于小的应用,使用单一的HttpSessionContextIntegrationFilter (默认的allowSessionCreation设为true)应该足够了。
说到生命周期问题,如果对FilterChainProxy自身调用init(FilterConfig) 和 destroy()方法,它会把它代理到底层的filter。这样FilterChainProxy保证只初始化和析构每个filter一次,不论它在FilterInvocationDefinitionSource中定义了多少次。你可以通过FilterToBeanProxy的lifecycle初始化参数来控制这些方法是否被调用。如上面所讨论的那样,默认所有servlet容器生命周期调用是不被代理到FilterChainProxy的。
在web.xml中定义的filter的顺序是非常重要的。不管你实际用到哪个filter,<filter-mapping>的顺序应该是如下所示的:
1.ChannelProcessingFilter,因为可能要重定向到另一种协议。
2.ConcurrentSessionFilter 因为不使用任何SecurityContextHolder的功能,但是需要更新SessionRegistry来表示当前的发送请求的principal。
3. HttpSessionContextIntegrationFilter, 这样当一个web请求开始的时候就可以在SecurityContextHolder中设置一个SecurityContext,当web请求结束的时候任何对SecurityContext的改动都会被copy到HttpSession(以备下一个web请求使用)。
4.Authentication processing mechanisms - AuthenticationProcessingFilter, CasProcessingFilter, BasicProcessingFilter, HttpRequestIntegrationFilter, JbossIntegrationFilter 等 - 修改SecurityContextHolder,使其中包含一个有效的认证请求令牌(token)。
5.SecurityContextHolderAwareRequestFilter, 如果你使用它来在你的servlet容器中安装一个Acegi Security aware HttpServletRequestWrapper。
6.RememberMeProcessingFilter, 如果早期的认证处理过程没有更新SecurityContextHolder,并且请求(request)提供了一个cookie启用remember-me服务,一个合适的被记住的Authentication对象会被放到SecurityContextHolder那里。
7.AnonymousProcessingFilter, 如果早期的认证处理过程没有更新SecurityContextHolder,, 一个匿名Authentication 对象会被放到SecurityContextHolder那里。
8.ExceptionTranslationFilter, 捕获所有的Acegi Security 异常,这样要么返回一个HTTP错误响应或者加载一个对应的AuthenticationEntryPoint。
9.FilterSecurityInterceptor, 保护 web URIs
所有上述的filter使用FilterToBeanProxy或FilterChainProxy。建议在一个应用中使用一个单个的FilterToBeanProxy代理到一个单个的FilterChainProxy。,在FilterChainProxy中定义所有的Acegi Security Filters。如果你使用SiteMesh,确保Acegi Security filters 在 SiteMesh filters调用前调用。这样使SecurityContextHolder在SiteMesh decorator使用前能够及时被装配。
第四章. 信道安全
4.1. 概述
Acegi Security不仅能满足你的认证和授权的请求,而且能够保证你的未认证的web请求也能拥有某些属性。这些属性可能包括使用特定的传输类型,在HttpSession设置特定的属性等等。Web请求的最普遍的需求是使用特定的传输协议,例如HTTPS。
在传输安全中的一个重要议题就是会话劫持(session hijacking)。Web容器通过一个jsessionid来引用一个HttpSession,这个jsessionid通过cookie 或者URL重写转向(URL rewriting)发送到到客户端。如果jsessionid是通过HTTP发送的,那么就存在被劫持以及在认证过程之后冒充被认证用户的可能。这是因为大部分的web容器为特定的用户维护同一个会话标识符,即便是用户从HTTP 切换到 HTTPS页面。
如果对于你的特定应用来说,会话劫持(session hijacking)是一个很严重的风险,那么唯一的解决方法就是对每一个请求都使用HTTPS。这意味着jsessionid不会使用非安全信道传输。你要保证你的web.xml中定义<welcome-file>,把它指向一个HTTPS位置,同时应用程序不把用户指向一个HTTP位置。Acegi Security提供一个解决方案帮助你实现后者。
4.2. 配置
启用Acegi Security的信道安全服务,需要在web.xml中增加如下行:
<filter><filter-name>Acegi Channel Processing Filter</filter-name><filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class><init-param><param-name>targetClass</param-name><param-value>org.acegisecurity.securechannel.ChannelProcessingFilter</param-value></init-param></filter><filter-mapping><filter-name>Acegi Channel Processing Filter</filter-name><url-pattern>/*</url-pattern></filter-mapping>
和平时一样,你同样需要在application context中配置filter
<bean id="channelProcessingFilter" class="org.acegisecurity.securechannel.ChannelProcessingFilter"><property name="channelDecisionManager"><ref bean="channelDecisionManager"/></property><property name="filterInvocationDefinitionSource"><value>CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON\A/secure/.*\Z=REQUIRES_SECURE_CHANNEL\A/acegilogin.jsp.*\Z=REQUIRES_SECURE_CHANNEL\A/j_acegi_security_check.*\Z=REQUIRES_SECURE_CHANNEL\A.*\Z=REQUIRES_INSECURE_CHANNEL</value></property></bean><bean id="channelDecisionManager" class="org.acegisecurity.securechannel.ChannelDecisionManagerImpl"><property name="channelProcessors"><list><ref bean="secureChannelProcessor"/><ref bean="insecureChannelProcessor"/></list></property></bean><bean id="secureChannelProcessor" class="org.acegisecurity.securechannel.SecureChannelProcessor"/><bean id="insecureChannelProcessor" class="org.acegisecurity.securechannel.InsecureChannelProcessor"/>
ChannelProcessingFilter和FilterSecurityInterceptor一样支持Apache Ant style paths。
ChannelProcessingFilter的工作方式是过滤所有的web请求,并将判断将适合的配置属性应用于其上。然后它代理到ChannelDecisionManager。默认的实现类ChannelDecisionManagerImpl应该能够满足大多数需求。它就代理到配置好的ChannelProcessor实例列表。ChannelProcessor会检视请求,如果它不满意请求(例如请求是发送自不正确的传输协议)它将会重定向,抛出异常或者采取其他任何恰当的措施。
Acegi Security 包括ChannelProcessor两个实体类实现:SecureChannelProcessor 保证配置了REQUIRES_SECURE_CHANNEL 属性的请求都是从HTTPS发送过来的。而InsecureChannelProcessor 保证配置了REQUIRES_INSECURE_CHANNEL 属性的请求都是从HTTP发送过来的。如果没有使用请求的协议,这两个实现都会转到ChannelEntryPoint,而两个ChannelEntryPoint 实现所作的就是简单的把请求相应按照HTTP 和 HTTPS重定向。
要注意重定向是绝对(例如http://www.company.com:8080/app/page)而不是相对的(例如 /app/page)。在测试中发现Internet Explorer 6 Service Pack 1 有一个bug,因此如果在重定向的时候也改变使用的端口,它就不能正确响应。对应这个bug,在很多Acegi Security bean中都会使用的PortResolverImpl也使用绝对URL。请参阅PortResolverImpl的JavaDoc以获取更多信息。
你要注意使用为了在登录过程中保证用户名和密码的安全,要使用安全信道。如果你配合基于表单的登录使用ChannelProcessingFilter,请记得一定要把你的登录页面设置为REQUIRES_SECURE_CHANNEL,并且AuthenticationProcessingFilterEntryPoint.forceHttps属性设置为true。
4.3. 结论
一旦配置好了,使用安全信道是非常简单的。只要请求页面,不用管使用什么协议(HTTP 或 HTTPS)或什么端口(80, 8080, 443, 8443等)。显然你只要确定初始请求(获取通过在web.xml 中的<welcome-file> 或一个众所周知的主页URL),完成以后filter会执行你application context定义的重定向。
你也可以在ChannelDecisionManagerImpl中增加自己的ChannelProcessor实现。例如,你可能通过"输入图片中的内容"检测到一个个人类用户,然后在HttpSession中设置一个属性。
要判断一个安全检查应该是或者ChannelProcessor或是 AccessDecisionVoter 记得前者是设计用来处理认证或者未认证的请求,而后者是设计用来处理已认证的请求。因此后者可以访问已认证的principal被授予的权限。
另外,ChannelProcessor检测到问题后一般是引发一个HTTP/HTTPS重定向这样他的请求可以被满足,而AccessDecisionVoter将则会跑出一个AccessDeniedException异常(取决于支配的 AccessDecisionManager)。
文章来源:http://wiki.springside.org.cn/display/springside/Acegi+Reference+Preface