雯祖儿 發表於 2025-11-7 14:19:00

🌐SMB(Server Message Block)协议实现对远程 Windows 共享服务器或 Samba 服务的文件读取

<h1 id="1-概述">1. 概述</h1>
<blockquote>
<p>💡 <strong>作者</strong>:古渡蓝按</p>
<p><strong>个人微信公众号</strong>:微信公众号(深入浅出谈java)<br>
感觉本篇对你有帮助可以关注一下,会不定期更新知识和面试资料、技巧!!!</p>
</blockquote>
<br>
<p>本技术文档旨在说明如何通过 <strong>SMB(Server Message Block)协议</strong> 实现对远程 Windows 共享服务器或 Samba 服务的文件读取、写入与目录遍历操作。适用于 Java 应用程序在企业内网环境中安全、高效地访问远程共享资源。</p>
<p>主要应用场景包括:</p>
<ul>
<li>自动化从 Jenkins 构建服务器拉取构建产物;</li>
<li>定期同步业务系统生成的配置/数据文件;</li>
<li>批量处理远程共享目录中的特定类型文件(如 <code>.hex</code>、<code>.csv</code> 等)。</li>
</ul>
<h2 id="11技术选型">1.1技术选型</h2>
<table>
<thead>
<tr>
<th>组件</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>协议</strong></td>
<td>SMBv2 / SMBv3(推荐,安全性更高)</td>
</tr>
<tr>
<td><strong>Java 库</strong></td>
<td><code>jcifs-ng</code>(JCIFS 的活跃维护分支,支持现代 SMB 协议)</td>
</tr>
<tr>
<td><strong>认证方式</strong></td>
<td>NTLM(Windows 域或本地账户)</td>
</tr>
<tr>
<td><strong>开发语言</strong></td>
<td>Java 8+</td>
</tr>
</tbody>
</table>
<br>
<h2 id="12前提条件">1.2前提条件</h2>
<h3 id="-前提条件必须满足">✅ 前提条件(必须满足)</h3>
<p>在目标服务器 <strong><code>173.16.1.152</code></strong> 上:</p>
<ol>
<li>已共享 <code>D:\jenkins</code> 文件夹(<strong>这里改成你需要访问的共享目录</strong>)
<ul>
<li>共享名建议为 <code>jenkins</code> → 访问路径:<code>\\173.16.1.152\jenkins</code>(<strong>目录名称改成自己相应即可</strong>)</li>
</ul>
</li>
<li><strong>你有一个有写权限的 Windows 账户</strong>(如 <code>admin</code> / <code>deploy</code>)</li>
<li><strong>防火墙允许 445 端口</strong>(默认 SMB 端口)</li>
<li><strong>“密码保护的共享”已关闭</strong>(或你知道正确凭据)</li>
</ol>
<blockquote>
<p>💡 测试:在 <code>winds</code> 服务器上按 <code>Win+R</code>,输入<br>
<code>\\173.16.1.152\jenkins</code><br>
看是否能打开并写入文件。</p>
</blockquote>
<br>
<h1 id="2代码实现">2、代码实现</h1>
<p><strong>代码执行流程示意图:</strong></p>
<p><img src="https://img2024.cnblogs.com/blog/2719585/202511/2719585-20251107140523066-1527938019.png" alt="deepseek_mermaid_20251107_b9bbed" loading="lazy"></p>
<br>
<h2 id="21添加依赖">2.1、添加依赖</h2>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;eu.agno3.jcifs&lt;/groupId&gt;
    &lt;artifactId&gt;jcifs-ng&lt;/artifactId&gt;
    &lt;version&gt;2.1.9&lt;/version&gt; &lt;!-- 请使用最新稳定版 --&gt;
