2012-01-17 188 views
4

他们可以一起工作吗? 一些项目样本会很好。春季安全3 + JCIFS ntlm

我在Spring3上有一个Web应用程序。我需要实施NTLM。 Spring在第三版中停止了NTLM支持。有没有可能实现它?

寻找一个示例项目。

回答

5

它们可以一起使用。基本上你想要做的就是挂接到SPNEGO协议,并检测你何时从客户端收到NTLM数据包。该协议的一个很好的说明可以在这里找到:

http://www.innovation.ch/personal/ronald/ntlm.html

http://blogs.technet.com/b/tristank/archive/2006/08/02/negotiate-this.aspx

为NTLM另一个伟大的资源是:

http://davenport.sourceforge.net/ntlm.html

但是,你问一个样本所以在这里去。为了检测NTLM包,你需要为Base64 解码包,并检查是否起始字符串:

public void doFilter(ServletRequest req, ServletResponse res, 
        FilterChain chain) throws IOException, ServletException { 
    HttpServletRequest request = (HttpServletRequest) req; 
    HttpServletResponse response = (HttpServletResponse) res; 

    String header = request.getHeader("Authorization"); 

    if ((header != null) && header.startsWith("Negotiate ")) { 
     if (logger.isDebugEnabled()) { 
      logger.debug("Received Negotiate Header for request " + request.getRequestURL() + ": " + header); 
     } 
     byte[] base64Token = header.substring(10).getBytes("UTF-8"); 
     byte[] decodedToken = Base64.decode(base64Token); 

    if (isNTLMMessage(decodedToken)) { 
     authenticationRequest = new NTLMServiceRequestToken(decodedToken); 
    } 

... 
} 

public static boolean isNTLMMessage(byte[] token) { 
    for (int i = 0; i < 8; i++) { 
     if (token[i] != NTLMSSP_SIGNATURE[i]) { 
      return false; 
     } 
    } 
    return true; 
} 

public static final byte[] NTLMSSP_SIGNATURE = new byte[]{ 
     (byte) 'N', (byte) 'T', (byte) 'L', (byte) 'M', 
     (byte) 'S', (byte) 'S', (byte) 'P', (byte) 0 
}; 

你需要做的是能够处理这种类型的鉴权请求的身份认证提供:

import jcifs.Config; 
import jcifs.UniAddress; 
import jcifs.ntlmssp.NtlmMessage; 
import jcifs.ntlmssp.Type1Message; 
import jcifs.ntlmssp.Type2Message; 
import jcifs.ntlmssp.Type3Message; 
import jcifs.smb.NtlmPasswordAuthentication; 
import jcifs.smb.SmbSession; 
import jcifs.util.Base64; 
import org.springframework.beans.factory.InitializingBean; 
import org.springframework.beans.factory.annotation.Autowired; 
import org.springframework.security.authentication.AccountStatusUserDetailsChecker; 
import org.springframework.security.authentication.AuthenticationProvider; 
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 
import org.springframework.security.core.Authentication; 
import org.springframework.security.core.AuthenticationException; 
import org.springframework.security.core.userdetails.UserDetailsChecker; 

import javax.annotation.PostConstruct; 
import java.io.IOException; 

/** 
* User: gcermak 
* Date: 3/15/11 
* <p/> 
*/ 
public class ActiveDirectoryNTLMAuthenticationProvider implements AuthenticationProvider, InitializingBean { 
    protected String defaultDomain; 
    protected String domainController; 

    protected UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker(); 

    public ActiveDirectoryNTLMAuthenticationProvider(){ 
     Config.setProperty("jcifs.smb.client.soTimeout", "1800000"); 
     Config.setProperty("jcifs.netbios.cachePolicy", "1200"); 
     Config.setProperty("jcifs.smb.lmCompatibility", "0"); 
     Config.setProperty("jcifs.smb.client.useExtendedSecurity", "false"); 
    } 

    @Override 
    public Authentication authenticate(Authentication authentication) throws AuthenticationException { 
     NTLMServiceRequestToken auth = (NTLMServiceRequestToken) authentication; 
     byte[] token = auth.getToken(); 

     String name = null; 
     String password = null; 

     NtlmMessage message = constructNTLMMessage(token); 

     if (message instanceof Type1Message) { 
      Type2Message type2msg = null; 
      try { 
       type2msg = new Type2Message(new Type1Message(token), getChallenge(), null); 
       throw new NtlmType2MessageException(Base64.encode(type2msg.toByteArray())); 
      } catch (IOException e) { 
       throw new NtlmAuthenticationFailure(e.getMessage()); 
      } 
     } 
     if (message instanceof Type3Message) { 
      final Type3Message type3msg; 
      try { 
       type3msg = new Type3Message(token); 
      } catch (IOException e) { 
       throw new NtlmAuthenticationFailure(e.getMessage()); 
      } 
      final byte[] lmResponse = (type3msg.getLMResponse() != null) ? type3msg.getLMResponse() : new byte[0]; 
      final byte[] ntResponse = (type3msg.getNTResponse() != null) ? type3msg.getNTResponse() : new byte[0]; 

      NtlmPasswordAuthentication ntlmPasswordAuthentication = new NtlmPasswordAuthentication(type3msg.getDomain(), type3msg.getUser(), getChallenge(), lmResponse, ntResponse); 

      String username = ntlmPasswordAuthentication.getUsername(); 
      String domain = ntlmPasswordAuthentication.getDomain(); 
      String workstation = type3msg.getWorkstation(); 

      name = ntlmPasswordAuthentication.getName(); 
      password = ntlmPasswordAuthentication.getPassword(); 
     } 

     // do custom logic here to find the user ... 
     userDetailsChecker.check(user); 

     return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities()); 
    } 

    // The Client will only ever send a Type1 or Type3 message ... try 'em both 
    protected static NtlmMessage constructNTLMMessage(byte[] token) { 
     NtlmMessage message = null; 
     try { 
      message = new Type1Message(token); 
      return message; 
     } catch (IOException e) { 
      if ("Not an NTLMSSP message.".equals(e.getMessage())) { 
       return null; 
      } 
     } 

     try { 
      message = new Type3Message(token); 
      return message; 
     } catch (IOException e) { 
      if ("Not an NTLMSSP message.".equals(e.getMessage())) { 
       return null; 
      } 
     } 

     return message; 
    } 

    protected byte[] getChallenge() { 
     UniAddress dcAddress = null; 
     try { 
      dcAddress = UniAddress.getByName(domainController, true); 
      return SmbSession.getChallenge(dcAddress); 
     } catch (IOException e) { 
      throw new NtlmAuthenticationFailure(e.getMessage()); 
     } 
    } 

    @Override 
    public boolean supports(Class<? extends Object> auth) { 
     return NTLMServiceRequestToken.class.isAssignableFrom(auth); 
    } 

    @Override 
    public void afterPropertiesSet() throws Exception { 
     // do nothing 
    } 

    public void setSmbClientUsername(String smbClientUsername) { 
     Config.setProperty("jcifs.smb.client.username", smbClientUsername); 
    } 

    public void setSmbClientPassword(String smbClientPassword) { 
     Config.setProperty("jcifs.smb.client.password", smbClientPassword); 
    } 

    public void setDefaultDomain(String defaultDomain) { 
     this.defaultDomain = defaultDomain; 
     Config.setProperty("jcifs.smb.client.domain", defaultDomain); 
    } 

    /** 
    * 0: Nothing 
    * 1: Critical [default] 
    * 2: Basic info. (Can be logged under load) 
    * 3: Detailed info. (Highest recommended level for production use) 
    * 4: Individual smb messages 
    * 6: Hex dumps 
    * @param logLevel the desired logging level 
    */ 
    public void setDebugLevel(int logLevel) throws Exception { 
     switch(logLevel) { 
      case 0: 
      case 1: 
      case 2: 
      case 3: 
      case 4: 
      case 6: 
       Config.setProperty("jcifs.util.loglevel", Integer.toString(logLevel)); 
       break; 
      default: 
       throw new Exception("Invalid Log Level specified"); 
     } 
    } 

    /** 
    * 
    * @param winsList a comma separates list of wins addresses (ex. 10.169.10.77,10.169.10.66) 
    */ 
    public void setNetBiosWins(String winsList) { 
     Config.setProperty("jcifs.netbios.wins", winsList); 
    } 

    public void setDomainController(String domainController) { 
     this.domainController = domainController; 
    } 
} 

