0

0

Spring WebClient实现Windows NTLM认证的专业指南

心靈之曲

心靈之曲

发布时间:2025-10-28 12:09:22

|

790人浏览过

|

来源于php中文网

原创

spring webclient实现windows ntlm认证的专业指南

在现代企业级应用中,与依赖Windows NTLM认证的后端服务进行交互是常见需求。然而,Spring Framework的响应式Web客户端——WebClient,不像其前身RestTemplate那样直接支持NTLM认证,这给开发者带来了一定的挑战。本文将详细介绍如何通过自定义ExchangeFilterFunction并结合JCIFS库,为WebClient实现健壮的Windows NTLM认证机制。

1. NTLM认证机制概述与WebClient的挑战

NTLM(NT LAN Manager)是一种挑战-响应(Challenge-Response)协议,用于验证用户身份。其基本流程涉及客户端发送认证请求(Type 1消息),服务器返回挑战(Type 2消息),客户端根据挑战和用户凭据计算响应(Type 3消息)并发送给服务器,最终服务器验证响应。

RestTemplate可以通过配置HttpClient(如Apache HttpClient)并使用NTCredentials来相对容易地实现NTLM认证。然而,WebClient通常默认使用Reactor Netty作为底层HTTP客户端,且其ExchangeFilterFunctions主要针对Basic认证等更简单的机制。直接使用basicAuthentication或手动设置Authorization头并不能满足NTLM的挑战-响应流程。因此,我们需要一个能够拦截请求和响应,并根据NTLM协议进行多步处理的自定义过滤器。

2. 基于JCIFS的自定义NTLM认证过滤器实现

为了在WebClient中实现NTLM认证,我们可以利用JCIFS库,它提供了NTLM协议的Java实现。核心思想是创建一个ExchangeFilterFunction,它能够:

  1. 在初始请求中发送NTLM Type 1消息。
  2. 捕获服务器返回的NTLM Type 2挑战。
  3. 根据Type 2挑战和用户凭据生成NTLM Type 3响应,并重新发送请求。

以下是实现NtlmAuthorizedClientExchangeFilterFunction的详细代码:

import jcifs.ntlmssp.NtlmFlags;
import jcifs.ntlmssp.Type1Message;
import jcifs.ntlmssp.Type2Message;
import jcifs.ntlmssp.Type3Message;
import jcifs.util.Base64;
import org.springframework.http.HttpHeaders;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import java.io.IOException;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Spring WebClient的NTLM认证过滤器。
 * 使用JCIFS库实现NTLM挑战-响应机制。
 */
public final class NtlmAuthorizedClientExchangeFilterFunction implements ExchangeFilterFunction {

    private final String domain;
    private final String username;
    private final String password;
    private final boolean doSigning;
    private final int lmCompatibility;

    /**
     * 构造函数。
     * @param domain NTLM域
     * @param username 用户名
     * @param password 密码
     * @param doSigning 是否进行消息签名(推荐为true以增强安全性)
     * @param lmCompatibility LM兼容性级别 (0-5),影响密码哈希算法
     */
    public NtlmAuthorizedClientExchangeFilterFunction(String domain, String username, String password, boolean doSigning, int lmCompatibility) {
        this.domain = domain;
        this.username = username;
        this.password = password;
        this.doSigning = doSigning;
        this.lmCompatibility = lmCompatibility;
        // 设置JCIFS的LM兼容性系统属性
        System.setProperty("jcifs.smb.lmCompatibility", Integer.toString(lmCompatibility));
    }

