初步完善完毕
This commit is contained in:
12
Readme.md
12
Readme.md
@@ -25,14 +25,22 @@
|
|||||||
7.文件未授权访问
|
7.文件未授权访问
|
||||||
|
|
||||||
解决方案:上传的文件,展示时后端返回签名的文件,访问时,走一次后端,方便做权限验证,参见,/api/upload,/api/file
|
解决方案:上传的文件,展示时后端返回签名的文件,访问时,走一次后端,方便做权限验证,参见,/api/upload,/api/file
|
||||||
|
文件存储时,去除可执行权限,尽量和应用服务器进行物理隔离
|
||||||
|
|
||||||
8.文件不安全类型上传
|
8.文件不安全类型上传
|
||||||
|
|
||||||
解决方案:校验文件类型,校验流信息,校验文件真实类型,参见/api/upload
|
解决方案:校验文件类型,校验流信息,校验文件真实类型,参见/api/upload
|
||||||
|
|
||||||
9密码泄露风险
|
9.密码泄露风险
|
||||||
|
|
||||||
密码加密传输,参见login.html
|
密码加密传输,参见login.html
|
||||||
|
|
||||||
|
10.越权访问
|
||||||
|
解决方案:权限控制,参见SimpleAuthHandlerIntercepter
|
||||||
|
如果通过tomcat发布静态文件,可通过过滤器禁止非授权访问,如采用其他静态资源服务器,可严格控制后台权限,保证数据不被越权访问
|
||||||
|
,静态资源越权访问,可通过client端js控制location
|
||||||
|
|
||||||
|
11.中间人攻击
|
||||||
|
解决方案:采用https协议,参数签名,返回值签名,防止参数或返回值被篡改
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
260
src/main/java/com/taoyuanx/securitydemo/utils/FileHandler.java
Normal file
260
src/main/java/com/taoyuanx/securitydemo/utils/FileHandler.java
Normal file
@@ -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<String, Object> 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<String, Object> 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";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user