【Spring Boot深度实践】打造高效安全的文件上传服务:从JWT鉴权到持久化存储

在当前数字化时代,文件上传功能是各类Web应用中不可或缺的一部分。本文将为您展示如何利用Spring
Boot框架及其优秀生态构建一个优雅且安全的文件上传服务——UploadFileServiceImp。此服务不仅实现了批量处理上传请求、确保文件安全存储,还通过JWT令牌提取用户ID,并将文件访问地址妥善保存至数据库中。

首先,我们关注UploadFileServiceImp的核心实现逻辑。该类作为服务接口UploadFileService的实现,注入了HttpServletRequest对象以捕获客户端请求中的重要信息,如JWT令牌。通过Hutool库解析令牌,获取并转换用户的唯一标识(ID),确保文件上传与用户身份紧密关联,提高了系统安全性。

代码中使用了 请导包使用此代码

hutool包

lombok包

uploadFilesCollectionImp Collection 实现类
@Slf4j
@RestController
@RequestMapping("/upload")
public class uploadFilesCollectionImp {

    private final uploadFileServiceImp uploadFileServiceImp;

    public uploadFilesCollectionImp(uploadFileServiceImp uploadFileServiceImp) {
        this.uploadFileServiceImp = uploadFileServiceImp;
    }

    /**
     * 处理POST方法的多文件上传请求,接收一个MultipartFile类型的数组参数。
     *
     * @param files 客户端提交的待上传文件集合
     * @return Result 对象封装了上传结果信息。如果所有文件上传成功,则返回包含每个文件完整访问地址的Result.success对象;
     * 若有文件上传失败,则返回Result.error对象并附带错误提示信息。
     */
    @PostMapping
    public R upload(@RequestParam("files") MultipartFile[] files) {
        try {
            List<String> uploadFileUrls = uploadFileServiceImp.uploadFile(files);
            // 如果上传成功,则返回包含文件URL的R.success对象
            if (!CollectionUtils.isEmpty(uploadFileUrls)) {
                return R.success(uploadFileUrls);
            }

            // 如果上传失败且uploadFileUrls为空(假设这代表失败)
            return R.error("文件上传失败");
        } catch (RuntimeException e) {
            // 捕获自定义异常,如文件上传过程中可能出现的问题
            log.error("文件上传时发生错误:{}", e.getMessage());
            return R.error("文件上传失败:" + e.getMessage());
        } catch (Exception e) {
            // 其他未知异常
            log.error("文件上传时发生未知错误:{}", e.getMessage());
            return R.error("文件上传过程中出现未知错误");
        }
    }
}
UploadFileService Service接口层
public interface UploadFileService {

    /**
     * 批量上传文件方法。该方法接收一个MultipartFile数组,代表用户要上传的多个文件,
     * 并返回一个包含所有成功上传文件访问链接地址的字符串列表。
     *
     * @param files MultipartFile[] 用户选择并提交的多个待上传文件对象
     * @return List<String> 成功上传文件的网络访问链接地址列表,每个地址对应一个上传成功的文件
     */
    List<String> uploadFile(MultipartFile[] files);
}
UploadFileService Service实现类
@Slf4j
@Service
public class uploadFileServiceImp implements UploadFileService {

    // Spring自动注入HttpServletRequest对象以获取请求信息
    @Autowired
    private  HttpServletRequest request;

    // 注入自定义工具类FileUtils实例,用于进行文件操作
    @Autowired
    private  FileUtils fileUtils;

    // 注入上传链接地址服务实现类,用于保存上传文件的访问链接
    @Autowired
    private  uploadLinkAddressServiceImp uploadLinkAddressServiceImp;

    // 配置文件中读取上传服务的基础URL
    private String "8080";

    // 配置读取HTTP协议头
    private String "http://";


