《Spring Security3》第六章第二部分翻译(自定义AuthenticationProvider)(转载)

实现自定义的 AuthenticationProvider

在很多场景下,你的应用需要跳出 Spring Security 功能的边界,可能会需要实现自己的 AuthenticationProvider 。回忆在第二章中 AuthenticationProvider 的角色,在整个认证过程中,它接受安全实体请求提供的凭证(即 Authentication 对象或 authentication token )并校验其正确性和合法性。

通过AuthenticationProvider 实现一个简单的单点登录

通常,应用允许以用户或客户端方式登录。但是,有一种场景也很常见,尤其是在广泛使用的应用中,即允许系统中的不同用户以多种方式登录。

假设我们的系统与一个简单的“单点登录”提供者进行集成,用户名和密码分别在 HTTP 头中的 j_username j_password 发送。除此以外, j_signature 头信息包含了用户名和密码的随意编码算法形成的字符串,以辅助安全请求。

【不要使用这个例子作为真实单点登录的解决方案。这个例子很牵强,只是为了说明实现一个完全自定义 AuthenticationProvider 的步骤。真正的 SSO 解决方案显然会更安全并涉及到几次的握手以建立可信任的凭证。 Spring Security 支持几种 SSO 解决方案,包括中心认证服务( CAS )和 SiteMinder ,我们将会在第十章:使用中心认证服务实现单点登录中介绍。实际上, Spring Security 提供了一个类似的过滤器用来进行 SiteMinder 请求头的认证,即 o.s.s.web.authentication.preauth.RequestHeaderAuthenticationFilter ,也是这种类型功能的一个好例子。】

对于 admin 用户的登录,我们的算法期望在请求头中包含如下的数据:

请求头

j_username

admin

j_password

admin

j_signature

admin|+|admin

一般来讲, AuthenticationProvider 将会寻找特定的 AuthenticationToken ,而后者会在过滤器链中位置比较靠前的 servlet filter 里面进行填充赋值(明确会在 AuthenticationManager 访问检查执行之前),如下图描述:


 

在这种方式中, AuthenticationToken 的提供者与其消费者 AuthenticationProvider 有一点脱离关系了。所以,在实现自定义 AuthenticationProvider 时,通常还需要实现一个自定义的 servlet 过滤器,其作用是提供特定的 AuthenticationToken

自定义认证 token

我们实现自定义的方法会尽可能的使用 Spring Security 的基本功能。基于此,我们会扩展并增强基本类如 UsernamePasswordAuthenticationToken ,并添加一个新的域来存储我们已编码的签名字符串。最终的类 com.packtpub.springsecurity.security.SignedUsernamePasswordAuthenticationToken ,如下:

 

Java代码   收藏代码
  1. package  com.packtpub.springsecurity.security;  
  2. // imports omitted   
  3.   public   class  SignedUsernamePasswordAuthenticationToken   
  4. extends  UsernamePasswordAuthenticationToken {  
  5.     private  String requestSignature;  
  6.     private   static   final   long  serialVersionUID =   
  7.     3145548673810647886L;  
  8.   /**  
  9.    * Construct a new token instance with the given principal,   
  10. credentials, and signature.  
  11.   *   
  12.   * @param principal the principal to use  
  13.   * @param credentials the credentials to use  
  14.   * @param signature the signature to use  
  15.   */   
  16.   public  SignedUsernamePasswordAuthenticationToken(String principal,  
  17.     String credentials, String signature) {  
  18.     super (principal, credentials);  
  19.     this .requestSignature = signature;  
  20.     }  
  21.   public   void  setRequestSignature(String requestSignature) {  
  22.     this .requestSignature = requestSignature;  
  23.     }  
  24.   public  String getRequestSignature() {  
  25.     return  requestSignature;  
  26.     }  
  27. }   

          我们可以看到 SignedUsernamePasswordAuthenticationToken 是一个简单的 POJO 类,扩展自 UsernamePasswordAuthenticationTokenTokens 并不需要太复杂——它们的主要目的就是为后面的校验封装凭证信息。