最后,你需要在你的spring_security.xml文件绑在一起,这一切:

<beans:beans xmlns="http://www.springframework.org/schema/security" 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
      xmlns:beans="http://www.springframework.org/schema/beans" 
      xmlns:jdbc="http://www.springframework.org/schema/jdbc" 
      xsi:schemaLocation=" 
       http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd 
       http://www.springframework.org/schema/jdbc 
       http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd 
       http://www.springframework.org/schema/security 
       http://www.springframework.org/schema/security/spring-security-3.1.xsd"> 

    <http auto-config="true" use-expressions="true" disable-url-rewriting="true"> 
     <form-login login-page="/auth/login" 
        login-processing-url="/auth/j_security_check"/> 
     <remember-me services-ref="rememberMeServices"/> 
     <logout invalidate-session="true" logout-success-url="/auth/logoutMessage" logout-url="/auth/logout"/> 
     <access-denied-handler error-page="/error/accessDenied"/> 
    </http> 

    <authentication-manager alias="authenticationManager"> 
     <authentication-provider user-service-ref="myUsernamePasswordUserDetailsService"> 
      <password-encoder ref="passwordEncoder"> 
       <salt-source ref="saltSource"/> 
      </password-encoder> 
     </authentication-provider> 
     <authentication-provider ref="NTLMAuthenticationProvider"/> 
    </authentication-manager> 