    /**
     * 批量处理文件上传请求,并返回上传成功文件的详细访问地址列表。
     *
     * @param files 用户上传的MultipartFile数组
     * @return List<String> 包含已上传文件完整访问地址的列表
     */
    @Override
    public List<String> uploadFile(MultipartFile[] files) {
        Object id;
        Path path;
        List<String> uploadedFileDetails = new ArrayList<>();

        try {
            // 解析请求头中的token,提取用户ID
            String token = request.getHeader("token");
            JWT jwt = JWTUtil.parseToken(token);
            // 确保JWT类型正确及包含"chao"字段(此处未使用)
            jwt.getHeader(JWTHeader.TYPE);
            jwt.getPayload("chao");

            // 提取用户ID并转换为字符串
            id = jwt.getPayload("id");
            if (id instanceof Number) {
                id = String.valueOf(((Number) id).longValue()); // 根据实际数据类型选择合适的方法
            } else if (id != null) {
                id = id.toString();
            }
        } catch (Exception e) {
            log.error("解析或获取token中的用户ID时出错: {}", e.getMessage());
            uploadedFileDetails.add("上传文件获取token失败");
            return uploadedFileDetails;
        }

        // 遍历所有待上传的文件
        for (MultipartFile file : files) {
            if (Objects.isNull(file) || file.isEmpty()) {
                // 如果有空文件,则终止上传并返回错误提示
                uploadedFileDetails.add("存在空文件,请重新上传所有文件");
                return uploadedFileDetails;
            }

            try {
                // 获取文件内容字节数组
                byte[] bytes = file.getBytes();

                // 获取当前服务器IP地址
                String serverIp = GetServiceIp.getServerIp();

                // 设置目标文件存储路径(基于用户ID)
                path = Paths.get(fileUtils.getUploadFolder((String) id));

                // 确保目录存在,如果不存在则创建
                if (!Files.exists(path)) {
                    Files.createDirectories(path);
                }

                // 获取上传文件的扩展名
                String extension = FileUtils.getFileExtension(file);

                // 生成新的唯一文件名(UUID+扩展名)
                String newFileName = IdUtil.simpleUUID() + "." + extension;

                // 将文件保存到指定目录下
                FileUtils.getFileByBytes(bytes, fileUtils.getUploadFolder((String) id), newFileName);

                // 构建文件的内部访问路径
                String fullFilePath = "/picture/" + id + "/" + newFileName;

                // 添加上传后的文件访问地址到结果列表
                uploadedFileDetails.add(fullFilePath);

                // 将文件访问地址保存到数据库或其他持久化存储中
                uploadLinkAddressServiceImp.addLinkAddress((String) id, fullFilePath);

            } catch (Exception e) {
                log.error("上传文件[{}]时发生错误: {}", file.getOriginalFilename(), e.getMessage());

                uploadedFileDetails.add("部分文件上传失败,请检查后重试");
                return uploadedFileDetails;
            }
        }

        // 检查上传成功的文件数量与原始提交文件总数是否一致
        if (uploadedFileDetails.size() == files.length) {
            log.info("所有文件上传成功");

            // 返回上传成功的文件访问地址列表
        } else {
            int failedCount = files.length - uploadedFileDetails.size();
            log.error("{}个文件上传失败", failedCount);

            uploadedFileDetails.add("部分文件上传失败");
        }
        return uploadedFileDetails;
    }
}
UploadLinkAddressCollection Collection接口
public interface UploadLinkAddressCollection {

    R linAddressAll();

    R deleteById(Integer id);
}
UploadLinkAddressCollection Collection实现类
// 设置该类下所有方法的统一基础路径
@Slf4j
@RestController
public class uploadLinkAddressCollectionImp implements UploadLinkAddressCollection {

    @Autowired
    private  uploadLinkAddressServiceImp uploadLinkAddressServiceImp; // 上传链接地址业务服务实例