    @Override
    public Mono<ClientResponse> filter(final ClientRequest request, final ExchangeFunction next) {
        // NTLM认证需要状态,因此在每次请求中创建一个新的上下文
        // NTLM上下文的标志,包括请求签名和NTLMSSP_NEGOTIATE_ALWAYS_SIGN
        int flags = NtlmFlags.NTLMSSP_NEGOTIATE_UNICODE |
                    NtlmFlags.NTLMSSP_NEGOTIATE_OEM |
                    NtlmFlags.NTLMSSP_REQUEST_TARGET |
                    NtlmFlags.NTLMSSP_NEGOTIATE_NTLM;
        if (doSigning) {
            flags |= NtlmFlags.NTLMSSP_NEGOTIATE_SIGN | NtlmFlags.NTLMSSP_NEGOTIATE_ALWAYS_SIGN;
        }

        try {
            // 第一步:发送Type 1消息
            Type1Message type1 = new Type1Message(flags, domain, null); // workstation留空,JCIFS会自动处理
            byte[] type1Bytes = type1.toByteArray();

            return next.exchange(addNtlmHeader(request, type1Bytes))
                // 确保请求按顺序处理,以维持HTTP连接和状态
                .publishOn(Schedulers.single())
                .flatMap(clientResponse -> {
                    // 检查响应是否包含NTLM挑战
                    List<String> ntlmAuthHeaders = getNtlmAuthHeaders(clientResponse);
                    if (ntlmAuthHeaders.isEmpty()) {
                        // 如果没有NTLM挑战,则可能是认证成功或非NTLM认证,直接返回响应
                        // 或者根据业务需求抛出错误
                        return Mono.just(clientResponse);
                    }

                    // 提取Type 2消息
                    String ntlmHeader = ntlmAuthHeaders.get(0);
                    if (ntlmHeader.length() <= 5) { // "NTLM " + base64 content
                        return Mono.error(new IOException("Invalid NTLM challenge header: " + ntlmHeader));
                    }
                    try {
                        byte[] type2Bytes = Base64.decode(ntlmHeader.substring(5));
                        Type2Message type2 = new Type2Message(type2Bytes);

                        // 第二步:根据Type 2消息和凭据生成Type 3消息
                        Type3Message type3 = new Type3Message(type2, password, domain, username);
                        byte[] type3Bytes = type3.toByteArray();

                        // 重新发送带有Type 3消息的请求
                        return next.exchange(addNtlmHeader(request, type3Bytes));
                    } catch (IOException e) {
                        return Mono.error(new RuntimeException("Failed to process NTLM Type 2 message or generate Type 3 message", e));
                    }
                });
        } catch (IOException e) {
            return Mono.error(new RuntimeException("Failed to generate NTLM Type 1 message", e));
        }
    }

    /**
     * 从ClientResponse中提取NTLM认证头。
     * @param clientResponse 客户端响应
     * @return 包含"NTLM"前缀的WWW-Authenticate头列表
     */
    private static List<String> getNtlmAuthHeaders(ClientResponse clientResponse) {
        List<String> wwwAuthHeaders = clientResponse.headers().header(HttpHeaders.WWW_AUTHENTICATE);
        // 过滤出NTLM头,并按长度排序(通常更长的包含Type 2消息)
        return wwwAuthHeaders.stream()
            .filter(h -> h.startsWith("NTLM"))
            .sorted(Comparator.comparingInt(String::length).reversed()) // 优先处理更长的NTLM头
            .collect(Collectors.toList());
    }

    /**
     * 向请求中添加NTLM认证头。
     * @param clientRequest 原始请求
     * @param ntlmPayload NTLM消息的字节数组
     * @return 添加了认证头的新请求
     */
    private ClientRequest addNtlmHeader(ClientRequest clientRequest, byte[] ntlmPayload) {
        return ClientRequest.from(clientRequest)
            .header(HttpHeaders.AUTHORIZATION, "NTLM ".concat(Base64.encode(ntlmPayload)))
            .build();
    }
}