</beans:beans> 

最后,你需要知道如何配合这一切磕磕碰碰呃。第一组链接中描述的协议显示,您需要在客户端和服务器之间进行几次往返。因此,在你的过滤器,你需要多一点的逻辑:

import jcifs.ntlmssp.Type1Message; 
import jcifs.ntlmssp.Type2Message; 
import org.springframework.security.authentication.AuthenticationManager; 
import org.springframework.security.core.Authentication; 
import org.springframework.security.core.AuthenticationException; 
import org.springframework.security.core.codec.Base64; 
import org.springframework.security.core.context.SecurityContextHolder; 
import org.springframework.security.extensions.kerberos.KerberosServiceRequestToken; 
import org.springframework.security.web.authentication.AuthenticationFailureHandler; 
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 
import org.springframework.util.Assert; 
import org.springframework.web.filter.GenericFilterBean; 

import javax.servlet.FilterChain; 
import javax.servlet.ServletException; 
import javax.servlet.ServletRequest; 
import javax.servlet.ServletResponse; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 
import java.io.IOException; 

/** 
* User: gcermak 
* Date: 12/5/11 
*/ 
public class SpnegoAuthenticationProcessingFilter extends GenericFilterBean { 
    private AuthenticationManager authenticationManager; 
    private AuthenticationSuccessHandler successHandler; 
    private AuthenticationFailureHandler failureHandler; 