    /**
     * 处理查询所有上传链接地址的POST请求
     *
     * @return R 对象,包含查询结果信息
     */
    @PostMapping("/uploadList")
    @Log // 使用自定义的日志注解记录该方法调用
    public R linAddressAll() {
        // 调用业务层方法获取所有文件链接地址信息
        List<FileLinkAddress> fileLinkAddresses = uploadLinkAddressServiceImp.listByID();

        // 判断查询结果是否为空
        if (!CollectionUtils.isEmpty(fileLinkAddresses)) {
            // 如果查询到数据,则返回成功状态并携带查询结果
            log.info("查询当前用户照片成功");
            return R.success(fileLinkAddresses);
        }

        // 若查询不到数据,则返回失败状态及提示信息
        log.info("查询不到当前用户照片");
        return R.error("查询不到当前用户的照片");

    }

    @DeleteMapping("/deleteId/{id}")
    public R deleteById(@PathVariable Integer id) {
        int i = uploadLinkAddressServiceImp.deleteById(id);
        if (i != 0) {
            return R.success();
        }
        return R.error("删除失败,没有此图片");
    }
}
UploadLinkAddressService service接口
public interface UploadLinkAddressService {

    /**
     * 添加新的文件链接地址到数据库中。这个方法会将指定的用户(通过name字段标识)和其上传文件的访问链接关联起来。
     *
     * @param name        用户名或用户ID,用于标识文件所属的用户
     * @param addressLink 文件的网络访问链接地址
     */
    void addLinkAddress(String name, String addressLink);

    /**
     * 根据用户ID查询该用户的所有已上传文件链接地址列表。
     *
     * @return List<FileLinkAddress> 包含所有与给定用户ID相关联的文件链接地址信息的对象列表
     */
    List<FileLinkAddress> listByID();

    int deleteById(Integer id);
}
FileUtils 类
/**
 * 此类提供文件操作相关的工具方法,包括:
 * 1. 将本地文件转换成字节数组
 * 2. 根据字节数组创建并保存文件
 * 3. 获取MultipartFile对象的文件后缀名
 */
@Slf4j
@Component
public class FileUtils {

    /**
     * 配置默认上传文件的本地存储路径(示例为Windows环境下的路径)
     * 实际应用中可能需要配置成服务器上的路径。
     */


   
    public String "D:/picture";

   
    public static byte[] getBytesByFile(String pathStr) {
        // 获取文件对象
        File file = new File(pathStr);
        // 获取文件输入流
        try (FileInputStream fis = new FileInputStream(file);
             // 创建字节数组输出流
             ByteArrayOutputStream bos = new ByteArrayOutputStream(2048)) {
            // 创建字节数组
            byte[] buffer = new byte[2048];
            int n;
            while ((n = fis.read(buffer)) != -1) {
                bos.write(buffer, 0, n);
            }
            return bos.toByteArray();
        } catch (IOException e) {
            log.error(String.valueOf(e));
            return null;
        }
    }

    /**
     * 将字节数组写入到指定目录下,并以给定的文件名创建新文件
     *
     * @param bytes    要写入文件的字节数组数据
     * @param filePath 目标文件所在目录的路径
     * @param fileName 新建文件的名称(不含扩展名)
     */
    public static void getFileByBytes(byte[] bytes, String filePath, String fileName) {
        BufferedOutputStream bos = null;
        FileOutputStream fos = null;
        File file;
        try {
            // 确保目标目录存在,不存在则创建
            File dir = new File(filePath);
            if (!dir.exists()) {
                dir.mkdirs();
            }
            // 构建完整的文件路径
            file = new File(filePath + "\\" + fileName);

            // 创建输出流并将字节写入到文件
            fos = new FileOutputStream(file);
            bos = new BufferedOutputStream(fos);
            bos.write(bytes);

        } catch (Exception e) {
            log.error(String.valueOf(e));
        } finally {
            // 关闭输出流
            try {
                if (bos != null) {
                    bos.close();
                }
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                log.error(String.valueOf(e));
            }
        }
    }

