Jersey文件上传解析技术详解
0x01 Jersey文件上传的实现方式
1.1 添加multipart请求解析支持
在Jersey中解析multipart请求,需要使用jersey-media-multipart模块:
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-multipart</artifactId>
</dependency>
启用multipart支持的配置方式:
在ResourceConfig类中配置:
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.server.ResourceConfig;
@ApplicationPath("/api")
public class MyApplication extends ResourceConfig {
public MyApplication() {
register(MultiPartFeature.class);
}
}
在web.xml中配置:
<init-param>
<param-name>jersey.config.server.provider.classnames</param-name>
<param-value>org.glassfish.jersey.media.multipart.MultiPartFeature</param-value>
</init-param>
1.2 实现方式
1.2.1 使用@FormDataParam注解
@POST
@Path("uploadimage")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public String uploadimage1(
@FormDataParam("file") InputStream fileInputStream,
@FormDataParam("file") FormDataContentDisposition disposition) {
String imageName = Calendar.getInstance().getTimeInMillis() + disposition.getFileName();
File file = new File(ARTICLE_IMAGES_PATH + imageName);
try {
FileUtils.copyInputStreamToFile(fileInputStream, file);
} catch (IOException ex) {
Logger.getLogger(UploadImageResource.class.getName()).log(Level.SEVERE, null, ex);
}
return "images/" + imageName;
}
1.2.2 使用MultiPart对象
@POST
@Path("/upload")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadFile(MultiPart multiPart) {
for (BodyPart bodyPart : multiPart.getBodyParts()) {
if (bodyPart instanceof FormDataBodyPart) {
FormDataBodyPart filePart = (FormDataBodyPart) bodyPart;
String fileName = filePart.getContentDisposition().getFileName();
InputStream fileContent = filePart.getEntityAs(InputStream.class);
// 处理文件内容
fileContent.close();
} else {
// 处理其他表单字段
}
}
return Response.ok("File uploaded successfully").build();
}
1.2.3 使用FormDataMultiPart对象
@POST
@Path("uploadimage2")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Viewable uploadimage2(FormDataMultiPart form, @Context HttpServletResponse response) throws UnsupportedEncodingException {
FormDataBodyPart filePart = form.getField("file");
FormDataBodyPart usernamePart = form.getField("username");
InputStream fileInputStream = filePart.getValueAs(InputStream.class);
FormDataContentDisposition formDataContentDisposition = filePart.getFormDataContentDisposition();
String source = formDataContentDisposition.getFileName();
String result = new String(source.getBytes("ISO8859-1"), "UTF-8");
String filePath = ARTICLE_IMAGES_PATH + result;
File file = new File(filePath);
try {
FileUtils.copyInputStreamToFile(fileInputStream, file);
} catch (IOException ex) {
Logger.getLogger(UploadImageResource.class.getName()).log(Level.SEVERE, null, ex);
}
response.setCharacterEncoding("UTF-8");
Map map = new HashMap();
map.put("src", result);
return new Viewable("/showImg", map);
}
0x02 上传请求解析过程
2.1 解析请求过程
核心解析方法:org.glassfish.jersey.media.multipart.internal.MultiPartReaderClientSide#readMultiPart
-
根据请求的mediaType判断是否为multipart/form-data:
- 是:创建FormDataMultiPart对象
- 否:创建MultiPart对象
-
将请求的头信息(headers)复制到MultiPart对象的头信息(multiPartHeaders)中
-
如果是multipart/form-data请求,根据User-Agent判断是否需要进行文件名修复(fileNameFix)
-
遍历解析MIMEPart:
- 为每个MIMEPart创建对应的BodyPart对象
- multipart/form-data请求:创建FormDataBodyPart对象
- 其他:创建普通BodyPart对象
- 复制MIMEPart头信息到BodyPart对象
-
从BodyPart头信息中获取"Content-Type"并设置BodyPart的媒体类型(MediaType)
2.2 判断是否是Multipart请求
使用@Consumes({"multipart/*"})注解,只要Content-Type以multipart/开头的请求都会经过MultiPartReaderClientSide处理(注意:必须小写)
对于FormDataMultiPart解析,限制Content-Type必须为multipart/form-data(支持大小写转换)
0x03 获取上传的文件名
3.1 获取方式
方式一:通过FormDataContentDisposition
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
public String upload(
@FormDataParam("file") InputStream fis,
@FormDataParam("file") FormDataContentDisposition fileDisposition) {
String fileName = fileDisposition.getFileName();
// ...
}
方式二:通过ContentDisposition参数
@POST
@Path("upload")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public String upload(FormDataMultiPart form, @Context HttpServletResponse response) throws UnsupportedEncodingException {
FormDataBodyPart filePart = form.getField("file");
FormDataContentDisposition formDataContentDisposition = filePart.getFormDataContentDisposition();
Map<String, String> parameters = formDataContentDisposition.getParameters();
String fileName = parameters.get("filename");
// ...
}
3.2 文件名解析过程
-
文件名初始化在
org.glassfish.jersey.media.multipart.ContentDisposition#createParameters中处理 -
通过
defineFileName方法处理:- 优先处理
filename*参数 - 如果
filename*为null,直接返回filename的值 - 否则对
filename*参数值进行处理:- 使用
FILENAME_EXT_VALUE_PATTERN正则匹配 - 通过
isFilenameValueCharsEncoded方法检查编码 - 提取charset、lang和filenameValueChars
- 如果charset为UTF-8,拼接生成最终文件名
- 使用
- 优先处理
-
安全注意事项:
- 不检查类似
../的路径穿越符号 - 可能导致跨目录文件写入
- 利用场景:
- 写入Linux定时任务(/etc/cron.d/)
- 写入SSH公钥(需root权限)
- 替换JDK系统jar文件
- 不检查类似
3.3 fileNameFix属性
作用:处理IE浏览器在filename参数中添加额外反斜杠的问题
触发条件:
- 请求为formData类型
- User-Agent包含"MSIE"关键字
处理逻辑:
- 当fileNameFix为true时,对反斜杠内容进行截断处理
- 去除文件名中的路径部分,只保留文件名
示例:
// 正常情况:反斜杠会被剔除
filename="C:\\test.txt" → "C:test.txt"
// fileNameFix=true时:
filename="C:\\test.txt" → "test.txt"
0x04 与SpringMVC的对比
4.1 请求解析差异
-
SpringMVC:
- 可能通过转换multipart请求绕过安全检查
- 灵活支持多种请求方式间的解析转换
-
Jersey:
- 严格区分请求类型
@FormParam不能处理multipart请求- 必须使用
@FormDataParam处理multipart/form-data
4.2 参数绑定差异
Jersey示例:
@POST
@Path("/test")
public Response test(@FormParam("msg") String msg) {
return Response.ok().entity(msg).build();
}
@FormParam:仅处理application/x-www-form-urlencoded@FormDataParam:专门处理multipart/form-data
安全最佳实践
-
文件名处理:
- 对上传文件名进行重命名
- 检查并过滤路径穿越符号(../)
- 限制文件扩展名
-
文件存储:
- 使用独立存储目录
- 设置适当权限
- 避免覆盖系统文件
-
请求验证:
- 严格验证Content-Type
- 限制请求大小
- 实施CSRF防护
-
错误处理:
- 自定义错误响应
- 避免泄露服务器信息
- 记录安全相关事件