Spring 安全 3 + JCIFS ntlm
Posted
技术标签:
【中文标题】Spring 安全 3 + JCIFS ntlm【英文标题】:Spring security 3 + JCIFS ntlm 【发布时间】:2012-02-12 06:20:28 【问题描述】:他们可以一起工作吗? 一些项目样本会很棒。
我在 Spring3 上有一个网络应用程序。我需要实施 NTLM。 Spring 在第 3 版中停止了 NTLM 支持。有没有可能实现它?
正在寻找一个示例项目。
【问题讨论】:
【参考方案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 示例。以上是关于Spring 安全 3 + JCIFS ntlm的主要内容,如果未能解决你的问题,请参考以下文章
KSoap-Android\JCIFS 发送空的 HTTP 帖子
Spring Security Granted Authorities 总是返回空