diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/config/MinioConfig.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/config/MinioConfig.java new file mode 100644 index 0000000..7e2e071 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/config/MinioConfig.java @@ -0,0 +1,38 @@ +package com.lld.im.service.config; + +import com.lld.im.service.utils.MinioUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Minio文件上传配置文件 + */ +@Slf4j +@Configuration +public class MinioConfig { + @Value(value = "${minio.minio_url}") + private String minioUrl; + @Value(value = "${minio.minio_name}") + private String minioName; + @Value(value = "${minio.minio_pass}") + private String minioPass; + @Value(value = "${minio.bucketName}") + private String bucketName; + + @Bean + public void initMinio(){ + if(!minioUrl.startsWith("http")){ + minioUrl = "http://" + minioUrl; + } + if(!minioUrl.endsWith("/")){ + minioUrl = minioUrl.concat("/"); + } + MinioUtil.setMinioUrl(minioUrl); + MinioUtil.setMinioName(minioName); + MinioUtil.setMinioPass(minioPass); + MinioUtil.setBucketName(bucketName); + } + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/MinioUtil.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/MinioUtil.java new file mode 100644 index 0000000..8b89f35 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/MinioUtil.java @@ -0,0 +1,244 @@ +package com.lld.im.service.utils; + + +import com.lld.im.service.utils.filter.FileTypeFilter; +import com.lld.im.service.utils.filter.StrAttackFilter; +import io.minio.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.net.URLDecoder; + +/** + * minio文件上传工具类 + */ +@Slf4j +public class MinioUtil { + private static String minioUrl; + private static String minioName; + private static String minioPass; + private static String bucketName; + + public static void setMinioUrl(String minioUrl) { + MinioUtil.minioUrl = minioUrl; + } + + public static void setMinioName(String minioName) { + MinioUtil.minioName = minioName; + } + + public static void setMinioPass(String minioPass) { + MinioUtil.minioPass = minioPass; + } + + public static void setBucketName(String bucketName) { + MinioUtil.bucketName = bucketName; + } + + public static String getMinioUrl() { + return minioUrl; + } + + public static String getBucketName() { + return bucketName; + } + + private static MinioClient minioClient = null; + + /** + * 上传文件 + * @param file + * @return + */ + public static String upload(MultipartFile file, String bizPath, String customBucket) { + String file_url = ""; + //update-begin-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击 + bizPath= StrAttackFilter.filter(bizPath); + //update-end-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击 + String newBucket = bucketName; + if(StringUtils.isNotEmpty(customBucket)){ + newBucket = customBucket; + } + try { + initMinio(minioUrl, minioName,minioPass); + // 检查存储桶是否已经存在 + if(minioClient.bucketExists(BucketExistsArgs.builder().bucket(newBucket).build())) { + log.info("Bucket already exists."); + } else { + // 创建一个名为ota的存储桶 + minioClient.makeBucket(MakeBucketArgs.builder().bucket(newBucket).build()); + log.info("create a new bucket."); + } + //update-begin-author:liusq date:20210809 for: 过滤上传文件类型 + FileTypeFilter.fileTypeFilter(file); + //update-end-author:liusq date:20210809 for: 过滤上传文件类型 + InputStream stream = file.getInputStream(); + // 获取文件名 + String orgName = file.getOriginalFilename(); + if("".equals(orgName)){ + orgName=file.getName(); + } + orgName = getFileName(orgName); + String objectName = bizPath+"/" + +( orgName.indexOf(".")==-1 + ?orgName + "_" + System.currentTimeMillis() + :orgName.substring(0, orgName.lastIndexOf(".")) + "_" + System.currentTimeMillis() + orgName.substring(orgName.lastIndexOf(".")) + ); + + // 使用putObject上传一个本地文件到存储桶中。 + if(objectName.startsWith("/")){ + objectName = objectName.substring(1); + } + PutObjectArgs objectArgs = PutObjectArgs.builder().object(objectName) + .bucket(newBucket) + .contentType("application/octet-stream") + .stream(stream,stream.available(),-1).build(); + minioClient.putObject(objectArgs); + stream.close(); + file_url = minioUrl+newBucket+"/"+objectName; + }catch (Exception e){ + log.error(e.getMessage(), e); + } + return file_url; + } + + /** + * 判断文件名是否带盘符,重新处理 + * @param fileName + * @return + */ + private static String getFileName(String fileName){ + //判断是否带有盘符信息 + // Check for Unix-style path + int unixSep = fileName.lastIndexOf('/'); + // Check for Windows-style path + int winSep = fileName.lastIndexOf('\\'); + // Cut off at latest possible point + int pos = (winSep > unixSep ? winSep : unixSep); + if (pos != -1) { + // Any sort of path separator found... + fileName = fileName.substring(pos + 1); + } + //替换上传文件名字的特殊字符 + fileName = fileName.replace("=","").replace(",","").replace("&","") + .replace("#", "").replace("“", "").replace("”", ""); + //替换上传文件名字中的空格 + fileName=fileName.replaceAll("\\s",""); + return fileName; + } + + /** + * 文件上传 + * @param file + * @param bizPath + * @return + */ + public static String upload(MultipartFile file, String bizPath) { + return upload(file,bizPath,null); + } + + /** + * 获取文件流 + * @param bucketName + * @param objectName + * @return + */ + public static InputStream getMinioFile(String bucketName,String objectName){ + InputStream inputStream = null; + try { + initMinio(minioUrl, minioName, minioPass); + GetObjectArgs objectArgs = GetObjectArgs.builder().object(objectName) + .bucket(bucketName).build(); + inputStream = minioClient.getObject(objectArgs); + } catch (Exception e) { + log.info("文件获取失败" + e.getMessage()); + } + return inputStream; + } + + /** + * 删除文件 + * @param bucketName + * @param objectName + * @throws Exception + */ + public static void removeObject(String bucketName, String objectName) { + try { + initMinio(minioUrl, minioName,minioPass); + RemoveObjectArgs objectArgs = RemoveObjectArgs.builder().object(objectName) + .bucket(bucketName).build(); + minioClient.removeObject(objectArgs); + }catch (Exception e){ + log.info("文件删除失败" + e.getMessage()); + } + } + + /** + * 获取文件外链 + * @param bucketName + * @param objectName + * @param expires + * @return + */ + public static String getObjectURL(String bucketName, String objectName, Integer expires) { + initMinio(minioUrl, minioName,minioPass); + try{ + GetPresignedObjectUrlArgs objectArgs = GetPresignedObjectUrlArgs.builder().object(objectName) + .bucket(bucketName) + .expiry(expires).build(); + String url = minioClient.getPresignedObjectUrl(objectArgs); + return URLDecoder.decode(url,"UTF-8"); + }catch (Exception e){ + log.info("文件路径获取失败" + e.getMessage()); + } + return null; + } + + /** + * 初始化客户端 + * @param minioUrl + * @param minioName + * @param minioPass + * @return + */ + private static MinioClient initMinio(String minioUrl, String minioName,String minioPass) { + if (minioClient == null) { + try { + minioClient = MinioClient.builder() + .endpoint(minioUrl) + .credentials(minioName, minioPass) + .build(); + } catch (Exception e) { + e.printStackTrace(); + } + } + return minioClient; + } + + /** + * 上传文件到minio + * @param stream + * @param relativePath + * @return + */ + public static String upload(InputStream stream,String relativePath) throws Exception { + initMinio(minioUrl, minioName,minioPass); + if(minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) { + log.info("Bucket already exists."); + } else { + // 创建一个名为ota的存储桶 + minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); + log.info("create a new bucket."); + } + PutObjectArgs objectArgs = PutObjectArgs.builder().object(relativePath) + .bucket(bucketName) + .contentType("application/octet-stream") + .stream(stream,stream.available(),-1).build(); + minioClient.putObject(objectArgs); + stream.close(); + return minioUrl+bucketName+"/"+relativePath; + } + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/filter/FileTypeFilter.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/filter/FileTypeFilter.java new file mode 100644 index 0000000..191d661 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/filter/FileTypeFilter.java @@ -0,0 +1,164 @@ +package com.lld.im.service.utils.filter; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Iterator; + +/** + * @Description: TODO + * @author: lsq + * @date: 2021年08月09日 15:29 + */ +public class FileTypeFilter { + + //文件后缀 + private static String[] forbidType = {"jsp","php"}; + + // 初始化文件头类型,不够的自行补充 + final static HashMap fileTypeMap = new HashMap<>(); + + static { + fileTypeMap.put("3c25402070616765206c", "jsp"); + fileTypeMap.put("3c3f7068700a0a2f2a2a0a202a205048", "php"); + /* fileTypeMap.put("ffd8ffe000104a464946", "jpg"); + fileTypeMap.put("89504e470d0a1a0a0000", "png"); + fileTypeMap.put("47494638396126026f01", "gif"); + fileTypeMap.put("49492a00227105008037", "tif"); + fileTypeMap.put("424d228c010000000000", "bmp"); + fileTypeMap.put("424d8240090000000000", "bmp"); + fileTypeMap.put("424d8e1b030000000000", "bmp"); + fileTypeMap.put("41433130313500000000", "dwg"); + fileTypeMap.put("3c21444f435459504520", "html"); + fileTypeMap.put("3c21646f637479706520", "htm"); + fileTypeMap.put("48544d4c207b0d0a0942", "css"); + fileTypeMap.put("696b2e71623d696b2e71", "js"); + fileTypeMap.put("7b5c727466315c616e73", "rtf"); + fileTypeMap.put("38425053000100000000", "psd"); + fileTypeMap.put("46726f6d3a203d3f6762", "eml"); + fileTypeMap.put("d0cf11e0a1b11ae10000", "doc"); + fileTypeMap.put("5374616E64617264204A", "mdb"); + fileTypeMap.put("252150532D41646F6265", "ps"); + fileTypeMap.put("255044462d312e350d0a", "pdf"); + fileTypeMap.put("2e524d46000000120001", "rmvb"); + fileTypeMap.put("464c5601050000000900", "flv"); + fileTypeMap.put("00000020667479706d70", "mp4"); + fileTypeMap.put("49443303000000002176", "mp3"); + fileTypeMap.put("000001ba210001000180", "mpg"); + fileTypeMap.put("3026b2758e66cf11a6d9", "wmv"); + fileTypeMap.put("52494646e27807005741", "wav"); + fileTypeMap.put("52494646d07d60074156", "avi"); + fileTypeMap.put("4d546864000000060001", "mid"); + fileTypeMap.put("504b0304140000000800", "zip"); + fileTypeMap.put("526172211a0700cf9073", "rar"); + fileTypeMap.put("235468697320636f6e66", "ini"); + fileTypeMap.put("504b03040a0000000000", "jar"); + fileTypeMap.put("4d5a9000030000000400", "exe"); + fileTypeMap.put("3c25402070616765206c", "jsp"); + fileTypeMap.put("4d616e69666573742d56", "mf"); + fileTypeMap.put("3c3f786d6c2076657273", "xml"); + fileTypeMap.put("494e5345525420494e54", "sql"); + fileTypeMap.put("7061636b616765207765", "java"); + fileTypeMap.put("406563686f206f66660d", "bat"); + fileTypeMap.put("1f8b0800000000000000", "gz"); + fileTypeMap.put("6c6f67346a2e726f6f74", "properties"); + fileTypeMap.put("cafebabe0000002e0041", "class"); + fileTypeMap.put("49545346030000006000", "chm"); + fileTypeMap.put("04000000010000001300", "mxp"); + fileTypeMap.put("504b0304140006000800", "docx"); + fileTypeMap.put("6431303a637265617465", "torrent"); + fileTypeMap.put("6D6F6F76", "mov"); + fileTypeMap.put("FF575043", "wpd"); + fileTypeMap.put("CFAD12FEC5FD746F", "dbx"); + fileTypeMap.put("2142444E", "pst"); + fileTypeMap.put("AC9EBD8F", "qdf"); + fileTypeMap.put("E3828596", "pwl"); + fileTypeMap.put("2E7261FD", "ram");*/ + } + + /** + * @param fileName + * @return String + * @description 通过文件后缀名获取文件类型 + */ + private static String getFileTypeBySuffix(String fileName) { + return fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length()); + } + + /** + * 文件类型过滤 + * + * @param file + */ + public static void fileTypeFilter(MultipartFile file) throws Exception { + String suffix = getFileType(file); + for (String type : forbidType) { + if (type.contains(suffix)) { + throw new Exception("上传失败,文件类型异常:" + suffix); + } + } + } + + /** + * 通过读取文件头部获得文件类型 + * + * @param file + * @return 文件类型 + * @throws Exception + */ + + private static String getFileType(MultipartFile file) throws Exception { + String fileExtendName = null; + InputStream is; + try { + //is = new FileInputStream(file); + is = file.getInputStream(); + byte[] b = new byte[10]; + is.read(b, 0, b.length); + String fileTypeHex = String.valueOf(bytesToHexString(b)); + Iterator keyIter = fileTypeMap.keySet().iterator(); + while (keyIter.hasNext()) { + String key = keyIter.next(); + // 验证前5个字符比较 + if (key.toLowerCase().startsWith(fileTypeHex.toLowerCase().substring(0, 5)) + || fileTypeHex.toLowerCase().substring(0, 5).startsWith(key.toLowerCase())) { + fileExtendName = fileTypeMap.get(key); + break; + } + } + // 如果不是上述类型,则判断扩展名 + if (StringUtils.isBlank(fileExtendName)) { + String fileName = file.getOriginalFilename(); + return getFileTypeBySuffix(fileName); + } + is.close(); + return fileExtendName; + } catch (Exception exception) { + throw new Exception(exception.getMessage(), exception); + } + } + + /** + * 获得文件头部字符串 + * + * @param src + * @return + */ + private static String bytesToHexString(byte[] src) { + StringBuilder stringBuilder = new StringBuilder(); + if (src == null || src.length <= 0) { + return null; + } + for (int i = 0; i < src.length; i++) { + int v = src[i] & 0xFF; + String hv = Integer.toHexString(v); + if (hv.length() < 2) { + stringBuilder.append(0); + } + stringBuilder.append(hv); + } + return stringBuilder.toString(); + } +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/filter/StrAttackFilter.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/filter/StrAttackFilter.java new file mode 100644 index 0000000..0fda4d3 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/filter/StrAttackFilter.java @@ -0,0 +1,24 @@ +package com.lld.im.service.utils.filter; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * 文件上传字符串过滤特殊字符 + */ +public class StrAttackFilter { + + public static String filter(String str) throws PatternSyntaxException { + // 清除掉所有特殊字符 + String regEx = "[`_《》~!@#$%^&*()+=|{}':;',\\[\\].<>?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]"; + Pattern p = Pattern.compile(regEx); + Matcher m = p.matcher(str); + return m.replaceAll("").trim(); + } + +// public static void main(String[] args) { +// String filter = filter("@#jeecg/《》【bo】¥%……&*(o))))!@t<>,.,/?'\'~~`"); +// System.out.println(filter); +// } +} diff --git a/hs-im-server/im-service/src/main/resources/application-dev.yml b/hs-im-server/im-service/src/main/resources/application-dev.yml index 30b2e64..b6fef8f 100644 --- a/hs-im-server/im-service/src/main/resources/application-dev.yml +++ b/hs-im-server/im-service/src/main/resources/application-dev.yml @@ -117,3 +117,10 @@ httpclient: mpp: entityBasePath: com.lld.im.service.friendship.dao + +# minio文件上传 +minio: + minio_url: http://43.139.240.200:9000 + minio_name: admin + minio_pass: 12345678 + bucketName: im