spring-security ntlm jcifs

Spring security 3+JCIFS ntlm



spring-security (1)

¿Pueden trabajar juntos? Algunos proyectos de muestra serían geniales.

Tengo una aplicación web en Spring3. Y necesito implementar NTLM. Spring detuvo el soporte de NTLM en la 3ra versión. ¿Hay alguna posibilidad de implementarlo?

Buscando un proyecto de muestra.


Se pueden usar juntos. Esencialmente, lo que quiere hacer es conectar el protocolo SPNEGO y detectar cuándo recibe un paquete NTLM del cliente. Una buena descripción del protocolo se puede encontrar aquí:

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

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

Otro gran recurso para NTLM es este:

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

Pero usted pidió una muestra así que aquí va. Para detectar un paquete NTLM, necesita decodificar en base64 el paquete e inspeccionar para encontrar una cadena de inicio:

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

Tendrá que crear un proveedor de autenticación que pueda manejar ese tipo de autenticaciónRequest:

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

Y finalmente debes unir todo en tu archivo 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>

Por último, debes saber cómo unirlo todo. El protocolo tal como se describe en el primer conjunto de enlaces muestra que hay un par de viajes redondos que debe realizar entre el cliente y el servidor. Por lo tanto, en su filtro necesita un poco más de lógica:

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

Verá que en la excepción usamos "Negociar" en lugar de 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); } }

El filtro de resorte (arriba) fue modelado en gran medida en jcifs.http.NtlmHttpFilter que puedes encontrar en la fuente de jcifs aquí:

http://jcifs.samba.org/

Este no es un proyecto descargable completo como lo solicitó, pero si hay interés de la comunidad, podría agregar este código NTLM a mi proyecto github:

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

¡Espero que esto ayude!

Conceder