实现对请求头处理的 servlet 过滤器

         现在,我们要写 servlet 过滤器的代码,它负责将请求头转换成我们新定义的 token 。同样的,我们扩展对应的 Spring Security 基本类。在本例中, o.s.s.web.authentication.

AbstractAuthenticationProcessingFilter 满足我们的要求。

【基本过滤器 AbstractAuthenticationProcessingFilterSpring Security 中是很多进行认证过滤器的父类(包括 OpenID 、中心认证服务以及基于 form 的用户名和密码登录)。这个类提供了标准的认证逻辑并适当织入了其它重要的资源如 RememberMeServicesApplicationEventPublisher (本章的后面将会讲解到)。】

          现在,让我们看一下代码:

 

Java代码   收藏代码
  1. // imports omitted   
  2.   public   class  RequestHeaderProcessingFilter  extends    
  3.     AbstractAuthenticationProcessingFilter {  
  4.   private  String usernameHeader =  "j_username" ;  
  5.   private  String passwordHeader =  "j_password" ;  
  6.   private  String signatureHeader =  "j_signature" ;  
  7.   protected  RequestHeaderProcessingFilter() {  
  8.     super ( "/j_spring_security_filter" );  
  9.     }  
  10.   @Override   
  11.   public  Authentication attemptAuthentication   
  12.     (HttpServletRequest request,HttpServletResponse response)   
  13.     throws  AuthenticationException,  
  14.       IOException, ServletException {  
  15.     String username = request.getHeader(usernameHeader);  
  16.     String password = request.getHeader(passwordHeader);  
  17.     String signature = request.getHeader(signatureHeader);  
  18.     SignedUsernamePasswordAuthenticationToken authRequest =   
  19.       new  SignedUsernamePasswordAuthenticationToken   
  20.       (username, password, signature);  
  21.     return   this .getAuthenticationManager().authenticate(authRequest);  
  22.     }  
  23.   // getters and setters omitted below   
  24. }   

           可以看到,这个比较简单的过滤器查找三个已命名的请求头,正如我们已经规划的那样(如果需要的话,可以通过 bean 属性进行配置),并监听默认的 URL   /j_spring_security_filter 。正如其它的 Spring Security 过滤器那样,这是一个虚拟的 URL 并被我们的过滤器的基类 AbstractAuthenticationProcessingFilter 所识别,基于此这个过滤器采取行动尝试创建 Authentication token 并认证用户的请求。

【区分参与认证 token 流程的各个组件。在这个功能中,很容易被这些术语、接口和类的名字搞晕。代表要认证的 token 接口是 o.s.s.core.Authentication ,这个接口的实现将以后缀 AuthenticationToken 结尾。这是一个很简单的方式来区分 Spring Security 提供的认证实现类。】

在本例中,我们尽可能将错误检查最小化(译者注:即参数的合法性与完整性的检查)。可能在实际的应用中,会校验是否所有头信息都提供了以及在发现用户提供信息不正确的时候要抛出异常或对用户进行重定向。

一个细小的配置变化是需要将我们的过滤器插入到过滤器链中:

 

Xml代码   收藏代码
  1. < http   auto-config = "true"  ... >   
  2.   < custom-filter   ref = "requestHeaderFilter"    
  3.   before = "FORM_LOGIN_FILTER" />   
  4. </ http >   

 你可以看到过滤器代码最后请求 AuthenticationManager 来进行认证。这将最终委托配置的 AuthenticationProvider ,它们中的一个要支持检查 SignedUsernamePasswordAuthenticationToken 。接下来,我们需要书写一个 AuthenticationProvider 来做这件事情。

实现基于请求头的 AuthenticationProvider

现在,我们写一个 AuthenticationProvider 的实现类,即 com.packtpub.springsecurity.security.SignedUsernamePasswordAuthenticationProvider ,负责校验我们自定义 Authentication token 的签名。

 

