第二章. 技术概览
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对象。
你可以在你的应用程序中的任何地方使用下述的代码块:
上述的代码展示了一些有趣的联系和关键的对象。首先,你会注意到在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)”。当认证信息从客户代理收集过来以后,一个“认证请求(Authentication request)”对象被创建,并发送到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.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)”。当认证信息从客户代理收集过来以后,一个“认证请求(Authentication request)”对象被创建,并发送到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中。这非常容易做,也是一种广泛支持的集成方式。