    /**
     * 从MultipartFile对象中获取原始文件的扩展名
     *
     * @param file Spring框架提供的用于处理上传文件的对象
     * @return 原始文件的扩展名(不包含点),例如 ".jpg" 或 ".txt"
     */
    public static String getFileExtension(MultipartFile file) {
        String originalFileName = file.getOriginalFilename();
        // 使用Objects.requireNonNull防止空指针异常,并截取扩展名部分
        return Objects.requireNonNull(originalFileName).substring(originalFileName.lastIndexOf("."));
    }

    public String getUploadFolder(String id) {
        return UPLOAD_FOLDER + "/" + id;
    }
}
UploadLinkAddressService service实现类
@Service
@Slf4j
public class uploadLinkAddressServiceImp implements UploadLinkAddressService {

    // Spring自动注入HttpServletRequest对象以获取请求信息
    @Autowired
    private  HttpServletRequest request;
    // 注入UploadAddressLinkMapper实例,用于操作数据库
    @Autowired
    private UploadAddressLinkMapper uploadLinkAddressMapper;


    /**
     * 添加新的文件访问链接到数据库,并关联给定的用户名(或用户ID)。
     *
     * @param name        用户名或用户ID标识符
     * @param addressLink 上传文件的访问链接
     */
    @Override
    @Log
    public void addLinkAddress(String name, String addressLink) {
        // 将链接、名称和当前时间戳保存到数据库
        uploadLinkAddressMapper.addressLink(name, addressLink, LocalDateTime.now());
    }

    /**
     * 根据用户ID查询该用户所有已上传文件的链接地址列表。
     *
     * @return List<FileLinkAddress> 包含用户上传文件链接地址信息的对象列表
     */
    @Override
    public List<FileLinkAddress> listByID() {
        Object id;

        try {
            // 解析请求头中的JWT token,提取用户ID
            JWT jwt = JWTUtil.parseToken(request.getHeader("token"));
            jwt.getHeader(TYPE);
            jwt.getPayload("chao"); 

            // 提取并转换用户ID为字符串
            id = jwt.getPayload("id");
            if (id instanceof Number) {
                id = String.valueOf(((Number) id).longValue()); // 根据实际数据类型选择合适的方法
            } else if (id != null) {
                id = id.toString();
            }
        } catch (Exception e) {
            log.error("解析或获取token中的用户ID时出错: {}", e.getMessage());
            return null; // 或者抛出异常,此处假设返回null表示处理失败
        }

        // 查询并返回该用户的所有文件链接地址记录
        return uploadLinkAddressMapper.listById((String) id);
    }


    /**
     * 根据id删除数据
     *
     * @param id 要删除的数据的id
     * @return 删除的行数
     */
    @Override
    public int deleteById(Integer id) {
        return uploadLinkAddressMapper.deleteById(id);

    }
}
R 实现类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class R {
    private Integer code;//响应码,1 代表成功; 0 代表失败
    private String msg;  //响应信息 描述字符串
    private Object data; //返回的数据

    //增删改 成功响应


    public static @NotNull R success() {
        return new R(1, "success" , null);
    }

    //查询 成功响应

    public static @NotNull R success(Object data) {
        return new R(1, "success" , data);
    }

    //失败响应

    public static @NotNull R error(String msg) {
        return new R(0, msg, null);
    }
}
UploadAddressLinkMapper Mapper层
@Mapper
public interface UploadAddressLinkMapper {
    @Insert("insert into fileLink (name, address, create_time)values  (#{name},#{address},#{createTime})")
    void addressLink(String name, String address, LocalDateTime createTime);

    @Select("select * from fileLink where  name=#{id};")
    List<FileLinkAddress> listById(String id);

    @Delete("delete from filelink where id =#{id};")
    int deleteById(Integer id);
}