&lt;/dependency&gt;
</code></pre>
<br>
<h2 id="22-提供接口核心代码">2.2 提供接口核心代码</h2>
<p>这部分主要是提供接口,和有些参数校验</p>
<pre><code class="language-java">@ApiOperation("只下载目录下的 .hex 文件并下载")
    @PostMapping("/getJenkinsHexData")
    public R&lt;String&gt; downloadSmbHexFiles(@RequestBody SmbDownloadRequestVo request) {

      // 1. 路径安全检查(防止路径遍历)
      if (request.getLocalBaseDir() != null &amp;&amp;
                (request.getLocalBaseDir().contains("..") ||
                        request.getLocalBaseDir().contains("/"))) {
            throw new UserException("无效的本地基础目录路径");
      }

//      // 2. 从环境变量获取密码(生产环境必须)
//      String safePassword = System.getenv("SMB_PASSWORD");
//      if (safePassword == null) {
//            throw new UserException("未设置SMB_PASSWORD环境变量");
//      }

      // 3. 验证请求参数
      if (request.getSmbHost() == null || request.getShareName() == null ||
                request.getUsername() == null) {
            throw new UserException("缺少必需参数:smbHost、shareName、username");
      }

      try {
            // 4. 使用安全密码执行下载
            WindowsDownloaderHexFile.downloadHexFiles(
                  request.getSmbHost(),
                  request.getShareName(),
                  request.getRemotePath(),
                  request.getUsername(),
                  request.getPassword(),
                  request.getLocalBaseDir(),
                  true,
                  request.getFileExtension()
            );
            return R.ok("文件下载成功");
      } catch (Exception e) {
            return R.fail("文件下载失败");
      }

    }
</code></pre>
<br>
<h2 id="23逻辑实现核心代码">2.3逻辑实现核心代码</h2>
<p>具体代码</p>
<br>
<pre><code class="language-java">@Service
@Slf4j
public class WindowsDownloaderHexFile {


    /**
   * 从指定的 SMB 远程路径递归查找并下载所有 .hex 文件到本地目录。
   *
   * @param smbHost         SMB 服务器地址 (e.g., "172.16.1.85")
   * @param shareName         SMB 共享名 (e.g., "jenkins")
   * @param remoteBasePath    需要开始搜索的远程基础路径 (相对于共享根目录)。支持多级,使用 \ 或 / 分隔。 (e.g., "1/8-位号文件/图号导入文件")
   *                        如果路径是目录,建议以分隔符结尾或确保它是目录。
   * @param username          用户名 (e.g., "administrator")
   * @param password          密码 (e.g., "Jn300880")
   * @param localDownloadDir本地下载目录,找到的 .hex 文件将被下载到这里,并保持相对结构。(e.g., "D:\\DownloadedHexFiles")
   * @param preserveStructure 是否在本地保持远程的目录结构。true: 保持结构;false: 所有文件下载到 localDownloadDir 根目录下。
   * @param fileType   文件类型,指定只下载以该后缀的文件。
   * @throws RuntimeException 如果发生 IO、SMB 或其他错误
   */
    public static void downloadHexFiles(
            String smbHost,
            String shareName,
            String remoteBasePath,
            String username,
            String password,
            String localDownloadDir,
            boolean preserveStructure,
            String fileType) {

      CIFSContext context = null;
      try {
            // 1. 初始化 SMB 上下文和认证
            context = SingletonContext.getInstance().withCredentials(new NtlmPasswordAuthenticator(null, username, password));

            // 2. 构建基础 SMB URL
            String baseSmbUrl = "smb://" + smbHost + "/" + shareName + "/";

            // 3. 处理 remoteBasePath,确保格式正确并构建目标 SmbFile
            // 移除路径开头和结尾的多余分隔符
            remoteBasePath = remoteBasePath.replaceAll("^[\\\\/]+|[\\\\/]+$", "");
            String targetSmbUrl = baseSmbUrl + (remoteBasePath.isEmpty() ? "" : remoteBasePath.replace("\\", "/") + "/");

            SmbFile targetRemoteDir = new SmbFile(targetSmbUrl, context);

            // 4. 检查远程基础路径是否存在且为目录
            if (!targetRemoteDir.exists()) {
                throw new RuntimeException("远程路径不存在: " + targetSmbUrl);
            }
            if (!targetRemoteDir.isDirectory()) {
                throw new RuntimeException("远程路径不是目录: " + targetSmbUrl);
            }
            Path localBasePath = Paths.get(localDownloadDir);
            System.out.println("尝试创建目录: " + localBasePath.toAbsolutePath());
            try {
                Files.createDirectories(localBasePath);
                System.out.println("目录创建成功");
            } catch (Exception e) {
                e.printStackTrace();
            }

            // 6. 开始递归查找和下载
            findAndDownloadHexFiles(targetRemoteDir, localBasePath, context, preserveStructure, targetRemoteDir.getCanonicalPath(),fileType);

      } catch (Exception e) {
            throw new RuntimeException("初始化 SMB 连接或准备下载时出错", e);
      }
    }

