Spring security 3 + JCIFS ntlm

时间:2012-01-17 12:39:05

标签: spring-security ntlm jcifs

他们可以一起工作吗? 一些项目样本会很棒。

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

寻找示例项目。

1 个答案:

答案 0 :(得分: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
};

您需要创建一个可以处理该类型的authenticationRequest的身份验证提供程序:

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");
    }
}

你会在异常中看到我们使用“Negotiate”而不是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/

这不是您所要求的完整可下载项目,但如果社区有兴趣,我可以将此NTLM代码添加到我的github项目中:

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

希望这有帮助!

捐赠