用SpringBoot做一个Windows可挂载到本地的NAS网盘

先看使用效果:   能在上面修改删除添加文件.

1. 准备工作

调研了一下,windows挂载的几种方式如下:

1. NFSv3(RFC1813)可以基于Netty做开发,需要基于RFC1813实现linux的网络协议服务器

2. FTP(以前实现过 Apache有开源的Jar内嵌了FTP服务)

3. WebDav(tomcat自带了WebDav的Servlet 基于源码改造改造即可使用)

2. 选型工作

因为NFSv3只有RFC文件,资料比较少如果要实现需要抓包看TCP和UDP,成本较大所以未采用。

FTP实现过,需要在项目多加端口,暂不考虑。

最后就选择了WebDav。

3. 搭建WebDav&问题

https://tomcat.apache.org/tomcat-7.0-doc/api/org/apache/catalina/servlets/WebdavServlet.html
参考这个链接写了个SpringBoot版的Serverlet

@WebServlet(name = "MyServlet", urlPatterns = {UploadConstant.WEB_DAV_URL}
        , initParams = {@WebInitParam(name = "listings", value = "true"),
        @WebInitParam(name = "readonly", value = "false"),
        @WebInitParam(name = "debug", value = "0")
})
public class WebDavSupport extends WebdavServlet {

}

同时application启动类上加上   @ServletComponentScan

发现windows可以连接,但是每次重启后文件会消失,而且缺少我需要的密码还有自定义目录位置。

 因为SpringBoot下WebDav暴露的目录是一个临时目录: eg

 C:/Users/%username%/AppData/Local/Temp/tomcat-docbase.8080.9768959453169634688

 也就是所有上传的文件都到这个临时目录里面了。

4. 源码分析

逻辑基本在这两个类里面: WebdavServlet处理了大部分逻辑,少部分在DefaultServlet里面
org.apache.catalina.servlets.DefaultServlet                
                ^
                |
org.apache.catalina.servlets.WebdavServlet

  WebdavServlet 源码所有对资源的操作都是用了 DefaultServlet 的resources对象。

   例如copyResource

/**
     * Copy a resource.
     *
     * @param req Servlet request
     * @param resp Servlet response
     * @return boolean true if the copy is successful
     * @throws IOException If an IO error occurs
     */
    private boolean copyResource(HttpServletRequest req,
                                 HttpServletResponse resp)
            throws IOException {

        // Parsing destination header

        String destinationPath = req.getHeader("Destination");

        if (destinationPath == null) {
            resp.sendError(WebdavStatus.SC_BAD_REQUEST);
            return false;
        }

        // Remove url encoding from destination
        destinationPath = UDecoder.URLDecode(destinationPath, StandardCharsets.UTF_8);

        int protocolIndex = destinationPath.indexOf("://");
        if (protocolIndex >= 0) {
            // if the Destination URL contains the protocol, we can safely
            // trim everything upto the first "/" character after "://"
            int firstSeparator =
                destinationPath.indexOf('/', protocolIndex + 4);
            if (firstSeparator < 0) {
                destinationPath = "/";
            } else {
                destinationPath = destinationPath.substring(firstSeparator);
            }
        } else {
            String hostName = req.getServerName();
            if ((hostName != null) && (destinationPath.startsWith(hostName))) {
                destinationPath = destinationPath.substring(hostName.length());
            }

            int portIndex = destinationPath.indexOf(':');
            if (portIndex >= 0) {
                destinationPath = destinationPath.substring(portIndex);
            }

            if (destinationPath.startsWith(":")) {
                int firstSeparator = destinationPath.indexOf('/');
                if (firstSeparator < 0) {
                    destinationPath = "/";
                } else {
                    destinationPath =
                        destinationPath.substring(firstSeparator);
                }
            }
        }

        // Normalise destination path (remove '.' and '..')
        destinationPath = RequestUtil.normalize(destinationPath);

        String contextPath = req.getContextPath();
        if ((contextPath != null) &&
            (destinationPath.startsWith(contextPath))) {
            destinationPath = destinationPath.substring(contextPath.length());
        }

        String pathInfo = req.getPathInfo();
        if (pathInfo != null) {
            String servletPath = req.getServletPath();
            if ((servletPath != null) &&
                (destinationPath.startsWith(servletPath))) {
                destinationPath = destinationPath
                    .substring(servletPath.length());
            }
        }

        if (debug > 0) {
            log("Dest path :" + destinationPath);
        }

        // Check destination path to protect special subdirectories
        if (isSpecialPath(destinationPath)) {
            resp.sendError(WebdavStatus.SC_FORBIDDEN);
            return false;
        }

        String path = getRelativePath(req);

        if (destinationPath.equals(path)) {
            resp.sendError(WebdavStatus.SC_FORBIDDEN);
            return false;
        }

        // Parsing overwrite header

        boolean overwrite = true;
        String overwriteHeader = req.getHeader("Overwrite");

        if (overwriteHeader != null) {
            if (overwriteHeader.equalsIgnoreCase("T")) {
                overwrite = true;
            } else {
                overwrite = false;
            }
        }

        // Overwriting the destination

        WebResource destination = resources.getResource(destinationPath);

        if (overwrite) {
            // Delete destination resource, if it exists
            if (destination.exists()) {
                if (!deleteResource(destinationPath, req, resp, true)) {
                    return false;
                }
            } else {
                resp.setStatus(WebdavStatus.SC_CREATED);
            }
        } else {
            // If the destination exists, then it's a conflict
            if (destination.exists()) {
                resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED);
                return false;
            }
        }

        // Copying source to destination

        Hashtable<String,Integer> errorList = new Hashtable<>();

        boolean result = copyResource(errorList, path, destinationPath);

        if ((!result) || (!errorList.isEmpty())) {
            if (errorList.size() == 1) {
                resp.sendError(errorList.elements().nextElement().intValue());
            } else {
                sendReport(req, resp, errorList);
            }
            return false;
        }

        // Copy was successful
        if (destination.exists()) {
            resp.setStatus(WebdavStatus.SC_NO_CONTENT);
        } else {
            resp.setStatus(WebdavStatus.SC_CREATED);
        }

        // Removing any lock-null resource which would be present at
        // the destination path
        lockNullResources.remove(destinationPath);

        return true;
    }

 在DefaultServlet 看到其实WebResourceRoot拿到的是org.apache.catalina.resources

 可以往里面加我们自己要暴露的路径就好了。这个操作比较像是代码启动tomcat。