Java代码   收藏代码
  1. package  com.packtpub.springsecurity.security;  
  2. // imports omitted   
  3.   public   class  SignedUsernamePasswordAuthenticationProvider   
  4.   extends  DaoAuthenticationProvider {  
  5.     @Override   
  6.     public   boolean  supports(Class<?  extends  Object> authentication) {  
  7.       return  (SignedUsernamePasswordAuthenticationToken . class .isAssignableFrom(authentication));  
  8.     }  
  9.   @Override   
  10.   protected   void  additionalAuthenticationChecks   
  11.   (UserDetails userDetails,  
  12.     UsernamePasswordAuthenticationToken authentication)  
  13.     throws  AuthenticationException {  
  14.     super .additionalAuthenticationChecks   
  15.     (userDetails, authentication);  
  16.     SignedUsernamePasswordAuthenticationToken signedToken =   
  17.       (SignedUsernamePasswordAuthenticationToken) authentication;  
  18.     if (signedToken.getRequestSignature() ==  null ) {  
  19.       throw   new  BadCredentialsException(messages.getMessage(  
  20.       "SignedUsernamePasswordAuthenticationProvider  
  21.       .missingSignature", " Missing request signature"),  
  22.       isIncludeDetailsObject() ? userDetails : null );  
  23.     }  
  24. // calculate expected signature    
  25.   if (!signedToken.getRequestSignature()   
  26.   .equals(calculateExpectedSignature(signedToken))) {  
  27.   throw   new  BadCredentialsException(messages.getMessage   
  28.   ("SignedUsernamePasswordAuthenticationProvider   
  29.   .badSignature", " Invalid request signature"),  
  30.   isIncludeDetailsObject() ? userDetails : null );  
  31.   }  
  32. }  
  33. private  String calculateExpectedSignature   
  34.   (SignedUsernamePasswordAuthenticationToken signedToken) {  
  35.     return  signedToken.getPrincipal() +  "|+|"  +   
  36.     signedToken.getCredentials();  
  37.   }  
  38. }  

 你可以看到我们再次扩展了框架中的类 DaoAuthenticationProvider ,因为有用的数据访问代码仍然需要进行实际的用户密码校验以及通过 UserDetailsService 加载 UserDetails

         这个类有些复杂,所以我们将分别介绍其中的每个方法。

         Supports 方法,是重写父类的方法,向 AuthenticationManager 指明当前 AuthenticationProvider 要进行校验的期望运行时 Authentication token

         接下来, additionalAuthenticationChecks 方法被父类调用,此方法允许子类对 token 进行特有的校验。这正适合我们的策略,所以添加上我们对 token 新的签名检查。基本上已经完成了我们自定义“简单 SSO ”的实现,仅剩一处配置了。

连接AuthenticationProvider

         一个常见的要求就是将一个或更多的 AuthenticationProvider 接口连接起来,因为用户可能会以几种校验方式中的某一种登录系统。

因为到目前为止,我们还没有了解其它的 AuthenticationProvider ,我们可以假设以下的需求,即使用标准的用户名和密码基于 form 的认证以及前面实现的自定义简单 SSO 认证。当配置了多个 AuthenticationProvider 时,每个 AuthenticationProvider 都会检查过滤器提供给它的 AuthenticationToken ,仅当这个 token 类型它支持时才会处理这个 token 。以这种方式,你的应用同时支持不同的认证方式并不会有什么坏处。

连接多个 AuthenticationProvider 实际上很简单。只需要在我们的 dogstore-security.xml 配置文件中声明另一个 authentication-provider 引用。

 

Xml代码   收藏代码
  1. < authentication-manager   alias = "authenticationManager" >   
  2.   < authentication-provider   ref"signedRequestAuthenticationProvider" />   
  3.   < authentication-provider   user-service-ref = "jdbcUserService" >   
  4.     < password-encoder   ref = "passwordEncoder"   >   
  5.       < salt-source   ref = "saltSource" />   
  6.     </ password-encoder >   
  7.   </ authentication-provider >   
  8. </ authentication-manager >   

          与我们安全配置文件中引用的其它 Spring bean 一样, signedRequestAuthenticationProvider 引用就是我们的 AuthenticationProvider ,它将在 dogstore-base.xml 中与其它的 Spring bean 一起进行配置。

 

Xml代码   收藏代码
  1. < bean   id = "signedRequestAuthenticationProvider"    class ="com.packtpub.springsecurity.security   
  2. .SignedUsernamePasswordAuthenticationProvider">   
  3.   < property   name = "passwordEncoder"   ref = "passwordEncoder" />   
  4.   < property   name = "saltSource"   ref = "saltSource" />   
  5.   < property   name = "userDetailsService"   ref = "jdbcUserService" />   
  6. </ bean >   

 我们自定义的 AuthenticationProviderbean 属性其实都是父类所需要的。这些也都指向了我们在 AuthenticationManager 的中第二个 authentication-provider 声明中的那些 bean

         最终已经完成了支持这个简单单点登录功能的编码和配置,至此可以给自己一个小小的喝彩。但是,还有一个小问题——我们应该怎样操作请求的 http 头以模拟我们的 SSO 认证提供者呢?

使用请求头模拟单点登录

         尽管我们的场景比较牵强,但是有一些商业和开源的单点登录解决方案,它们能够被配置以通过 HTTP 请求头发送凭证信息,最具有代表性的是 CA (以前的 NetegritySiteMinder

【需要特别注意的是,与 SSO 方案集成的应用是不能通过用户的直接请求访问的。通常情况下, SSO provider 功能作为代理,通过它确定用户的请求流程(是安全的)或 provider 持有关于密码的信息并将这些信息与单个的安全应用隔离。在没有完全了解一个其使用的硬件、网络和安全设施之前,不要部署 SSO 应用。】

         Mozilla Firefox 的浏览器扩展,名为 Modify Headers (可以在以下地址获得: http://modifyheaders.mozdev.org ),是一个很简单的工具能够用来模拟伪造 HTTP 头的请求。以下的截图表明了如何使用这个工具添加我们的 SSO 方案所希望得到的请求头信息:



 将所有的头信息标示为 Enabled ,访问这个 URLhttp://localhost:8080/JBCPPets/j_spring_security_filter ,会发现我们能够自动登录系统。你可能也会发现我们给予 form 的登录还能继续可用,这是因为保留了这两个 AuthenticationProvider 实现以及过滤器链中对应的过滤器。

(译者注:说实话,作者这个实现自定义 AuthenticationProvider 的例子真的是比较牵强,但是还算完整描述出了其实现方式,想深入了解 AuthenticationProvider 自定义的朋友,可以参照 Spring Security 提供的 CasAuthenticationProvider 等实现。)

实现自定义AuthenticationProviders 时要考虑的事项

         尽管我们刚刚看到的例子并没有阐述你想构建的 AuthenticationProvider ,但是任何自定义 AuthenticationProvider 的步骤是类似的。这个练习的关键在于:

基于用户的请求完成一个 Authentication 实现的任务一般情况下会在过滤器链中的某一个中进行。取决于是否校验凭证的数据,这个校验组件可能要进行扩展;

基于一个合法的 Authentication 认证用户的任务需要 AuthenticationProvider 的实现来完成。请查看我们在第二章中讨论过的 AuthenticationProvider 所被期望拥有的功能;

在一些特殊的场景下,如果未认证的 session 被发现,可能会需要自定义的 AuthenticationEntryPoint 。我们将会在本章接下来的部分更多了解这个接口,也会在第十章介绍中心认证服务( CAS )时,介绍一些 AuthenticationEntryPoint 的实际例子。

如果你能时刻记住它们的角色,当你在开发应用特定的 AuthenticationProvider 时,会在实现和调试过程中少很多的迷惑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值