注意:在数据库建一个 fileLink 表来存储上传文件的路经用户姓名(用户名是JWT中的token获取到的),地址,上传时间

create table if not exists filelink
(
    id          int auto_increment
        primary key,
    name        varchar(1000) not null,
    address     varchar(500)  not null,
    create_time datetime      null
);

在批量处理文件上传的过程中,服务采取了细致入微的错误处理策略。当接收到MultipartFile数组时,会逐一检查每个文件的有效性。若发现空文件,则立即停止上传流程并反馈给调用方。对于每一个待上传的文件,服务执行以下关键步骤:

1. 获取文件内容字节数组。
2. 根据用户ID生成并确保目标存储路径存在,创建必要的目录结构。
3. 为文件生成一个独特的名称,结合UUID和文件扩展名。
4. 使用自定义工具类FileUtils将文件内容写入服务器指定位置。
5. 构建内部访问路径,添加至已上传文件的详细访问地址列表。
6. 将生成的文件访问地址持久化存储至数据库中,便于后续检索和管理。

在完成所有文件上传后,服务会对上传成功的文件数量进行校验,如果与原始提交文件总数一致,则输出成功消息;反之则记录失败次数并提醒用户部分文件上传失败。

总结来说,本篇所探讨的UploadFileServiceImp实现在提供高效稳定的文件上传功能的同时,兼顾了安全性与数据一致性,展示了Spring
Boot在实际开发场景下的强大威力。通过巧妙地整合JWT令牌验证、自定义工具类以及数据库操作,这一服务组件无疑为您的项目增添了独特魅力与实用价值。

题外话

初入计算机行业的人或者大学计算机相关专业毕业生,很多因缺少实战经验,就业处处碰壁。下面我们来看两组数据:

2023届全国高校毕业生预计达到1158万人,就业形势严峻;

国家网络安全宣传周公布的数据显示,到2027年我国网络安全人员缺口将达327万。

一方面是每年应届毕业生就业形势严峻,一方面是网络安全人才百万缺口。

6月9日,麦可思研究2023年版就业蓝皮书(包括《2023年中国本科生就业报告》《2023年中国高职生就业报告》)正式发布。

2022届大学毕业生月收入较高的前10个专业

本科计算机类、高职自动化类专业月收入较高。2022届本科计算机类、高职自动化类专业月收入分别为6863元、5339元。其中,本科计算机类专业起薪与2021届基本持平,高职自动化类月收入增长明显,2022届反超铁道运输类专业(5295元)排在第一位。

具体看专业,2022届本科月收入较高的专业是信息安全(7579元)。对比2018届,电子科学与技术、自动化等与人工智能相关的本科专业表现不俗,较五年前起薪涨幅均达到了19%。数据科学与大数据技术虽是近年新增专业但表现亮眼,已跻身2022届本科毕业生毕业半年后月收入较高专业前三。五年前唯一进入本科高薪榜前10的人文社科类专业——法语已退出前10之列。

“没有网络安全就没有国家安全”。当前,网络安全已被提升到国家战略的高度,成为影响国家安全、社会稳定至关重要的因素之一。

网络安全行业特点

1、就业薪资非常高,涨薪快 2022年猎聘网发布网络安全行业就业薪资行业最高人均33.77万!

img

2、人才缺口大,就业机会多

2019年9月18日《中华人民共和国中央人民政府》官方网站发表:我国网络空间安全人才 需求140万人,而全国各大学校每年培养的人员不到1.5W人。猎聘网《2021年上半年网络安全报告》预测2027年网安人才需求300W,现在从事网络安全行业的从业人员只有10W人。
img

行业发展空间大,岗位非常多

网络安全行业产业以来,随即新增加了几十个网络安全行业岗位︰网络安全专家、网络安全分析师、安全咨询师、网络安全工程师、安全架构师、安全运维工程师、渗透工程师、信息安全管理员、数据安全工程师、网络安全运营工程师、网络安全应急响应工程师、数据鉴定师、网络安全产品经理、网络安全服务工程师、网络安全培训师、网络安全审计员、威胁情报分析工程师、灾难恢复专业人员、实战攻防专业人员…