    public void doFilter(ServletRequest req, ServletResponse res, 
         FilterChain chain) throws IOException, ServletException { 
     HttpServletRequest request = (HttpServletRequest) req; 
     HttpServletResponse response = (HttpServletResponse) res; 

     String header = request.getHeader("Authorization"); 

     if ((header != null) && header.startsWith("Negotiate ")) { 
      if (logger.isDebugEnabled()) { 
       logger.debug("Received Negotiate Header for request " + request.getRequestURL() + ": " + header); 
      } 
      byte[] base64Token = header.substring(10).getBytes("UTF-8"); 
      byte[] decodedToken = Base64.decode(base64Token); 

      // older versions of ie will sometimes do this 
      // logic cribbed from jcifs filter implementation jcifs.http.NtlmHttpFilter 
      if (request.getMethod().equalsIgnoreCase("POST")) { 
       if (decodedToken[8] == 1) { 
        logger.debug("NTLM Authorization header contains type-1 message. Sending fake response just to pass this stage..."); 
        Type1Message type1 = new Type1Message(decodedToken); 
        // respond with a type 2 message, where the challenge is null since we don't 
        // care about the server response (type-3 message) since we're already authenticated 
        // (This is just a by-pass - see method javadoc) 
        Type2Message type2 = new Type2Message(type1, new byte[8], null); 
        String msg = jcifs.util.Base64.encode(type2.toByteArray()); 
        response.setHeader("WWW-Authenticate", "Negotiate " + msg); 
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 
        response.setContentLength(0); 
        response.flushBuffer(); 
        return; 
       } 
      } 

      Authentication authenticationRequest = null; 
      if (isNTLMMessage(decodedToken)) { 
       authenticationRequest = new NTLMServiceRequestToken(decodedToken); 
      } 

      Authentication authentication; 
      try { 
       authentication = authenticationManager.authenticate(authenticationRequest); 
      } catch (NtlmBaseException e) { 
       // this happens during the normal course of action of an NTLM authentication 
       // a type 2 message is the proper response to a type 1 message from the client 
       // see: http://www.innovation.ch/personal/ronald/ntlm.html 
       response.setHeader("WWW-Authenticate", e.getMessage()); 
       response.setHeader("Connection", "Keep-Alive"); 
       response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 
       response.setContentLength(0); 
       response.flushBuffer(); 
       return; 
      } catch (AuthenticationException e) { 
       // That shouldn't happen, as it is most likely a wrong configuration on the server side 
       logger.warn("Negotiate Header was invalid: " + header, e); 
       SecurityContextHolder.clearContext(); 
       if (failureHandler != null) { 
        failureHandler.onAuthenticationFailure(request, response, e); 
       } else { 
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 
        response.flushBuffer(); 
       } 
       return; 
      } 
      if (successHandler != null) { 
       successHandler.onAuthenticationSuccess(request, response, authentication); 
      } 
      SecurityContextHolder.getContext().setAuthentication(authentication); 
     } 

     chain.doFilter(request, response); 
    } 

    public void setAuthenticationManager(AuthenticationManager authenticationManager) { 
     this.authenticationManager = authenticationManager; 
    } 

    public void setSuccessHandler(AuthenticationSuccessHandler successHandler) { 
     this.successHandler = successHandler; 
    } 

    public void setFailureHandler(AuthenticationFailureHandler failureHandler) { 
     this.failureHandler = failureHandler; 
    } 

    @Override 
    public void afterPropertiesSet() throws ServletException { 
     super.afterPropertiesSet(); 
     Assert.notNull(this.authenticationManager, "authenticationManager must be specified"); 
    } 
} 

你会看到,在我们使用“谈判”,而不是NTLM例外:

/** 
* User: gcermak 
* Date: 12/5/11 
*/ 
public class NtlmType2MessageException extends NtlmBaseException { 
    private static final long serialVersionUID = 1L; 

    public NtlmType2MessageException(final String type2Msg) { 
     super("Negotiate " + type2Msg); 
    } 
} 

弹簧过滤器(上图)在很大程度上图案上jcifs.http.NtlmHttpFilter,你可以在源JCIFS在这里找到:

http://jcifs.samba.org/

这不是一个整体,因为你requeste下载项目d但如果有来自社会各界的关心,我可以添加此NTLM代码到我的github上的项目:

http://git.springsource.org/~grantcermak/spring-security/activedirectory-se-security

希望这有助于!

格兰特

+0

我可以担保此实施方案有效。做得好。 – aweigold 2012-01-17 16:09:29

+0

谢谢。已经使用doFilter方法修复了IE。但我会在周末试试你的spring + jscifs样本。 – StrekoZ 2012-01-19 09:00:01