From 36f1543a21addfb539165e878a3e2d1e364aed41 Mon Sep 17 00:00:00 2001 From: dushitaoyuan Date: Sat, 31 Aug 2019 21:42:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E5=AE=8C=E5=96=84=E5=AE=8C?= =?UTF-8?q?=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Readme.md | 12 +- .../securitydemo/utils/FileHandler.java | 260 ++++++++++++++++++ 2 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/taoyuanx/securitydemo/utils/FileHandler.java diff --git a/Readme.md b/Readme.md index fad9fd4..120bf9b 100644 --- a/Readme.md +++ b/Readme.md @@ -25,14 +25,22 @@ 7.文件未授权访问 解决方案:上传的文件,展示时后端返回签名的文件,访问时,走一次后端,方便做权限验证,参见,/api/upload,/api/file - +文件存储时,去除可执行权限,尽量和应用服务器进行物理隔离 8.文件不安全类型上传 解决方案:校验文件类型,校验流信息,校验文件真实类型,参见/api/upload -9密码泄露风险 +9.密码泄露风险 密码加密传输,参见login.html +10.越权访问 +解决方案:权限控制,参见SimpleAuthHandlerIntercepter +如果通过tomcat发布静态文件,可通过过滤器禁止非授权访问,如采用其他静态资源服务器,可严格控制后台权限,保证数据不被越权访问 +,静态资源越权访问,可通过client端js控制location + +11.中间人攻击 +解决方案:采用https协议,参数签名,返回值签名,防止参数或返回值被篡改 + diff --git a/src/main/java/com/taoyuanx/securitydemo/utils/FileHandler.java b/src/main/java/com/taoyuanx/securitydemo/utils/FileHandler.java new file mode 100644 index 0000000..dfff354 --- /dev/null +++ b/src/main/java/com/taoyuanx/securitydemo/utils/FileHandler.java @@ -0,0 +1,260 @@ +package com.taoyuanx.securitydemo.utils; + +import com.taoyuanx.securitydemo.constant.SystemConstants; +import com.taoyuanx.securitydemo.exception.UnAuthException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.URLEncoder; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; +import java.util.zip.GZIPOutputStream; + +/** + * @author dushitaoyuan + * @desc 文件处理类 + * @date 2019/7/3 17:31 + */ +public class FileHandler { + + /** + * 参数解释 + * cacheDir 本地文件缓存目录 + * fileDownStrategy 文件下载策略 + * isGzip 是否开启gzip压缩 + * tokenManager token生成器 + * tokenExpire token过期时间 + * urlFmt 授权url模板 如:http://localhost:8080/file?token=%s + */ + //文件处理类型: 0下载,1查看 2断点续传 + public static final String DOWN = "0", + LOOK = "1", + BYTE_RANGE_DOWN = "2"; + public static Logger LOG = LoggerFactory.getLogger(FileHandler.class); + private String cacheDir; + private boolean isGzip = false; + private String urlFmt; + private boolean tokenOpen = true; + private Integer buffSize = 4 << 20; + private String signKey; + + public FileHandler(String cacheDir, String signKey, + boolean isGzip, String urlFmt) { + this.signKey = signKey; + this.cacheDir = cacheDir; + this.isGzip = isGzip; + this.urlFmt = urlFmt + "?"+SystemConstants.REQUEST_PARAM_TOKEN_KEY+"=%s"; + } + + public FileHandler(String cacheDir, + boolean isGzip, String urlFmt) { + this.cacheDir = cacheDir; + this.isGzip = isGzip; + tokenOpen = false; + this.urlFmt = urlFmt + "?"+SystemConstants.REQUEST_PARAM_FILE_KEY+"=%s&"+SystemConstants.REQUEST_PARAM_TYPE_KEY+"=%s"; + } + + /** + * 文件处理 + * + * @param resp + * @param req + */ + public void handleFile(HttpServletResponse resp, HttpServletRequest req) throws Exception { + String type = null, filePath = null; + try { + if (tokenOpen) { + String token = req.getParameter(SystemConstants.REQUEST_PARAM_TOKEN_KEY); + if (StringUtils.isEmpty(token)) { + throw new UnAuthException("操作非法"); + } + Map signData = SimpleTokenManager.vafy(signKey, token); + type = (String) signData.get(SystemConstants.REQUEST_PARAM_TYPE_KEY); + filePath = (String) signData.get(SystemConstants.REQUEST_PARAM_FILE_KEY); + } else { + type = req.getParameter(SystemConstants.REQUEST_PARAM_TYPE_KEY); + filePath = req.getParameter(SystemConstants.REQUEST_PARAM_FILE_KEY); + } + File absoluteFile = new File(cacheDir, filePath); + switch (type) { + case LOOK: {// 查看 + resp.setContentType(req.getServletContext().getMimeType(absoluteFile.getName())); + } + break; + case DOWN: {// 下载 + resp.setContentType(req.getServletContext().getMimeType(absoluteFile.getName())); + resp.setHeader("Content-type", "application/octet-stream"); + resp.setHeader("Content-Disposition", + "attachment;fileName=" + URLEncoder.encode(absoluteFile.getName(), "UTF-8")); + resp.setContentType(req.getServletContext().getMimeType(absoluteFile.getName())); + + } + break; + case BYTE_RANGE_DOWN: { + handleByteRange(req, resp, absoluteFile, isGzip); + } + return; + } + //gzip 压缩 + if (isGzip) { + resp.setHeader("Content-Encoding", "gzip"); + GZIPOutputStream gzip = new GZIPOutputStream(resp.getOutputStream()); + handle(gzip, absoluteFile); + gzip.close(); + } else { + ServletOutputStream out = resp.getOutputStream(); + handle(out, absoluteFile); + out.flush(); + } + } catch (Exception e) { + handleError(resp, req, e, filePath); + } + + } + + /** + * 文件签名url构造接口 + * + * @param fileUrl 文件路径 + * @param handleType 文件处理类型 0下载 1查看 + * @return + */ + public String signFileUrl(String fileUrl, String handleType, Long expire, TimeUnit timeUnit) { + Map signMap = new HashMap<>(); + signMap.put(SystemConstants.REQUEST_PARAM_FILE_KEY, fileUrl); + signMap.put(SystemConstants.REQUEST_PARAM_TYPE_KEY, handleType); + return String.format(urlFmt, SimpleTokenManager.createToken(signKey, signMap, expire, timeUnit)); + } + + /** + * 文件url构造接口 + * + * @param fileUrl 文件路径 + * @param handleType 文件处理类型 0下载 1查看 + * @return + */ + public String createPublicUrl(String fileUrl, String handleType) { + return String.format(urlFmt, fileUrl, handleType); + } + + + private void handle(OutputStream out, File localFile) throws Exception { + FileChannel channel = new FileInputStream(localFile).getChannel(); + ByteBuffer buffer = ByteBuffer.allocate(buffSize); + int len = 0; + while ((len = channel.read(buffer)) > 0) { + buffer.flip(); + out.write(buffer.array(), 0, len); + buffer.clear(); + } + channel.close(); + + } + + private void handleError(HttpServletResponse resp, HttpServletRequest req, Exception e, String filePath) { + try { + LOG.error("处理文件[{}]异常{}", filePath, e); + if (e instanceof UnAuthException) { + resp.getWriter().println("operation not allowed,url Unauthorized or url expired"); + return; + } + resp.getWriter().println("file " + filePath + " error" + e.getMessage()); + } catch (IOException e1) { + } + } + + + public String getCacheDir() { + return cacheDir; + } + + + private void handleByteRange(HttpServletRequest req, HttpServletResponse resp, File localFile, boolean isGzip) throws Exception { + String range = req.getHeader("Range"); + String name = localFile.getName(); + long fileSize = localFile.length(); + resp.addHeader("ETag", String.valueOf(localFile.lastModified())); + if (StringUtils.isEmpty(range)) { + resp.setContentType(req.getServletContext().getMimeType(name)); + resp.setHeader("Content-Length", String.valueOf(fileSize)); + //断点下载支持 + resp.setHeader("Accept-Ranges", "bytes"); + return; + } + resp.setHeader("Accept-Ranges", "bytes"); + Long[] byteRange = resolveByteRange(range, fileSize); + Long start = byteRange[0], endSize = byteRange[1], count = endSize - start + 1; + //range格式非法 + if (start > fileSize || endSize > fileSize || start > endSize) { + resp.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + return; + } + resp.setHeader("Content-type", "application/octet-stream"); + resp.setHeader("Content-Disposition", + "attachment;fileName=" + URLEncoder.encode(localFile.getName(), "UTF-8")); + //格式 bytes %s-%s/%s + resp.addHeader(" Content-Range", String.format(CONTENTRANGE_FMT, start, endSize, fileSize)); + RandomAccessFile randomAccessFile = new RandomAccessFile(localFile, "r"); + FileChannel channel = randomAccessFile.getChannel(); + OutputStream outputStream = resp.getOutputStream(); + if (isGzip) { + resp.setHeader("Content-Encoding", "gzip"); + outputStream = new GZIPOutputStream(resp.getOutputStream()); + } + resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + channel.transferTo(start, count, Channels.newChannel(outputStream)); + outputStream.close(); + channel.close(); + } + + private Long[] resolveByteRange(String range, Long fileSize) { + /** + * Range header 格式:bytes= + * 1. 500-1000:指定开始和结束的范围,一般用于多线程下载。 + * 2. 500- :指定开始区间,一直传递到结束。这个就比较适用于断点续传、或者在线播放等等。 + * 3. -500:无开始区间,只意思是需要最后 500 bytes 的内容实体。 + * 4. 100-300,1000-3000:指定多个范围,这种方式使用的场景很少,了解一下就好了 + */ + Long[] result = new Long[2]; + range = range.replaceFirst("bytes=", ""); + if (range.contains(",")) { + //暂不支持 + } else { + int index = range.indexOf("-"); + String[] split = range.split("-"); + if (RANGE_FMT_1.matcher(range).matches()) { + result[0] = Long.parseLong(split[0]); + result[1] = Long.parseLong(split[1]); + return result; + } + if (RANGE_FMT_2.matcher(range).matches()) { + result[0] = Long.parseLong(split[0]); + result[1] = fileSize; + return result; + } + if (RANGE_FMT_3.matcher(range).matches()) { + result[0] = fileSize - Long.parseLong(split[0]); + result[1] = fileSize - 1; + return result; + } + + } + return null; + } + + private static Pattern RANGE_FMT_1 = Pattern.compile("^[0-9]{1,}-[0-9]{1,}$"); + private static Pattern RANGE_FMT_2 = Pattern.compile("^[0-9]{1,}-$"); + private static Pattern RANGE_FMT_3 = Pattern.compile("^-[0-9]{1,}$"); + + private static String CONTENTRANGE_FMT = "bytes %s-%s/%s"; +}