动态

详情 返回 返回

SMB(Server Message Block)協議實現對遠程 Windows 共享服務器或 Samba 服務的文件讀取 - 动态 详情

1. 概述

💡 作者:古渡藍按

個人微信公眾號:微信公眾號(深入淺出談java)
感覺本篇對你有幫助可以關注一下,會不定期更新知識和麪試資料、技巧!!!


本技術文檔旨在説明如何通過 SMB(Server Message Block)協議 實現對遠程 Windows 共享服務器或 Samba 服務的文件讀取、寫入與目錄遍歷操作。適用於 Java 應用程序在企業內網環境中安全、高效地訪問遠程共享資源。

主要應用場景包括:

  • 自動化從 Jenkins 構建服務器拉取構建產物;
  • 定期同步業務系統生成的配置/數據文件;
  • 批量處理遠程共享目錄中的特定類型文件(如 .hex.csv 等)。

1.1技術選型

組件 説明
協議 SMBv2 / SMBv3(推薦,安全性更高)
Java 庫 jcifs-ng(JCIFS 的活躍維護分支,支持現代 SMB 協議)
認證方式 NTLM(Windows 域或本地賬户)
開發語言 Java 8+

1.2前提條件

✅ 前提條件(必須滿足)

在目標服務器 173.16.1.152 上:

  1. 已共享 D:\jenkins 文件夾(這裏改成你需要訪問的共享目錄
    • 共享名建議為 jenkins → 訪問路徑:\\173.16.1.152\jenkins目錄名稱改成自己相應即可
  2. 你有一個有寫權限的 Windows 賬户(如 admin / deploy
  3. 防火牆允許 445 端口(默認 SMB 端口)
  4. “密碼保護的共享”已關閉(或你知道正確憑據)

💡 測試:在 winds 服務器上按 Win+R,輸入
\\173.16.1.152\jenkins
看是否能打開並寫入文件。


2、代碼實現

代碼執行流程示意圖:

deepseek_mermaid_20251107_b9bbed


2.1、添加依賴

<dependency>
    <groupId>eu.agno3.jcifs</groupId>
    <artifactId>jcifs-ng</artifactId>
    <version>2.1.9</version> <!-- 請使用最新穩定版 -->
</dependency>

2.2 提供接口核心代碼

這部分主要是提供接口,和有些參數校驗

@ApiOperation("只下載目錄下的 .hex 文件並下載")
    @PostMapping("/getJenkinsHexData")
    public R<String> downloadSmbHexFiles(@RequestBody SmbDownloadRequestVo request) {

        // 1. 路徑安全檢查(防止路徑遍歷)
        if (request.getLocalBaseDir() != null &&
                (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("文件下載失敗");
        }

    }

2.3邏輯實現核心代碼

具體代碼


@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(" -> 跳過此目錄。");
            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() && 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[8192];
                        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(" -> 繼續下載其他文件。");
                    }
                }
            }
        } else {
            System.out.println("⚠️  目錄 " + dirToList.getCanonicalPath() + " 列表為空或無法訪問。");
        }
    }
}

3、關鍵代碼邏輯深度解析


3.1. 路徑標準化處理(核心防錯點)

remoteBasePath = remoteBasePath.replaceAll("^[\\\\/]+|[\\\\/]+$", "");
String targetSmbUrl = baseSmbUrl + (remoteBasePath.isEmpty() ? "" : remoteBasePath.replace("\\", "/") + "/");
  • 為什麼必須:SMB 協議要求目錄路徑必須以 / 結尾,否則 listFiles() 會返回 SmbException: The system cannot find the file specified

  • 陷阱規避:處理了 Windows 路徑分隔符(\)與 URL 標準分隔符(/)的混合問題


3.2. 遞歸遍歷的防禦性設計

if (!dirUrl.endsWith("/")) {
    dirToList = new SmbFile(dirUrl + "/", context);
}
  • 關鍵作用:確保每次遍歷的目錄 URL 都以 / 結尾,避免因路徑格式錯誤導致的遍歷中斷

  • 錯誤案例:當遠程路徑為 smb://host/share/dir(缺少結尾/)時,listFiles() 會失敗


