Spring Security 2.0学习笔记
spring 2.5也发布了,Acegi 2.0也出来了,发现里面也多了很多新特性,不过好多都是英文的,所以就到处看看,记些东西,谓之笔记也,呵呵。废话不多说,配置文件当然要从web.xml开始啊。看代码。使用安全框架第一步就是需要在web.xml文件中声明要使用的过滤器<filter></filter>
<
filter
>
< filter-name > springSecurityFilterChain </ filter-name >
< filter-class > org.springframework.web.filter.DelegatingFilterProxy </ filter-class >
</ filter >
< filter-mapping >
< filter-name > springSecurityFilterChain </ filter-name >
< url-pattern > /* </ url-pattern >
</ filter-mapping >
以上代码在Spring Securtity网络构建下定义了一个钩子,接着你就可以开始编辑你的应用程序配置文件(这个就不是Web.xml了)。网页安全服务使用<http>元素配置。
< filter-name > springSecurityFilterChain </ filter-name >
< filter-class > org.springframework.web.filter.DelegatingFilterProxy </ filter-class >
</ filter >
< filter-mapping >
< filter-name > springSecurityFilterChain </ filter-name >
< url-pattern > /* </ url-pattern >
</ filter-mapping >
给你们看个最小应用的<http>设置
<
http
auto-config
='true'>
<intercept-url pattern ="/**" access ="ROLE_USER" />
</ http >
以上代码说明希望我们应用程序的所有URLS都被保护起来,并要求由角色为ROLE_USER登入(注意:你可以使用若干的<intercept-url>元素为不同的URLS设置不同的用户角色登录请求,但是它们会在合适的排列中进行评估,而且第一个匹配的将被使用。所以你应该将最细节的配置部分放在最前面!)
<intercept-url pattern ="/**" access ="ROLE_USER" />
</ http >
为了加一些用户,你可以在命名空间里面定义一些测试数据:
<
authentication-provider
>
< user-service >
< user name ="jimi" password ="jimispassword" authorities ="ROLE_USER, ROLE_ADMIN" />
< user name ="bob" password ="bobspassword" authorities ="ROLE_USER" />
</ user-service >
</ authentication-provider >
注意:<http>元素是为创建一个FilterChainProxy和它要使用的若干filter beans,因为filter顺序不正确产生的问题,不会再出现了,现在这些过滤器的位置都是预定义好的。
< user-service >
< user name ="jimi" password ="jimispassword" authorities ="ROLE_USER, ROLE_ADMIN" />
< user name ="bob" password ="bobspassword" authorities ="ROLE_USER" />
</ user-service >
</ authentication-provider >
<authentication-provider>元素创建一个DaoAuthenticationProvider bean,<user-service>元素创建一个InMemoryDaoImpl.一个ProviderManageer bean 总是由命名空间处理系统所创建而且DaoAuthenticationProvider自动被ProviderManageer bean注册。
我们接着说测试用户,上面定义了2个用户,他们的密码和角色被应用于程序中的登录控制。它也可以从<user-service>的配置属性中载入来自标准的属性文件中的用户信息。使用<authentication-provider>元素意味着用户信息将被authentication manager用来处理认证请求。
接下来说下auto-config属性,就如上面的配置定义一样,像这样定义就可以了
<
http
>
< intercept-url pattern ="/**" access ="ROLE_USER" />
< form-login />
< anonymous />
< http-basic />
< logout />
< remember-me />
</ http >
以上这些元素负责启用用户登录、匿名认证、基础认证、注销和remember-me服务,他们都有各自的属性去改变他们的行为。auto-config需要一个UserDetailsService.在使用没有UserDetailsService的auto-config时在你的配置文件中会发成一个错误(比如你正在使用LDAP认证)。这是因为remember-me在auto-config="true"的时候会自动激活而且它还要求一个使用UserDetailsService运行的认证机制,那么如果你有一个因缺少UserDetailsService而产生的错误,请尝试移除auto-config的设置。
< intercept-url pattern ="/**" access ="ROLE_USER" />
< form-login />
< anonymous />
< http-basic />
< logout />
< remember-me />
</ http >
好了我们接下来谈下表单和基本登录选项,你可能会奇怪当提示你要登录的时候登录表单是从哪儿来的,因为我们并没有提供任何HTML文件或者JSP文件。事实上,我们并没有为登录页面明确的设置URL。 Spring Security 会产生一个自动的、基于自动激活并使用专为URL提交登录的标准参数特性,对于默认目标URL,用户会自动发送相应信息。然而,命名空间提供足够丰富的支持允许你自定义这些选项。比如,你想提供自己的登录页面,你可以这么做:
<
http
auto-config
='true'>
<intercept-url pattern ="/login.jsp*" filters ="none" />
< intercept-url pattern ="/**" access ="ROLE_USER" />
< form-login login-page ='/login.jsp'/>
</http >
你依然能用auto-config,form-login元素仅仅是重写了默认的设置,同样的我们已经添加了额外的intercept-url元素告诉登录页面所有的请求都应该被安全过滤器所处理,另外,请求也应该被模式/**进行匹配并且它不能转向它自己的登录页面,如果你想使用basic authentication来代替form login,那么请修改配置文件如下:
<intercept-url pattern ="/login.jsp*" filters ="none" />
< intercept-url pattern ="/**" access ="ROLE_USER" />
< form-login login-page ='/login.jsp'/>
</http >
<
http
auto-config
='true'>
<intercept-url pattern ="/**" access ="ROLE_USER" />
< http-basic />
</ http >
Basic authentication将会获得优先并将用于登录提示当一个用户试图访问被保护的资源。如果你希望使用它的话Form login依然可以在这份配置文件中使用。
<intercept-url pattern ="/**" access ="ROLE_USER" />
< http-basic />
</ http >
现实中,你会需要更大型的用户信息源,而不是写在application context里的几个名字。 多数情况下,你会想把用户信息保存到数据库或者是LDAP服务器里。 LDAP命名控件会在LDAP章里详细讨论,所以我们这里不会讲它。 如果你自定义了一个Spring Security的UserDetailsService实现,在你的application context中名叫"myUserDetailsService",然后你可以使用下面的验证。
<authentication-provider user-service-ref='myUserDetailsService'/>
如果你想用数据库,可以使用下面的方式
<authentication-provider>
<jdbc-user-service data-source-ref="securityDataSource"/>
</authentication-provider>
这里的"securityDataSource"就是 DataSource bean在application context里的名字,它指向了包含着Spring Security用户信息的表。 另外,你可以配置一个Spring Security JdbcDaoImpl bean,使用user-service-ref属性指定。
2.2.3.1. 添加一个密码编码器
你的密码数据通常要使用一种散列算法进行编码。 使用<password-encoder>元素支持这个功能。 使用SHA加密密码,原始的认证供应器配置,看起来就像这样:
<authentication-provider>
<password-encoder hash="sha"/>
<user-service>
<user name="jimi" password="d7e6351eaa13189a5a3641bab846c8e8c69ba39f" authorities="ROLE_USER, ROLE_ADMIN" />
<user name="bob" password="4e7421b1b8765d8f9406d87e7cc6aa784c4ab97f" authorities="ROLE_USER" />
</user-service>
</authentication-provider>
在使用散列密码时,用盐值防止字典攻击是个好主意,Spring Security也支持这个功能。 理想情况下,你可能想为每个用户随机生成一个盐值,不过,你可以使用从UserDetailsService读取出来的UserDetails对象中的属性。 比如,使用username属性,你可以这样用:
<password-encoder hash="sha">
<salt-source user-property="username"/>
</password-encoder>
你可以通过password-encoder的ref属性,指定一个自定义的密码编码器bean。 这应该包含application context中一个bean的名字,它应该是Spring Security的PasswordEncoder接口的一个实例。
2.3. 高级web特性
2.3.1. Remember-Me认证
参考Remember-Me章获得remember-me命名空间配置的详细信息。
2.3.2. 添加HTTP/HTTPS信道安全
如果你的同时支持HTTP和HTTPS协议,然后你要求特定的URL只能使用HTTPS,这时可以直接使用<intercept-url>的requires-channel属性:
<http>
<intercept-url pattern="/secure/**" access="ROLE_USER" requires-channel="https"/>
<intercept-url pattern="/**" access="ROLE_USER" requires-channel="any"/>
...
</http>
使用了这个配置以后,如果用户通过HTTP尝试访问"/secure/**"匹配的网址,他们会先被重定向到HTTPS网址下。 可用的选项有"http", "https" 或 "any"。 使用"any"意味着使用HTTP或HTTPS都可以。
如果你的程序使用的不是HTTP或HTTPS的标准端口,你可以用下面的方式指定端口对应关系:
<http>
...
<port-mappings>
<port-mapping http="9080" https="9443"/>
</port-mappings>
</http>
你可以在Chapter 7, Channel Security找到更详细的讨论。
2.3.3. 同步Session控制
如果你希望限制单个用户只能登录到你的程序一次,Spring Security通过添加下面简单的部分支持这个功能。 首先,你需要把下面的监听器添加到你的web.xml文件里,让Spring Security获得session生存周期事件:
<listener>
<listener-class>org.springframework.security.ui.session.HttpSessionEventPublisher</listener-class>
</listener>
然后,在你的application context加入如下部分:
<http>
...
<concurrent-session-control max-sessions="1" />
</http>
这将防止一个用户重复登录好几次-第二次登录会让第一次登录失效。 通常我们更想防止第二次登录,这时候我们可以使用
<http>
...
<concurrent-session-control max-sessions="1" exception-if-maximum-exceeded="true"/>
</http>
第二次登录将被阻止。
2.3.4. OpenID登录
命名空间支持OpenID登录,替代普通的表单登录,或作为一种附加功能,只需要进行简单的修改:
<http auto-config='true'>
<intercept-url pattern="/**" access="ROLE_USER" />
<openid-login />
</http>
你应该注册一个OpenID供应器(比如myopenid.com),然后把用户信息添加到你的内存<user-service>中:
<user name="http://jimi.hendrix.myopenid.com/" password="notused" authorities="ROLE_USER" />
你应该可以使用myopenid.com网站登录来进行验证了。
2.3.5. 添加你自己的filter
如果你以前使用过Spring Security,你应该知道这个框架里维护了一个过滤器链,来提供它的服务。 你也许想把你自己的过滤器添加到链条的特定位置,或者让已存在的过滤器,使用特定的版本。 你如何在命名空间配置里实现这些功能呢?过滤器链现在已经不能之间看到了。
过滤器顺序在使用命名空间的时候是被严格执行的。 每个Spring Security过滤器都实现了Spring的Ordered接口,这些过滤器在初始化的时候先被排好序了。 标准的过滤器在命名空间里都有自己的假名:
Table 2.1. 标准过滤器假名和顺序
Alias Filter Class
CHANNEL_FILTER ChannelProcessingFilter
CONCURRENT_SESSION_FILTER ConcurrentSessionFilter
SESSION_CONTEXT_INTEGRATION_FILTER HttpSessionContextIntegrationFilter
LOGOUT_FILTER LogoutFilter
X509_FILTER X509PreAuthenticatedProcessigFilter
PRE_AUTH_FILTER Subclass of AstractPreAuthenticatedProcessingFilter
CAS_PROCESSING_FILTER CasProcessingFilter
AUTHENTICATION_PROCESSING_FILTER AuthenticationProcessingFilter
BASIC_PROCESSING_FILTER BasicProcessingFilter
SERVLET_API_SUPPORT_FILTER classname
REMEMBER_ME_FILTER RememberMeProcessingFilter
ANONYMOUS_FILTER AnonymousProcessingFilter
EXCEPTION_TRANSLATION_FILTER ExceptionTranslationFilter
NTLM_FILTER NtlmProcessingFilter
FILTER_SECURITY_INTERCEPTOR FilterSecurityInterceptor
SWITCH_USER_FILTER SwitchUserProcessingFilter
你可以把你自己的过滤器添加到队列中,使用custom-filter元素,使用这些名字中的一个,来指定你的过滤器应该出现的位置:
<beans:bean id="myFilter" class="com.mycompany.MySpecialAuthenticationFilter">
<custom-filter position="AUTHENTICATION_PROCESSING_FILTER"/>
</beans:bean>
你还可以使用after 或 before属性,如果你想把你的过滤器添加到队列中另一个过滤器的前面或后面。可以使用"FIRST" 或 "LAST"来指定你想让你的过滤器分别出现在队列元素的前面或后面。
2.3.6. 防止Session固定攻击
Session固定攻击是一个潜在危险,当一个恶意攻击者可以创建一个session访问一个网站的时候,然后说服另一个用户登录到同一个会话上(比如,发送给他们一个包含了session标识参数的链接)。 Spring Security通过在用户登录时,创建一个新session来防止这个问题。 如果你不需要保护,或者它与其他一些需求冲突,你可以通过使用<http>中的session-fixation-protection属性来配置它的行为,它有三个选项
*
migrateSession - 创建一个新session,把原来session中所有属性复制到新session中。这是默认值。
*
none - 什么也不做,继续使用原来的session。
*
newSession - 创建一个新的“干净的”session,不会复制session中的数据。
2.3.7. 设置自定义AuthenticationEntryPoint
如果你不使用命名空间里的表单登录,OpenID或基本身份验证,你也许想定义个验证过滤器和入口点,使用传统的bean语法,把他们链接到命名空间里。 你可以像Section 2.3.5, “添加你自己的filter”里解释的那样,添加过滤器。 对应的AuthenticationEntryPoint可以使用<http>中的entry-point-ref进行设置。
CAS例子,是一个在命名空间里使用自定义bean的好例子,包括这个语法。如果你不熟悉验证入口点,可以看看技术纵览章节中的讨论。
2.4. 保护方法
Spring Security 2.0大幅改善了对你的服务层方法添加安全。 如果你使用Java 5或更高版本,还支持JSR-250的安全注解,同框架提供的@secured注解相似。 你可以为单个bean提供安全控制,通过使用intercept-methods元素装饰bean声明,或者你可以使用AspectJ方式的切点来控制实体服务层里的多个bean。
2.4.1. <global-method-security>元素
这个元素用来在你的应用程序中启用基于安全的注解(通过在这个元素中设置响应的属性),也可以用来声明将要应用在你的实体application context中的安全切点组。 你应该只定义一个<global-method-security>元素。 下面的声明同时启用两种类型的注解:
<global-method-security secured-annotations="enabled" jsr250-annotations="enabled"/>
2.4.1.1. 使用protect-pointcut添加安全切点
protect-pointcut是非常强大的,它让你可以用简单的声明对多个bean的进行安全声明。 参考下面的例子:
<global-method-security>
<protect-pointcut expression="execution(* com.mycompany.*Service.*(..))" access="ROLE_USER"/>
</global-method-security>
这样会保护application context中的符合条件的bean的所有方法,这些bean要在com.mycompany包下,类名以"Service"结尾。 ROLE_USER的角色才能调用这些方法。 就像URL匹配一样,指定的匹配要放在切点队列的最前面,第一个匹配的表达式才会被用到。
2.5. 默认的AccessDecisionManager
这章假设你有一些Spring Security权限控制有关的架构知识。 如果没有,你可以跳过这段,以后再来看,因为这章只是为了自定义的用户设置的,需要在简单基于角色安全的基础上加一些客户化的东西。
当你使用命名空间配置时,默认的AccessDecisionManager实例会自动注册,然后用来为方法调用和web URL访问做验证,这些都是基于你设置的intercept-url和protect-pointcut权限属性内容(和注解中的内容,如果你使用注解控制方法的权限)。
默认的策略是使用一个AffirmativeBased AccessDecisionManager ,以及RoleVoter 和AuthenticatedVoter。
2.5.1. 自定义AccessDecisionManager
如果你需要使用一个更复杂的访问控制策略,把它设置给方法和web安全是很简单的。
对于方法安全,你可以设置global-security里的access-decision-manager-ref属性,用对应 AccessDecisionManager bean在application context里的id:
<global-method-security access-decision-manager-ref="myAccessDecisionManagerBean">
...
</global-method-security>
web安全安全的语法也是一样,但是放在http元素里:
<http access-decision-manager-ref="myAccessDecisionManagerBean">
...
</http>
2.5.2. 验证管理器
我们大概知道命名空间配置会自动为我们注册一个验证管理器bean。 这是一个Spring Security的ProviderManager类,如果你以前使用过框架,应该对它很熟悉了。
你也许想为ProviderManager注册另外的AuthenticationProvider bean,你可以使用<custom-authentication-provider>元素实现。比如:
<bean id="casAuthenticationProvider"
class="org.springframework.security.providers.cas.CasAuthenticationProvider">
<security:custom-authentication-provider />
...
</bean>
另一个常见的需求是,上下文中的另一个bean可能需要引用AuthenticationManager。 这里有一个特殊的元素,可以让你为AuthenticationManager注册一个别名,然后你可以application context的其他地方使用这个名字。
<security:authentication-manager alias="authenticationManager"/>
<bean id="casProcessingFilter" class="org.springframework.security.ui.cas.CasProcessingFilter">
<security:custom-filter position="CAS_PROCESSING_FILTER"/>
<property name="authenticationManager" ref="authenticationManager"/>
...
</bean>
第三章 简单的应用
这几个网络应用程序在项目中是可用的。为了避免大负荷的下载,仅仅“指南”和“连接”例子包含在发行文件中。你也可以自行生成其它例子的项目文件,或者你也可以通过MAVEN仓库获取war文件。正如入门文档所描述的那样,你可以获得源代码并很容易的利用maven编译和部署它。
3.1. 指南的例子
指南的例子是个不错的入门级应用。它将简单的命名空间进行配置贯彻始终.
关于运行环境就不多说了,JDK1.4以上版本包含1.4.
让我们深入的了解一下spring security里面的共享组组件吧
1.SecurityContextHolder,SecurityContext和Authentication对象,这里面最基本的对象是SecurityContextHolder. 这里将存储应用程序的安全细节包括当前应用程序使用的principal细节,默认的SecurityContextHolder是以本地线程进行存储细节的,这意味着在同一线程中安全上下文对方法总是可用的,即使安全上下文并不是明确的将请求分发给各个方法,使用本地线程对于principal的请求被处理时是非常安全的,当转移请求的时候。当然,spring security会自动帮你保管所以你也无需担心。有些应用程序却并不适合使用本地线程,因为它们以特殊的线程方式运行。比如一个SWING的客户端程序需要JAVA虚拟机所有线程共用一个安全上下文。在这种情况下你需要使用SecurityContextHolder.MODE_GLOBAL。其它程序也希望由相同的安全标识线程产生其它线程,可以用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL来实现上述要求。你有两种方式来改变默认的SecurityContextHolder.MODE_THREADLOCAL 模式。第一种是设置系统属性,作为选择可在SecurityContextHolder中调用一个静态方法。绝大多数的应用程序不需要改变,如果你想改变请在JAVADOC中获取更多的内容。
在SecurityContextHolder内部我们存储的当前principal细节与应用程序相结合,Spring Security使用一个Authentication对象去描述这些信息,同时你也不需要自己在去创建一个Authentication对象了,Spring Securtity为每一个用户都提供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。
if (obj instanceof UserDetails) {
String username = ((UserDetails)obj).getUsername();
} else {
String username = obj.toString();
}
5.2.2. The UserDetailsService
通过上面的代码片段你可以从Authentication对象中获得一个principal。principal也是一个对象,大多数情况你都能从UserDetails中得到它的映射。
在Spring Security中UserDetails是一个重要的接口,它描述一个principal,在可扩展的且特殊的应用方式。你可以将UserDetails想象成一个适配器在你的数据库与Spring Security内部需要的 SecurityContextHolder之间。你完全可以在UserDetails中找到你应用程序中提供的原始对象的映射,因此你可以调用诸如getEmail(), getEmployeeNumber()等等的业务逻辑方法。现在你可能感到奇怪,我在何时提供了一个UserDetails对象?我是怎么做到的?谁提供的?最简短的解释就是有一个叫UserDetailsService的接口,它仅仅只有一个方法接受String类型的username参数并返回一个UserDetails。大部分的认证模块都会委托给UserDetailsService成为认证处理的一部分。UserDetailsService被用来构建存储在SecurityContextHolder里的Authentication对象。有个好消息是我们提供了许多UserDetailsService的实现,包括用于内存处理和JDBC处理的实现。大多数用户趋于实现他们自己的实现,尽管,在DAO中仅仅是一些简单的实现来描述雇佣者,客户信息或者其它企业级应用。
5.2.3. GrantedAuthority
除了principal以外,由Authentication 提供的另外一个非常重要的方法就是getAuthorities( ),这个方法提供一个GrantedAuthority对象数组,一个GrantedAuthority是由principal准许的一个授权,通常这些授权是一些"角色",比如 ROLE_ADMINISTRATOR或者ROLE_HR_SUPERVISOR,这些角色稍后将配置于web授权、方法授权和业务逻辑对象授权。Spring Security的其它部分能够解释这些授权,并将他们显示出来。权限类通常由UserDetailsService加载!通常GrantedAuthority在大部分应用程序中是允许的。但是他们并不是单独的给特定的业务逻辑对象,因此,你不用给你的雇员业务逻辑对象单独提供角色,假如这里有数以千计的授权,你内存会很快溢出的,或者说为你一个用户授权要花费很长的一段时间。
当然Spring Security为这些公共的需求特别设计了句柄,不过你最好使用你自己项目的对象安全容器去替代掉。有时,你需要在HTTP请求间对SecurityContext进行存储,其它时间principal会对每个请求重新认证,尽管大多数情况下它是会被存储的。HttpSessionContextIntegrationFilter将负责在HTTP请求间对一个SecurityContext进行存储。根据类型的名字,HttpSession 会存储这些信息,你完全没有必要使用HttpSession 存储安全信息,通常都用SecurityContextHolder来代替。
5.2.4. Summary
Spring Security主要组件如下:
1.SecurityContextHolder, 提供各种各样的类型登录SecurityContext
2.SecurityContext,控制安全信息的请求认证
3.HttpSessionContextIntegrationFilter,在web请求间将SecurityContext存储在HttpSession中
4.Authentication,描述一个具有spring security风格的principal
5.GrantedAuthority,映射程序范围内许可的principal
6.UserDetails,从你的应用程序的DAOS中获取有用的信息来构建一个Authentication对象
7.UserDetailsService,当传进一个string类型的username时创建一个UserDetails.
5.3. Authentication
一个典型的网络应用认证处理
1.你访问一个主页,然后点击一个链接。
2.服务器收到一个请求,确定你访问了一个被保护的请求。
3.如果你没有被认证,服务器端会返回一个指示请求要求你认证。请求可以是一个HTTP请求代码,也可以是一个重定向的页面。
4.依赖认证机制,你的浏览器也会重导向到特定页面,所以你可以填写表单,或者浏览器以各种方式找回你的唯一标识。
5.浏览器回回传一个请求给服务器,可能是个POST请求包含你填写的表单内容,或者一个HTTP头包含你的认证细节。
6.服务器端将会判断发送过来的认证信息是否有效,如果有效,下一步将会启动。如果无效,你的浏览器将会再次询问(回到第三步)
7.如果你有访问权限将能够访问相应的资源,否则将会返回一个403错误。
5.3.1. ExceptionTranslationFilter
ExceptionTranslationFilter是一个spring security 过滤器,它负责检测所有spring secutrity所抛出的异常,这些异常通常由主要为认证服务的AbstractSecurityInterceptor抛出,我们将在下一部分讨论AbstractSecurityInterceptor。但是现在我们需要知道它产生JAVA错误,并不知道HTTP的内容和如何去验证一个principal,代替ExceptionTranslationFilter提供这些服务,并负责返回403的错误代码(如果principal已经被认证而且缺乏足够的登录按照上述步骤7)或者登录一个AuthenticationEntryPoint(如果principal没有被认证因此我们需要返回第三步)。
5.3.2. AuthenticationEntryPoint
AuthenticationEntryPoint 负责上述步骤的第三步,你可以想象,每个网络应用程序都将会有个默认的认证策略,每个主要的认证系统都将有它自己的AuthenticationEntryPoint实现,他们用来描述步骤3中的动作,在你的浏览器决定提交你的认证证书后,这需要服务器端需要一些类似于收集这些认证的细节的东西。现在我们已经到了第六步了。在Spring Security中有一个专用名为从用户参数收集认证细节的函数,那个名字就是authentication mechanism。从用户参数中获得相应的认证细节以后,一个认证请求对象被构建,然后将会指向一个AuthenticationProvider。
5.3.3. AuthenticationProvider
欢迎到 http://www.tutu6.com来看看