2.1 代码详解

  • 构造函数: 接收domain、username、password、doSigning和lmCompatibility作为参数。doSigning控制是否启用NTLM消息签名,这对于安全性非常重要。lmCompatibility是一个JCIFS特有的设置,影响密码哈希算法,通常根据目标NTLM服务器的配置进行调整。
  • filter方法: 这是ExchangeFilterFunction的核心。
    • Type 1消息发送: 首先构建一个Type1Message(初始认证请求),将其转换为字节数组并Base64编码,然后作为Authorization头(NTLM <base64_encoded_type1>)添加到原始请求中。
    • publishOn(Schedulers.single()): 关键点。NTLM认证是一个有状态的协议,需要确保同一个HTTP连接用于后续的挑战-响应。publishOn(Schedulers.single())确保了对next.exchange的调用在同一个线程上顺序执行,有助于维持HTTP连接的活性(Keep-Alive)和状态一致性。
    • 响应处理与Type 2消息提取: 在收到第一个响应后,过滤器检查WWW-Authenticate头是否包含NTLM挑战。如果包含,它会提取Base64编码的Type 2消息。
    • Type 3消息生成与重新发送: 使用提取的Type 2消息、用户的密码、域和用户名来生成Type3Message(认证响应)。同样,将其转换为字节数组并Base64编码,作为Authorization头添加到原始请求中,并重新发起请求。
  • getNtlmAuthHeaders: 辅助方法,用于从WWW-Authenticate头中过滤出所有以"NTLM"开头的认证头。通过对长度进行降序排序,可以优先处理包含Type 2消息的更长的头。
  • addNtlmHeader: 辅助方法,用于构建新的ClientRequest,并添加带有NTLM payload的Authorization头。

3. 集成自定义过滤器到WebClient

要使用上述自定义NTLM过滤器,您需要将其添加到WebClient.builder()的过滤器链中:

靠岸学术
靠岸学术

一款集翻译,阅读,文献管理于一体的英文文献阅读器

下载
import org.springframework.web.reactive.function.client.WebClient;

public class NtlmWebClientConfig {

    public WebClient ntlmAuthenticatedWebClient() {
        String domain = "YOUR_DOMAIN"; // 例如 "MYDOMAIN"
        String username = "YOUR_USERNAME";
        String password = "YOUR_PASSWORD";
        boolean doSigning = true; // 推荐开启消息签名
        int lmCompatibility = 3; // 根据NTLM服务器配置调整,常见值如3

        NtlmAuthorizedClientExchangeFilterFunction ntlmFilter =
            new NtlmAuthorizedClientExchangeFilterFunction(domain, username, password, doSigning, lmCompatibility);

        return WebClient.builder()
            .filter(ntlmFilter)
            // 可以添加其他过滤器或配置
            .baseUrl("https://my.ntlm.protected.service")
            .build();
    }

    public static void main(String[] args) {
        NtlmWebClientConfig config = new NtlmWebClientConfig();
        WebClient webClient = config.ntlmAuthenticatedWebClient();

        webClient.get()
            .uri("/some/resource")
            .retrieve()
            .bodyToMono(String.class)
            .doOnNext(System.out::println)
            .doOnError(e -> System.err.println("Error: " + e.getMessage()))
            .block(); // 阻塞以等待结果,实际应用中通常使用订阅
    }
}

3.1 依赖管理

为了使上述代码正常工作,您需要在项目的pom.xml(Maven)或build.gradle(Gradle)中添加JCIFS库的依赖:

Maven:

<dependency>
    <groupId>jcifs</groupId>
    <artifactId>jcifs</artifactId>
    <version>1.3.17</version> <!-- 请检查最新稳定版本 -->
</dependency>

Gradle:

implementation 'jcifs:jcifs:1.3.17' // 请检查最新稳定版本

注意: JCIFS库的最新版本可能在Maven中央仓库中有所变动,请查阅官方文档或Maven Central以获取最新稳定版本。

4. 注意事项与限制

  • 凭据管理: 示例代码中直接在代码中硬编码了用户名和密码。在生产环境中,这些凭据应通过安全的方式(如环境变量、Vault、Spring Cloud Config等)进行管理和注入。
  • lmCompatibility: 这个参数非常重要。不同的NTLM服务器对LM兼容性级别有不同的要求。如果设置不正确,可能导致认证失败。通常,3是一个比较通用的值,但可能需要根据实际环境进行调整。
  • 消息签名 (doSigning): 启用消息签名(doSigning = true)可以增强NTLM认证的安全性,防止中间人攻击篡改消息。建议在生产环境中开启。
  • 错误处理: 示例代码中的错误处理相对简单。在实际应用中,应添加更健壮的错误日志和异常处理逻辑,例如区分认证失败、网络错误等。
  • 当前用户上下文认证: 关于在Windows环境下不提供用户名和密码,而是使用当前运行进程的用户上下文进行NTLM认证的需求,这是一个更复杂的场景。JCIFS库本身可能不直接支持这种"无凭据"的认证方式,因为它通常需要明确的用户名、密码和域。这种需求通常依赖于底层的操作系统API(如SSPI),这超出了纯Java库的范畴,并且在跨平台环境中实现起来非常困难。对于WebClient而言,目前没有直接的、通用的解决方案来利用操作系统的当前用户上下文进行NTLM认证。如果这是强制要求,可能需要考虑使用JNI/JNA调用Windows SSPI API,或者寻找其他支持此功能的特定HTTP客户端库。

