在网络环境中,NTLM 用作身份验证协议以处理两台计算机(其中至少有一台计算机运行 Windows NT 4.0 或更早版本)之间的事务。具有此配置的网络称为“混合模式”,这是 Windows Server 2003 家族中的默认设置。
例如,以下配置将使用 NTLM 作为身份验证机制:
另外,NTLM 是为没有加入到域中的计算机(如独立服务器和工作组)提供的身份验证协议。
概述质询/应答机制
用户请求访问。用户尝试通过提供用户凭据登录到客户端。登录前,客户端计算机缓存密码的哈希值并放弃密码。客户端向服务器发送一个请求,该请求包括用户名以及纯文本格式的请求。
服务器发送质询消息。服务器生成一个称为质询的 16 字节随机数(即 NONCE),并将它发送到客户端。
客户端发送应答消息。客户端使用由用户的密码生成的一个密码哈希值来加密服务器发送的质询。它以应答的形式将这个加密的质询发回到服务器。
服务器将质询和应答发送到域控制器。服务器将用户名、原始质询以及应答从客户端计算机发送到域控制器。
域控制器比较质询和应答以对用户进行身份验证。域控制器获取该用户的密码哈希值,然后使用该哈希值对原始质询进行加密。接下来,域控制器将加密的质询与客户端计算机的应答进行比较。如果匹配,域控制器则发送该用户已经过身份验证的服务器确认。
服务器向客户端发送应答。假定凭据有效,服务器授予对所请求的服务或资源的客户端访问权。
具体实现
web.xml配置
<filter> <filter-name>NTLM HTTP Authentication Filter</filter-name> <filter-class>com.security.filter.sso.ntlm.NtlmAuthFilter</filter-class> <init-param> <param-name>jcifs.smb.client.domain</param-name> <param-value>DOMAIN</param-value> </init-param> <init-param> <param-name>jcifs.http.domainController</param-name> <param-value>192.168.1.11</param-value> </init-param> <init-param> <param-name>jcifs.http.loadBalance</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>jcifs.http.enableBasic</param-name> <param-value>false</param-value> </init-param> <init-param> <param-name>jcifs.http.insecureBasic</param-name> <param-value>false</param-value> </init-param> <init-param> <param-name>jcifs.http.basicRealm</param-name> <param-value>jCIFS</param-value> </init-param> <init-param> <param-name>jcifs.smb.client.ssnLimit</param-name> <param-value>1000</param-value> </init-param> <init-param> <param-name>jcifs.util.loglevel</param-name> <param-value>0</param-value> </init-param> <init-param> <param-name>portalError</param-name> <param-value>/error.jsp</param-value> </init-param> </filter> <filter-mapping> <filter-name>NTLM HTTP Authentication Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
NtlmAuthFilter.java
package com.security.filter.sso.ntlm; import java.io.IOException; import java.util.Enumeration; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import jcifs.Config; import jcifs.UniAddress; import jcifs.http.NtlmSsp; import jcifs.smb.NtlmChallenge; import jcifs.smb.NtlmPasswordAuthentication; import jcifs.smb.SmbAuthException; import jcifs.smb.SmbSession; import jcifs.util.Base64; import jcifs.util.LogStream; import org.apache.log4j.Logger; public class NtlmAuthFilter implements Filter { private static LogStream log = LogStream.getInstance(); private final Logger logger = Logger.getLogger(NtlmAuthFilter.class); protected FilterConfig filterConfig; private String defaultDomain; private String domainController; private boolean loadBalance; private boolean enableBasic; private boolean insecureBasic; private String realm; private static final String BACK_SLASH = "\\"; private String portalError; public void init(FilterConfig config) throws ServletException { logger.info("NTLM HTTP Authentication Filter Init"); this.filterConfig = config; Config.setProperty("jcifs.smb.client.soTimeout", "300000"); // 1800000 Config.setProperty("jcifs.netbios.cachePolicy", "1200"); Config.setProperty("jcifs.smb.lmCompatibility", "0"); Config.setProperty("jcifs.smb.client.useExtendedSecurity", "false"); Enumeration e = filterConfig.getInitParameterNames(); while (e.hasMoreElements()) { String name = (String)e.nextElement(); if (name.startsWith("jcifs.")); Config.setProperty(name, filterConfig.getInitParameter(name)); } this.domainController = Config.getProperty("jcifs.smb.client.domain"); this.defaultDomain = Config.getProperty("jcifs.http.domainController"); this.loadBalance = Boolean.valueOf(Config.getProperty("jcifs.http.loadBalance")).booleanValue(); this.enableBasic = Boolean.valueOf(Config.getProperty("jcifs.http.enableBasic")).booleanValue(); this.insecureBasic = Boolean.valueOf(Config.getProperty("jcifs.http.insecureBasic")).booleanValue(); this.realm = Config.getProperty("jcifs.http.basicRealm"); this.portalError = config.getInitParameter("portalError"); int level; if ((level = Config.getInt("jcifs.util.loglevel", -1)) != -1) { LogStream.setLevel(level); } if (LogStream.level <= 2) return; try { Config.store(log, "JCIFS PROPERTIES"); } catch (IOException ioe) { } } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws SmbAuthException, IOException, ServletException { logger.info("NTLM HTTP Authentication Filter..........."); HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; try { if (domainController == null || "".equals(domainController)) { req.setAttribute("error", "初始化认证配置错误,缺少参数!"); req.getRequestDispatcher(portalError).forward(request, response); return; } if (defaultDomain == null || "".equals(defaultDomain)) { req.setAttribute("error", "初始化认证配置错误,缺少参数!"); req.getRequestDispatcher(portalError).forward(request, response); return; } logger.info("Host: " + domainController); logger.info("Domain: " + defaultDomain); NtlmPasswordAuthentication ntlm = negotiate(req, resp, false); if (ntlm == null) { return; } String authuser = ntlm.getName(); int pos = authuser.indexOf(BACK_SLASH); if (pos != -1) { authuser = authuser.substring(pos + 1); } logger.info("NTLM HTTP Authentication User: " + authuser); req.setAttribute("NTLM_USER", authuser); } catch (IOException e) { logger.error(e); req.setAttribute("error", "登录失败, 系统错误!"); req.getRequestDispatcher(portalError).forward(request, response); return; } catch (Exception e) { logger.error(e); req.setAttribute("error", "登录失败, 系统错误!"); req.getRequestDispatcher(portalError).forward(request, response); return; } chain.doFilter(request, response); } protected NtlmPasswordAuthentication negotiate(HttpServletRequest req, HttpServletResponse resp, boolean skipAuthentication) throws Exception { UniAddress dc; String msg; NtlmPasswordAuthentication ntlm = null; msg = req.getHeader("Authorization"); boolean offerBasic = enableBasic && (insecureBasic || req.isSecure()); if (msg != null && (msg.startsWith("NTLM ") || (offerBasic && msg.startsWith("Basic ")))) { if (msg.startsWith("NTLM ")) { HttpSession ssn = req.getSession(); byte[] challenge; if (loadBalance) { NtlmChallenge chal = (NtlmChallenge) ssn.getAttribute("NtlmHttpChal"); if (chal == null) { chal = SmbSession.getChallengeForDomain(); ssn.setAttribute("NtlmHttpChal", chal); } dc = chal.dc; challenge = chal.challenge; } else { dc = UniAddress.getByName(domainController, true); challenge = SmbSession.getChallenge(dc); } if ((ntlm = NtlmSsp.authenticate(req, resp, challenge)) == null) { return null; } /* negotiation complete, remove the challenge object */ ssn.removeAttribute("NtlmHttpChal"); } else { String auth = new String(Base64.decode(msg.substring(6)), "US-ASCII"); int index = auth.indexOf(':'); String user = (index != -1) ? auth.substring(0, index) : auth; String password = (index != -1) ? auth.substring(index + 1) : ""; index = user.indexOf('\\'); if (index == -1) index = user.indexOf('/'); String domain = (index != -1) ? user.substring(0, index) : defaultDomain; user = (index != -1) ? user.substring(index + 1) : user; ntlm = new NtlmPasswordAuthentication(domain, user, password); dc = UniAddress.getByName(domainController, true); } try { SmbSession.logon( dc, ntlm ); logger.info("NTLM HTTP Authentication: " + ntlm + " Successfully Authenticated Against " + dc); } catch( SmbAuthException sae ) { sae.printStackTrace(); logger.info("NTLM HTTP Authentication: " + ntlm.getName() + ": 0x" + jcifs.util.Hexdump.toHexString(sae.getNtStatus(), 8) + ": " + sae); if(sae.getNtStatus() == sae.NT_STATUS_ACCESS_VIOLATION ) { /* * Server challenge no longer valid for externally supplied * password hashes. */ HttpSession ssn = req.getSession(false); if (ssn != null) { ssn.removeAttribute( "NtlmHttpAuth" ); } } resp.setHeader( "WWW-Authenticate", "NTLM" ); if (offerBasic) { resp.addHeader( "WWW-Authenticate", "Basic realm=\"" + realm + "\""); } resp.setStatus( HttpServletResponse.SC_UNAUTHORIZED ); resp.setContentLength(0); /* Marcel Feb-15-2005 */ resp.flushBuffer(); return null; } req.getSession().setAttribute("NtlmHttpAuth", ntlm); } else { if (!skipAuthentication) { HttpSession ssn = req.getSession(false); if (ssn == null || (ntlm = (NtlmPasswordAuthentication) ssn.getAttribute("NtlmHttpAuth")) == null) { resp.setHeader("WWW-Authenticate", "NTLM"); if (offerBasic) { resp.addHeader("WWW-Authenticate", "Basic realm=\"" + realm + "\""); } resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); resp.setContentLength(0); resp.flushBuffer(); return null; } } } return ntlm; } public void destroy() { } }