    /**
   * 递归查找 .hex 文件并下载的核心方法。
   *
   * @param currentRemoteDir 当前正在处理的远程目录 SmbFile。
   * @param localBasePath    本地下载的基础目录 Path。
   * @param context          SMB 上下文。
   * @param preserveStructure 是否保持目录结构。
   * @param rootRemotePath   搜索的根远程路径,用于计算相对路径。
   * @param fileType   文件类型,指定只下载以该后缀的文件。
   * @throws IOException如果发生 IO 错误。
   * @throws SmbException 如果发生 SMB 错误。
   */
    private static void findAndDownloadHexFiles(
            SmbFile currentRemoteDir,
            Path localBasePath,
            CIFSContext context,
            boolean preserveStructure,
            String rootRemotePath,
            String fileType) throws IOException {
      log.info("进入递归方法开始查询!!!");

      // --- 确保目录 URL 以 '/' 结尾,这是 listFiles 的关键 ---
      String dirUrl = currentRemoteDir.getURL().toString();
      SmbFile dirToList = currentRemoteDir;
      if (!dirUrl.endsWith("/")) {
            dirToList = new SmbFile(dirUrl + "/", context);
      }
      // -------------------------------------------------------------

      SmbFile[] children;
      try {
            children = dirToList.listFiles(); // 列出子项
            System.out.println("列出目录内容: " + dirToList.getCanonicalPath());
      } catch (SmbException e) {
            System.err.println("❌ SmbException while listing children of: " + dirToList.getCanonicalPath() + " - " + e.getMessage());
            // 可以选择跳过此目录或抛出异常
            // 这里选择打印错误并跳过
            System.err.println(" -&gt; 跳过此目录。");
            return;
      }

      if (children != null) {
            for (SmbFile child : children) {
                String childName = child.getName();
                // 过滤掉 . 和 ..
                if (".".equals(childName) || "..".equals(childName)) {
                  continue;
                }

                if (child.isDirectory()) {
                  // 递归进入子目录
                  findAndDownloadHexFiles(child, localBasePath, context, preserveStructure, rootRemotePath,fileType);
                } else if (child.isFile() &amp;&amp; childName.toLowerCase().endsWith(fileType)) {
                  // 找到 .hex 文件,准备下载
                  System.out.println("🔍 找到 .hex 文件: " + child.getCanonicalPath());

                  // 计算本地文件路径
                  Path localFilePath;
                  if (preserveStructure) {
                        // 计算相对于搜索根目录的路径
                        String relativePath = child.getCanonicalPath().substring(rootRemotePath.length());
                        // 清理路径分隔符 (确保使用本地分隔符)
                        relativePath = relativePath.replace('/', File.separatorChar).replace('\\', File.separatorChar);
                        localFilePath = localBasePath.resolve(relativePath);
                  } else {
                        // 直接放在基础目录下
                        localFilePath = localBasePath.resolve(childName);
                  }

                  // 确保本地文件的父目录存在
                  Path parentDir = localFilePath.getParent();
                  if (parentDir != null) {
                        Files.createDirectories(parentDir);
                  }

                  // 下载文件
                  System.out.println("📥 下载到: " + localFilePath);
                  try (InputStream in = child.getInputStream();
                         OutputStream out = new BufferedOutputStream(new FileOutputStream(localFilePath.toFile()))) {

                        byte[] buffer = new byte;
                        int bytesRead;
                        while ((bytesRead = in.read(buffer)) != -1) {
                            out.write(buffer, 0, bytesRead);
                        }
                        System.out.println("✅ 下载完成: " + childName);

                  } catch (IOException e) {
                        System.err.println("❌ 下载文件失败: " + child.getCanonicalPath() + " - " + e.getMessage());
                        // 可以选择继续下载其他文件或抛出异常
                        // 这里选择打印错误并继续
                        System.err.println(" -&gt; 继续下载其他文件。");
                  }
                }
            }
      } else {
            System.out.println("⚠️目录 " + dirToList.getCanonicalPath() + " 列表为空或无法访问。");
      }
    }
}
</code></pre>
<br>
<h1 id="3关键代码逻辑深度解析">3、关键代码逻辑深度解析</h1>
<br>
<h3 id="31-路径标准化处理核心防错点">3.1. 路径标准化处理(核心防错点)</h3>
<pre><code class="language-java">remoteBasePath = remoteBasePath.replaceAll("^[\\\\/]+|[\\\\/]+$", "");
String targetSmbUrl = baseSmbUrl + (remoteBasePath.isEmpty() ? "" : remoteBasePath.replace("\\", "/") + "/");
</code></pre>
<ul>
<li>
<p><strong>为什么必须</strong>:SMB 协议要求目录路径必须以 <code>/</code> 结尾,否则 <code>listFiles()</code> 会返回 <code>SmbException: The system cannot find the file specified</code></p>
</li>
<li>
<p><strong>陷阱规避</strong>:处理了 Windows 路径分隔符(<code>\</code>)与 URL 标准分隔符(<code>/</code>)的混合问题</p>
</li>
</ul>
<br>
<h3 id="32-递归遍历的防御性设计">3.2. 递归遍历的防御性设计</h3>
<pre><code class="language-java">if (!dirUrl.endsWith("/")) {
    dirToList = new SmbFile(dirUrl + "/", context);
}
</code></pre>
<ul>
<li>
<p><strong>关键作用</strong>:确保每次遍历的目录 URL 都以 <code>/</code> 结尾,避免因路径格式错误导致的遍历中断</p>
</li>
<li>
<p><strong>错误案例</strong>:当远程路径为 <code>smb://host/share/dir</code>(缺少结尾/)时,<code>listFiles()</code> 会失败</p>
</li>
</ul>
<br>
<h3 id="33-目录结构保持的精准实现">3.3. 目录结构保持的精准实现</h3>
<pre><code class="language-java">String relativePath = child.getCanonicalPath().substring(rootRemotePath.length());
relativePath = relativePath.replace('/', File.separatorChar);
localFilePath = localBasePath.resolve(relativePath);
</code></pre>
<ul>
<li>
<p><strong>逻辑核心</strong>:通过 <code>substring</code> 精确截取相对路径(从根路径开始的后缀)</p>
</li>
<li>
<p><strong>平台适配</strong>:<code>replace('/', File.separatorChar)</code> 确保在 Windows/Linux 系统都能正确生成本地路径</p>
</li>
</ul>
<br>
<h3 id="34-文件过滤的精准匹配">3.4. 文件过滤的精准匹配</h3>
<pre><code class="language-java">childName.toLowerCase().endsWith(fileType)
</code></pre>
<ul>
<li>
<p><strong>设计优势</strong>:大小写不敏感匹配(<code>.HEX</code>/<code>.Hex</code>/<code>.hex</code> 均被识别)</p>
</li>
<li>
<p><strong>安全边界</strong>:避免正则表达式导致的性能问题(<code>endsWith</code> 是 O(1) 操作)</p>
</li>
</ul>
<br>
<h3 id="35-错误隔离机制企业级健壮性">3.5. 错误隔离机制(企业级健壮性)</h3>
<pre><code class="language-java">try {
    // 下载文件
} catch (IOException e) {
    System.err.println("❌ 下载失败: " + child.getCanonicalPath() + " - " + e.getMessage());
    System.err.println(" -&gt; 继续下载其他文件。");
}
</code></pre>
<ul>
<li>
<p><strong>关键价值</strong>:单个文件下载失败(如文件被锁定)不会导致整个目录遍历中断</p>
</li>
<li>
<p><strong>对比</strong>:若未做此隔离,一个文件失败将导致整个任务失败</p>
</li>
</ul>
<br>
<h3 id="36-资源安全释放">3.6. 资源安全释放</h3>
<pre><code class="language-java">try (InputStream in = child.getInputStream();
   OutputStream out = new BufferedOutputStream(...)) {
    // 传输数据
}
</code></pre>
<ul>
<li><strong>Java 7 try-with-resources</strong>:确保 <code>InputStream</code> 和 <code>OutputStream</code> 在作用域结束时自动关闭</li>
<li><strong>避免泄漏</strong>:防止因未关闭流导致的文件句柄耗尽</li>
</ul>
<br>
<h2 id="37-代码设计决策总结">3.7 代码设计决策总结</h2>
<table>
<thead>
<tr>
<th>代码段</th>
<th>设计决策</th>
<th>为什么重要</th>
</tr>
</thead>
<tbody>
<tr>
<td>`replaceAll("<sup class="footnote-ref"></sup>+</td>
<td>[\/]+$", "")`</td>
<td>路径两端标准化</td>
</tr>
<tr>
<td><code>dirUrl.endsWith("/")</code> 检查</td>
<td>目录 URL 标准化</td>
<td>确保 <code>listFiles()</code> 能正确识别目录</td>
</tr>
<tr>
<td><code>child.getCanonicalPath().substring()</code></td>
<td>精确计算相对路径</td>
<td>保持原始目录结构不丢失</td>
</tr>
<tr>
<td><code>toLowerCase().endsWith()</code></td>
<td>文件类型匹配</td>
<td>处理大小写不敏感的文件名</td>
</tr>
<tr>
<td><code>try-with-resources</code></td>
<td>流资源自动关闭</td>
<td>防止文件句柄泄漏</td>
</tr>
<tr>
<td>独立文件异常捕获</td>
<td>错误隔离</td>
<td>保证单个文件失败不影响整体任务</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>核心工程哲学</strong>:在 SMB 传输中,<strong>路径格式</strong>和<strong>错误隔离</strong>是决定系统是否能稳定运行的两个关键因素。本实现通过精准处理路径和设计错误隔离机制,确保在工业环境中(如测试设备频繁生成文件)也能可靠运行。</p>
</blockquote>
<br>
<h1 id="4-异常处理与最佳实践">4. 异常处理与最佳实践</h1>
<h3 id="41-常见异常">4.1 常见异常</h3>
<table>
<thead>
<tr>
<th>异常类型</th>
<th>可能原因</th>
<th>解决方案</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>jcifs.smb.SmbAuthException</code>:<strong><code>unknown user name or bad password</code></strong></td>
<td>用户名/密码错误,或无权限</td>
<td>检查账户权限,确认共享设置</td>
</tr>
<tr>
<td><code>jcifs.smb.SmbException: Access is denied</code></td>
<td>账户有登录权限但无文件访问权限</td>
<td>联系管理员授予“读取”或“完全控制”权限</td>
</tr>
<tr>
<td><code>SmbException: The system cannot find the file specified</code></td>
<td><strong>1、目录 URL 未以 <code>/</code> 结尾;<br>2、这个目录在远程并不存在</strong></td>
<td>确保 <code>listFiles()</code> 前 URL 以 <code>/</code> 结尾</td>
</tr>
<tr>
<td><code>UnknownHostException</code></td>
<td>主机名无法解析</td>
<td>检查 IP 或 DNS 配置</td>
</tr>
<tr>
<td><code>ConnectException</code></td>
<td>网络不通或防火墙阻断</td>
<td>确认 445/TCP 端口开放</td>
</tr>
<tr>
<td><strong><code>The filename, directory name, or volume label syntax is incorrect</code></strong></td>
<td>提供的文件或目录名称不符合语法要求(包含非法字符)。如 `&lt; &gt; : "</td>
<td>? * `)、URL 编码问题。</td>
</tr>
</tbody>
</table>
<h3 id="42-安全建议">4.2 安全建议</h3>
<ul>
<li><strong>禁止硬编码密码</strong>:使用配置中心、环境变量或密钥管理服务;</li>
<li><strong>最小权限原则</strong>:SMB 账户仅授予必要读写权限;</li>
<li><strong>启用 SMB 签名</strong>(如需):在 <code>jcifs-ng</code> 中可通过 <code>withProperties()</code> 配置;</li>
<li><strong>避免使用 SMBv1</strong>:<code>jcifs-ng</code> 默认禁用 SMBv1,符合安全规范。</li>
</ul>
<h3 id="43-性能优化">4.3 性能优化</h3>
<ul>
<li>使用缓冲流(<code>BufferedInputStream</code>)提升大文件传输效率;</li>
<li>对大量小文件,考虑压缩后传输再解压;</li>
<li>控制并发连接数,避免对 SMB 服务器造成压力。</li>
</ul>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>\/ ↩︎</p>
</li>
</ol>
</section><br><br>
来源:https://www.cnblogs.com/blbl-blog/p/19199555
頁: [1]
查看完整版本: 🌐SMB(Server Message Block)协议实现对远程 Windows 共享服务器或 Samba 服务的文件读取