先看使用效果: 能在上面修改删除添加文件.
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访问了
输入账号密码
接下来是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等存储。等以后有时间可以看看(咕咕)。