2012-03-17 08:46:44
《Spring Security 3》 【第二章】 Spring Security起步(3) 浏览(4922)|评论(0) 交流分类:Java|笔记分类: Spring Sec……
安全的复杂之处:安全web请求的架构借助于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过滤器是如何封装一个用户请求的:
与生活中金属制定的链类似,每一个servelt过滤器代表链上的一环,它会进行方法的调用以处理用户的请求。请求穿过整个过滤器链,按顺序调用每个过滤器。
Spring Security使用了过滤器链的概念并实现了自己抽象,提供了VirtualFilterChain,它可以根据Spring Security XML配置文件中设置的URL模式动态的创建过滤器链(可以将它与标准的Java EE过滤器链进行对比,后者需要在web应用的部署描述文件中进行设置)。 【Servlet过滤器除了能够如它的名字所描述的那样进行过滤功能(或阻止请求)以外,还可以用于很多其他的目的。实际上,很多的servlet过滤器的功能类似于在web运行的环境中对请求进行AOP式的代理拦截,因为它们可以允许一些功能在任何发往servelt容器的请求处理之前或之后执行。过滤器能实现的多功能在Spring Security中页得到了体现,因为很多过滤器实际上并不直接影响用户的请求。】
自动配置的选项为你建立了十个Spring Security的过滤器。理解这些过滤器的默认行为以及它们在哪里以及如何配置的,对使用Spring Security的高级功能至关重要。
这些过滤器以及它们使用的顺序,在下面的表格中进行了描述。大多数这些过滤器在我们完善JBCP Pets在线商店的过程中都会被再次提到,所以如果你现在不明白它们的确切功能也不必担心。
Spring Security拥有总共大约25个过滤器,它们能够根据你的需要进行适当的应用以改变用户请求的行为。当然,如果需要的话,你也可以添加你自己实现了javax.servlet.Filter接口的过滤器。 请记住,如果你在XML配置文件中使用了auto-config属性,以上表格中列出的过滤器自动添加的。通过使用一些额外的配置指令,以上列表中的过滤器能够精确的控制是否被包含,在后续的章节章将会进行介绍。
你可能会完全从头做起来配置过滤器链。尽管这会很单调乏味,因为有很多的依赖关系要配置,但是它为配置和应用场景的匹配方面提供了更高层次的灵活性。我们将在第六章讲述在启动的过程中所依赖的Spring Bean的声明。
你可能想知道DelegatingFilterProxy是怎样找到Spring Security配置的过滤器链的。让我们回忆一下,在web.xml文件中,我们需要给DelegatingFilterProxy一个过滤器的名字:
这个过滤器的名字并不是随意配置的,实际上会跟根据这个名字把Spring Security织入到DelegatingFilterProxy。除非明确配置,否则DelegatingFilterProxy会在Spring WebApplicationContext中寻找同名的配置bean(名字是在filter-name中指明的)。更多配置DelegatingFilterProxy的细节可以在这个类对应的Javadoc中找到。 在auto-config场景下,发生了什么事情?在Spring Security 3中,使用auto-config会自动提供以下三个认证相关的功能: HTTP基本认证 Form登录认证 退出 值得注意的是,也可以使用配置元素实现这三个功能,能够实现比使用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登录中学到的概念是如何应用到更高级的认证机制中。 涉及到认证功能的重要接口在下边的图标中有一个概览性的描述:
有两个重要接口的实现是在认证链中被这些参与的类初始化的,它们用来封装一个认证过(或还没有认证过的)的用户的详细信息和权限。 o.s.s.core.Authentication是你以后要经常接触到的接口,因为它存储了用户的详细信息,包括唯一标识(如用户名)、凭证信息(如密码)以及本用户被授予的一个或多个权限(o.s.s.core. GrantedAuthority)。开发人员通常会使用Authentication对象来获取用户的详细信息,或者使用自定义的认证实现以便在Authentication对象中增加应用依赖的额外信息。
以下列出了Authentication接口可以实现的方法:
你可能会担心的发现,Authentication接口有好几个方法的返回值是简单的java.lang.Object。这可能会导致在编译阶段很难知道调用Authentication对象的方法返回值是什么类型的对象。
需要注意的一点是AuthenticationProvider并不是直接被AuthenticationManager接口使用或引用的。但是Spring Security只提供了AuthenticationManager的一个具体实现类,即o.s.s.authentication.ProviderManager,它会使用一个或更多以上描述的AuthenticationProvider实现类。因为AuthenticationProvider的使用非常普遍并且被很好的集成在ProviderManager中,所以理解它在最常见的基本配置下是如何工作的就非常重要了。
让我们更仔细的看看在基于web用户名和密码认证的请求下,这些类的处理过程:
|
<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包含两个域,可能在提供调试失败信息以及报告信息给用户方面很有用处。
authentication:存储关联认证请求的Authentication实例;
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接口定义了两个简单而合理的方法,它们能够用于请求的决策判断流程:
supports:这个逻辑操作实际上包含两个方法,它们允许AccessDecisionManager的实现类判断是否支持当前的请求。
decide:基于请求的上下文和安全配置,允许AccessDecisionManager去核实访问是否被允许以及请求是否能够被接受。decide方法实际上没有返回值,通过抛出异常来表明对请求访问的拒绝。
与AuthenticationException及其子类在认证过程中的使用很类似,特定类型的异常能够表明应用在授权决策中的不同处理结果。o.s.s.access.AccessDeniedException是在授权领域里最常见的异常,因此值得过滤器链进行特殊的处理。我们将在第六章中详细介绍它的高级配置。
AccessDecisionManager是能够通过标准的Spring bean绑定和引用实现完全的自定义配置。AccessDecisionManager的默认实现提供了一个基于AccessDecisionVoter接口和投票集合的授权机制。
投票器(voter)是在授权过程中的一个重要角色,它的作用是评估以下的内容:
要访问受保护资源的请求所对应上下文(如URL请求的IP地址);
用户的凭证信息(如果存在的话);
要试图访问的受保护资源;
系统的配置以及要访问资源本身的配置参数。
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) | 投票器对是否能够访问做了弃权处理(即没有做出决定)。可能在多种原因下发生,如: 投票器没有确凿的判断信息; 投票器不能对这种类型的请求做出决策。 |
正如你从访问决策相关类和接口的设计中可以猜到的那样,Spring Security的这部分被精心设计,所以认证和访问控制的使用场景并不仅仅限于web领域。我们将会在:精确的访问控制中关于方法级别的安全时,再次讲解投票器和访问控制管理。
当将他们组合在一起,“对web请求的默认认证检查”的整体流程将如下图所示:
我们可以看到ConfigAttribute能够从配置声明(在DefaultFilterInvocationSecurityMetadataSource类中保存)中传递数据到投票器,投票器并不需要其他的类来理解ConfigAttribute的内容。这种分离能够为新类型的安全声明(例如我们将要看到的方法安全声明)使用相同的访问决策模式提供基础。
配置access decision集合
实际上Spring Security允许通过security命名空间来配置AccessDecisionManager。<http>元素的access-decision-manager-ref属性来指明一个实现了AccessDecisionManager的Spring Bean。Spring Security提供了这个接口的三个实现类,都在o.s.s.access.vote包中:
类名 | 描述 |
AffirmativeBased | 如果有任何一个投票器允许访问,请求将被立刻允许,而不管之前可能有的拒绝决定。 |
ConsensusBased | 多数票(允许或拒绝)决定了AccessDecisionManager的结果。平局的投票和空票(全是弃权的)的结果是可配置的。 |
UnanimousBased | 所有的投票器必须全是允许的,否则访问将被拒绝。 |
配置使用UnanimousBased的访问决策管理器(access decision manager)
如果你想修改我们的应用来使用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 | 支持特定类型的声明,允许使用通配符: IS_AUTHENTICATED_FULLY——允许提供完整的用户名和密码的用户访问; IS_AUTHENTICATED_REMEMBERED——如果用户是通过remember me功能认证的则允许访问; IS_AUTHENTICATED_ANONYMOUSLY——允许匿名用户访问。 | access=" IS_AUTHENTICATED_ANONYMOUSLY" |
使用 Spring 表达式语言配置访问控制
基于角色标准投票机制的标准实现是使用 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 访问控制声明能够提供一种灵活的配置访问控制决策的方式。
在本章中,我们提供了安全领域两个重要概念即认证和授权的介绍。
在总体上了解我们要进行安全保护的系统;
使用 Spring Security 的自动配置在三步之内实现了我们应用的安全配置;
了解了在 Spring Security 中 servlet 过滤器的使用及重要性;
了解了认证和授权过程中重要的角色,包括一些重要类实现的详细介绍如 Authentication 和 UserDetails
体验了与访问控制规则有关的 SpEL 表达式的配置。
在接下来的一章中,我们将通过添加一些增强用户体验的功能,把基于用户名和密码的认证提高一个新的水平。
感谢 iteye lengyun3566 。
他的博客地址:http://lengyun3566.iteye.com
他的新浪微博:http://weibo.com/1920428940
本书源代码的地址:http://www.packtpub.com/support?nid=4435