//
resources = (WebResourceRoot) getServletContext().getAttribute(Globals.RESOURCES_ATTR);
//Globals.RESOURCES_ATTR
public static final String RESOURCES_ATTR = "org.apache.catalina.resources";  

 5. 编码&Windows下的坑

   0. 启动类@SpringBootApplication 下加上@ServletComponentScan

   1. 我们需要在WebResourceRoot(这是Tomcat自带的一个类)下添加自定义的路径,这里默认是D盘。

    2. 同时添加Basic密码校验

package hex.wang.hexworker.upload;

import org.apache.catalina.Globals;
import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.servlets.WebdavServlet;
import org.apache.catalina.webresources.DirResourceSet;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Value;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;

/**
 * @Author hex.wang
 * @Class WebDavSupport
 * @Description
 * @Date 2022/1/5 14:45
 */
@WebServlet(name = "MyServlet", urlPatterns = {UploadConstant.WEB_DAV_URL}
        , initParams = {@WebInitParam(name = "listings", value = "true"),
        @WebInitParam(name = "readonly", value = "false"),
        @WebInitParam(name = "debug", value = "0")
})

public class WebDavSupport extends WebdavServlet {
    @Value("${config.baseDav:d:/}")
    private String baseDav;
    @Value("${config.user:user}")
    private String user;
    @Value("${config.password:password}")
    private String password;


    @Override
    public void init() throws ServletException {
        WebResourceRoot webResourceRoot = (WebResourceRoot) getServletContext().getAttribute(Globals.RESOURCES_ATTR);
        File additionWebInfClasses = new File(baseDav);
        webResourceRoot.addPreResources(new DirResourceSet(webResourceRoot, "/", additionWebInfClasses
                .getAbsolutePath(), "/"));
        super.init();
    }

    public boolean auth(ServletRequest req, ServletResponse res) {
        String authorization = ((HttpServletRequest) req).getHeader("Authorization");
        if (authorization != null) {
            String base64 = authorization.replaceFirst("Basic\\s+", "");
            String string = new String(Base64.decodeBase64(base64), Charset.forName("UTF-8"));
            String array[] = string.split(":");
            if (array.length == 2&&user.equals(array[0]) && password.equals(array[1])) {
                return true;
            }
        }
        HttpServletResponse res1 = (HttpServletResponse) res;
        res1.setStatus(HttpServletResponse.SC_UNAUTHORIZED);//401
        res1.setCharacterEncoding("UTF-8");
        res1.setHeader("WWW-Authenticate", "Basic realm=\"DAV\"");
        return false;
    }


    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        if (auth(req, res)) {
            super.service(req, res);
        }
    }
}

可以看到已经可以web访问了 

http://localhost:8080/WebDav

输入账号密码

接下来是windows挂载:

1. 修改注册表

打开cmd 输入regedit 

修改

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters

值为2 (默认windows下面不支持basic认证http,只支持https毕竟不安全)

2. 重启webclient

打开管理员CMD

net stop webclient

net start webclient

3. 挂载

net use * http://localhost:8080/WebDav 

输入账号密码 挂载成功。

 6. 备注

这个问题基本上是没修改注册表。也有可能windows webclient版本有问题。可以下个专业的webdav客户端。

7. 感想

1. 其实可以做到每个user一个目录。

2. 可以根据源码自己写个webdav连接hdfs minio等存储。等以后有时间可以看看(咕咕)。

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值