5. 总结

通过实现自定义的ExchangeFilterFunction并结合JCIFS库,我们成功地为Spring WebClient带来了Windows NTLM认证的能力。这种方法遵循了NTLM的挑战-响应协议,并允许开发者在响应式应用中与NTLM保护的资源进行交互。虽然实现过程比简单的Basic认证复杂,但其提供了高度的灵活性和控制力。在实际应用中,务必注意凭据的安全管理、lmCompatibility的正确配置以及健壮的错误处理。对于利用当前用户上下文进行认证的特殊需求,则需要考虑更深层次的系统集成方案。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
spring框架介绍
spring框架介绍

本专题整合了spring框架相关内容,想了解更多详细内容,请阅读专题下面的文章。

161

2025.08.06

Java Spring Security 与认证授权
Java Spring Security 与认证授权

本专题系统讲解 Java Spring Security 框架在认证与授权中的应用,涵盖用户身份验证、权限控制、JWT与OAuth2实现、跨站请求伪造(CSRF)防护、会话管理与安全漏洞防范。通过实际项目案例,帮助学习者掌握如何 使用 Spring Security 实现高安全性认证与授权机制,提升 Web 应用的安全性与用户数据保护。

89

2026.01.26

Java 微服务与 Spring Cloud 实战
Java 微服务与 Spring Cloud 实战

本专题讲解 Java 微服务架构的开发与实践,重点使用 Spring Cloud 实现服务注册与发现、负载均衡、熔断与限流、分布式配置管理、API Gateway 和消息队列。通过实际项目案例,帮助开发者理解 如何将传统单体应用拆分为高可用、可扩展的微服务架构,并有效管理和调度分布式系统中的各个组件。

51

2026.02.05

Java Maven专题
Java Maven专题

本专题聚焦 Java 主流构建工具 Maven 的学习与应用,系统讲解项目结构、依赖管理、插件使用、生命周期与多模块项目配置。通过企业管理系统、Web 应用与微服务项目实战,帮助学员全面掌握 Maven 在 Java 项目构建与团队协作中的核心技能。

0

2025.09.15

pdf怎么转换成xml格式
pdf怎么转换成xml格式

将 pdf 转换为 xml 的方法:1. 使用在线转换器;2. 使用桌面软件(如 adobe acrobat、itext);3. 使用命令行工具(如 pdftoxml)。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

1949

2024.04.01

xml怎么变成word
xml怎么变成word

步骤:1. 导入 xml 文件;2. 选择 xml 结构;3. 映射 xml 元素到 word 元素;4. 生成 word 文档。提示:确保 xml 文件结构良好,并预览 word 文档以验证转换是否成功。想了解更多xml的相关内容,可以阅读本专题下面的文章。

2119

2024.08.01

xml是什么格式的文件
xml是什么格式的文件

xml是一种纯文本格式的文件。xml指的是可扩展标记语言,标准通用标记语言的子集,是一种用于标记电子文件使其具有结构性的标记语言。想了解更多相关的内容,可阅读本专题下面的相关文章。

1171

2024.11.28

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

765

2023.08.10

TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

26

2026.03.13

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 6万人学习

国外Web开发全栈课程全集
国外Web开发全栈课程全集

共12课时 | 1万人学习

React核心原理新老生命周期精讲
React核心原理新老生命周期精讲

共12课时 | 1.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号