职业增值潜力大

网络安全专业具有很强的技术特性,尤其是掌握工作中的核心网络架构、安全技术,在职业发展上具有不可替代的竞争优势。

随着个人能力的不断提升,所从事工作的职业价值也会随着自身经验的丰富以及项目运作的成熟,升值空间一路看涨,这也是为什么受大家欢迎的主要原因。

从某种程度来讲,在网络安全领域,跟医生职业一样,越老越吃香,因为技术愈加成熟,自然工作会受到重视,升职加薪则是水到渠成之事。

黑客&网络安全如何学习

今天只要你给我的文章点赞,我私藏的网安学习资料一样免费共享给你们,来看看有哪些东西。

1.学习路线图

行业发展空间大,岗位非常多

网络安全行业产业以来,随即新增加了几十个网络安全行业岗位︰网络安全专家、网络安全分析师、安全咨询师、网络安全工程师、安全架构师、安全运维工程师、渗透工程师、信息安全管理员、数据安全工程师、网络安全运营工程师、网络安全应急响应工程师、数据鉴定师、网络安全产品经理、网络安全服务工程师、网络安全培训师、网络安全审计员、威胁情报分析工程师、灾难恢复专业人员、实战攻防专业人员…

职业增值潜力大

网络安全专业具有很强的技术特性,尤其是掌握工作中的核心网络架构、安全技术,在职业发展上具有不可替代的竞争优势。

随着个人能力的不断提升,所从事工作的职业价值也会随着自身经验的丰富以及项目运作的成熟,升值空间一路看涨,这也是为什么受大家欢迎的主要原因。

从某种程度来讲,在网络安全领域,跟医生职业一样,越老越吃香,因为技术愈加成熟,自然工作会受到重视,升职加薪则是水到渠成之事。

黑客&网络安全如何学习

今天只要你给我的文章点赞,我私藏的网安学习资料一样免费共享给你们,来看看有哪些东西。

1.学习路线图

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

攻击和防守要学的东西也不少,具体要学的东西我都写在了上面的路线图,如果你能学完它们,你去就业和接私活完全没有问题。

2.视频教程

网上虽然也有很多的学习资源,但基本上都残缺不全的,这是我自己录的网安视频教程,上面路线图的每一个知识点,我都有配套的视频讲解。

内容涵盖了网络安全法学习、网络安全运营等保测评、渗透测试基础、漏洞详解、计算机基础知识等,都是网络安全入门必知必会的学习内容。

3.技术文档和电子书

技术文档也是我自己整理的,包括我参加大型网安行动、CTF和挖SRC漏洞的经验和技术要点,电子书也有200多本,由于内容的敏感性,我就不一一展示了。

4.工具包、面试题和源码

“工欲善其事必先利其器”我为大家总结出了最受欢迎的几十款款黑客工具。涉及范围主要集中在 信息收集、Android黑客工具、自动化工具、网络钓鱼等,感兴趣的同学不容错过。

还有我视频里讲的案例源码和对应的工具包,需要的话也可以拿走。

这些题目都是大家在面试深信服、奇安信、腾讯或者其它大厂面试时经常遇到的,如果大家有好的题目或者好的见解欢迎分享。

参考解析:深信服官网、奇安信官网、Freebuf、csdn等

内容特点:条理清晰,含图像化表示更加易懂。

内容概要:包括 内网、操作系统、协议、渗透测试、安服、漏洞、注入、XSS、CSRF、SSRF、文件上传、文件下载、文件包含、XXE、逻辑漏洞、工具、SQLmap、NMAP、BP、MSF…

img

因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

如果你对网络安全入门感兴趣,那么你需要的话可以点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!

  • 35
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值