3.3. 目錄結構保持的精準實現

String relativePath = child.getCanonicalPath().substring(rootRemotePath.length());
relativePath = relativePath.replace('/', File.separatorChar);
localFilePath = localBasePath.resolve(relativePath);
  • 邏輯核心:通過 substring 精確截取相對路徑(從根路徑開始的後綴)

  • 平台適配replace('/', File.separatorChar) 確保在 Windows/Linux 系統都能正確生成本地路徑


3.4. 文件過濾的精準匹配

childName.toLowerCase().endsWith(fileType)
  • 設計優勢:大小寫不敏感匹配(.HEX/.Hex/.hex 均被識別)

  • 安全邊界:避免正則表達式導致的性能問題(endsWith 是 O(1) 操作)


3.5. 錯誤隔離機制(企業級健壯性)

try {
    // 下載文件
} catch (IOException e) {
    System.err.println("❌ 下載失敗: " + child.getCanonicalPath() + " - " + e.getMessage());
    System.err.println(" -> 繼續下載其他文件。");
}
  • 關鍵價值:單個文件下載失敗(如文件被鎖定)不會導致整個目錄遍歷中斷

  • 對比:若未做此隔離,一個文件失敗將導致整個任務失敗


3.6. 資源安全釋放

try (InputStream in = child.getInputStream();
     OutputStream out = new BufferedOutputStream(...)) {
    // 傳輸數據
}
  • Java 7 try-with-resources:確保 InputStreamOutputStream 在作用域結束時自動關閉
  • 避免泄漏:防止因未關閉流導致的文件句柄耗盡

3.7 代碼設計決策總結

代碼段 設計決策 為什麼重要
`replaceAll("[1]+ [\/]+$", "")` 路徑兩端標準化
dirUrl.endsWith("/") 檢查 目錄 URL 標準化 確保 listFiles() 能正確識別目錄
child.getCanonicalPath().substring() 精確計算相對路徑 保持原始目錄結構不丟失
toLowerCase().endsWith() 文件類型匹配 處理大小寫不敏感的文件名
try-with-resources 流資源自動關閉 防止文件句柄泄漏
獨立文件異常捕獲 錯誤隔離 保證單個文件失敗不影響整體任務

核心工程哲學:在 SMB 傳輸中,路徑格式錯誤隔離是決定系統是否能穩定運行的兩個關鍵因素。本實現通過精準處理路徑和設計錯誤隔離機制,確保在工業環境中(如測試設備頻繁生成文件)也能可靠運行。


4. 異常處理與最佳實踐

4.1 常見異常

異常類型 可能原因 解決方案
jcifs.smb.SmbAuthExceptionunknown user name or bad password 用户名/密碼錯誤,或無權限 檢查賬户權限,確認共享設置
jcifs.smb.SmbException: Access is denied 賬户有登錄權限但無文件訪問權限 聯繫管理員授予“讀取”或“完全控制”權限
SmbException: The system cannot find the file specified 1、目錄 URL 未以 / 結尾;
2、這個目錄在遠程並不存在
確保 listFiles() 前 URL 以 / 結尾
UnknownHostException 主機名無法解析 檢查 IP 或 DNS 配置
ConnectException 網絡不通或防火牆阻斷 確認 445/TCP 端口開放
The filename, directory name, or volume label syntax is incorrect 提供的文件或目錄名稱不符合語法要求(包含非法字符)。如 `< > : " ? * `)、URL 編碼問題。

4.2 安全建議

  • 禁止硬編碼密碼:使用配置中心、環境變量或密鑰管理服務;
  • 最小權限原則:SMB 賬户僅授予必要讀寫權限;
  • 啓用 SMB 簽名(如需):在 jcifs-ng 中可通過 withProperties() 配置;
  • 避免使用 SMBv1jcifs-ng 默認禁用 SMBv1,符合安全規範。

4.3 性能優化

  • 使用緩衝流(BufferedInputStream)提升大文件傳輸效率;
  • 對大量小文件,考慮壓縮後傳輸再解壓;
  • 控制併發連接數,避免對 SMB 服務器造成壓力。

  1. \/ ↩︎

Add a new 评论

Some HTML is okay.