Waffle Java客户端:如何使用特定域用户认证Web服务
解决Tomcat上Spring应用访问Kerberos/NTLM认证OData服务的问题
我之前在处理Tomcat部署的Spring应用访问Kerberos认证的OData服务时遇到过几乎一模一样的问题,折腾了好几天终于搞定了,给你分享几个可行的方案和踩过的坑:
核心问题分析
你测试环境用NTCredentials能成功,是因为服务返回的WWW-Authenticate是NTLM;但客户环境返回Negotiate,意味着服务优先使用Kerberos认证,而NTCredentials只支持NTLM,所以直接失效。Waffle默认用当前进程的用户票据(比如启动Tomcat的账户),但Tomcat作为后台服务运行时没有交互登录用户,所以需要手动指定用户密码来获取Kerberos票据。
方案一:使用Waffle API手动指定用户认证
Waffle确实支持指定用户名密码完成Kerberos/NTLM认证,你之前尝试logonUser和impersonate没成功,大概率是没正确处理安全上下文和HttpClient的集成。以下是完整的实现代码:
1. 添加Waffle依赖
如果是Maven项目,在pom.xml中加入:
<dependency> <groupId>com.github.waffle</groupId> <artifactId>waffle-jna</artifactId> <version>3.3.0</version> </dependency> <dependency> <groupId>com.github.waffle</groupId> <artifactId>waffle-httpclient</artifactId> <version>3.3.0</version> </dependency>
2. 构建支持Kerberos的RestTemplate
import com.github.waffle.jna.WindowsAuthProviderImpl; import com.github.waffle.jna.WindowsCredentialsHandle; import com.github.waffle.windows.auth.IWindowsAuthProvider; import com.github.waffle.windows.auth.IWindowsCredentialsHandle; import org.apache.http.auth.AuthScope; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.config.AuthSchemes; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.impl.auth.SPNegoSchemeFactory; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.HttpClientBuilder; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; public class KerberosRestTemplateFactory { public RestTemplate createRestTemplate(String domain, String username, String password, String serviceSpn) { // 初始化Waffle认证提供者 IWindowsAuthProvider authProvider = new WindowsAuthProviderImpl(); try (IWindowsCredentialsHandle credHandle = authProvider.logonUser(username, domain, password)) { // 绑定到目标服务的SPN(必须正确,否则Kerberos认证失败) authProvider.initializeSecurityContext(credHandle, serviceSpn); // 注册支持Negotiate(Kerberos/NTLM)的认证方案 Registry<?> authSchemeRegistry = RegistryBuilder.create() .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true)) .build(); // 设置凭证提供者 CredentialsProvider credsProvider = new BasicCredentialsProvider(); credsProvider.setCredentials(AuthScope.ANY, new com.github.waffle.apache.http.NegotiateCredentials()); // 构建HttpClient var httpClient = HttpClientBuilder.create() .setDefaultAuthSchemeRegistry(authSchemeRegistry) .setDefaultCredentialsProvider(credsProvider) .build(); // 配置RestTemplate var requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); RestTemplate restTemplate = new RestTemplate(); restTemplate.setRequestFactory(requestFactory); return restTemplate; } catch (Exception e) { throw new RuntimeException("Failed to initialize Kerberos authenticated RestTemplate", e); } } }
关键注意事项
- SPN必须正确:目标OData服务的Service Principal Name(SPN)格式通常是
HTTP/your-odata-host.your-domain.com@YOUR-DOMAIN.COM。可以在客户域控制器上用setspn -L <服务运行账户>命令查看目标服务已注册的SPN。 - Tomcat运行权限:Tomcat的运行账户(比如域账户或本地账户)需要拥有允许模拟客户端和允许登录本地的权限,否则
logonUser会失败。 - 调试Kerberos:可以开启Waffle的调试日志,添加JVM参数:
-Dwaffle.debug=true,帮助定位认证失败原因。
方案二:使用Java原生Kerberos API
如果Waffle的方案还是有问题,可以尝试Java原生的Kerberos登录API,直接获取用户票据后模拟请求:
1. 配置JAAS和Kerberos文件
- 创建
jaas.conf文件:
KerberosLogin { com.sun.security.auth.module.Krb5LoginModule required useKeyTab=false useTicketCache=false principal="USERNAME@YOUR-DOMAIN.COM" debug=true; }
- 确保
krb5.ini(Windows)或krb5.conf(Linux)配置正确,指向客户的域控制器。
2. 代码实现
import javax.security.auth.Subject; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; import java.io.IOException; import java.security.PrivilegedAction; import org.springframework.web.client.RestTemplate; import org.springframework.http.ResponseEntity; public class NativeKerberosClient { public String callODataService(String url, String domain, String username, String password) throws LoginException { // 设置系统属性 System.setProperty("java.security.auth.login.config", "path/to/jaas.conf"); System.setProperty("java.security.krb5.conf", "path/to/krb5.ini"); System.setProperty("sun.security.krb5.debug", "true"); // 调试用 // 创建登录上下文 LoginContext lc = new LoginContext("KerberosLogin", new CallbackHandler() { @Override public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { for (Callback cb : callbacks) { if (cb instanceof NameCallback) { ((NameCallback) cb).setName(username + "@" + domain.toUpperCase()); } else if (cb instanceof PasswordCallback) { ((PasswordCallback) cb).setPassword(password.toCharArray()); } } } }); // 登录获取Subject lc.login(); Subject subject = lc.getSubject(); // 使用Subject.doAs模拟用户请求 return Subject.doAs(subject, (PrivilegedAction<String>) () -> { RestTemplate restTemplate = new RestTemplate(); ResponseEntity<String> response = restTemplate.exchange(url, org.springframework.http.HttpMethod.GET, null, String.class); return response.getBody(); }); } }
最后总结
- 优先选择Waffle方案,它对Windows环境的Kerberos/NTLM协议支持更完善,能自动处理Negotiate的协议切换。
- 认证失败的最常见原因是SPN配置错误,一定要确认目标服务的SPN和代码中指定的一致。
- 如果Tomcat运行在Windows服务中,确保服务账户有足够的权限,必要时联系客户的AD管理员配置权限。
内容的提问来源于stack exchange,提问作者Jon C
相关产品推荐
相关产品推荐

