【问题标题】:Spring security 3 + JCIFS ntlmSpring 安全 3 + JCIFS ntlm
【发布时间】:2012-02-12 06:20:28
【问题描述】:

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

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

正在寻找一个示例项目。

【问题讨论】:

    标签: spring-security ntlm jcifs


    【解决方案1】:

    它们可以一起使用。本质上,您要做的是连接到 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");
        }
    }
    

    您会看到,在我们使用“协商”而不是 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

    希望这会有所帮助!

    授予

    【讨论】:

    • 谢谢。已经使用 doFilter 方法修复了 IE。但我肯定会在周末尝试你的 spring+jscifs 示例。
    猜你喜欢
    • 2010-10-09
    • 1970-01-01
    • 2013-03-03
    • 2016-07-15
    • 2011-02-10
    • 1970-01-01
    • 2015-05-30
    • 2011-07-03
    • 2011-04-20
    相关资源
    最近更新 更多