You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Java Servlet API(Tomcat)无文件存储的临时txt下载链接方案咨询

无需文件系统的临时下载链接实现方案

嗨,这个场景我在项目里碰过好多次了,完全不用纠结存文件的问题,给你两个最优的实现思路,都是纯内存+签名验证的方式,既不用写磁盘,还能精准控制链接有效期:

方案一:内存缓存+Token验证(适合生成开销大的文件)

如果你的txt文件生成成本很高(比如要批量查询数据库、做复杂数据聚合),那把生成好的文件内容存在内存缓存里,用唯一Token做索引,同时给Token加过期时间是最划算的——既避免重复生成的开销,又不用管文件清理的麻烦。

具体步骤:

  1. 选个靠谱的内存缓存:用Guava Cache或者Caffeine都行,自带过期时间自动管理,完全不用自己写定时任务清理过期内容。
  2. 生成带有效期的Token:每次需要返回下载链接时,生成一个唯一Token(比如UUID),把文件内容、文件名、过期时间一起存到缓存里,Token作为索引key。
  3. 构造下载链接:把Token拼到你的Servlet路径里,比如/api/download?token=xxxx-xxxx-xxxx,把这个链接放到JSON响应的对应字段里。
  4. 下载Servlet处理逻辑:收到下载请求时,先从缓存里取Token对应的内容,验证是否过期,然后直接把内容写到响应流里,全程不碰文件系统。

代码示例:

缓存初始化(用Caffeine)

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

import java.util.concurrent.TimeUnit;

public class DownloadCache {
    // 缓存有效期设为1小时,可根据业务需求调整
    public static final Cache<String, DownloadContent> INSTANCE = Caffeine.newBuilder()
            .expireAfterWrite(1, TimeUnit.HOURS)
            .maximumSize(1000) // 限制缓存最大数量,防止内存溢出
            .build();

    // 存储文件内容的实体类
    public static class DownloadContent {
        private String fileName;
        private byte[] content;

        // 构造器、getter、setter省略
    }
}

生成下载链接的逻辑(在你的业务Servlet里)

import java.util.UUID;

// 模拟你的业务逻辑:生成txt文件内容
byte[] txtContent = generateYourTxtContent();
String token = UUID.randomUUID().toString();

// 将内容存入缓存
DownloadCache.INSTANCE.put(token, new DownloadCache.DownloadContent("export-data.txt", txtContent));

// 构造下载链接,放到JSON响应中
String downloadUrl = "/api/download?token=" + token;

下载Servlet的处理逻辑

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/api/download")
public class DownloadServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String token = req.getParameter("token");
        if (token == null) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "缺少下载凭证");
            return;
        }

        DownloadCache.DownloadContent content = DownloadCache.INSTANCE.getIfPresent(token);
        if (content == null) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "链接已过期或无效");
            return;
        }

        // 设置响应头,告诉浏览器这是可下载的文件
        resp.setContentType("text/plain");
        resp.setHeader("Content-Disposition", "attachment; filename=\"" + content.getFileName() + "\"");
        resp.setContentLength(content.getContent().length);

        // 直接把内容写入响应流
        resp.getOutputStream().write(content.getContent());
        resp.getOutputStream().flush();
    }
}

方案二:动态生成+签名防篡改(适合生成开销小的文件)

如果你的txt文件可以通过参数快速生成(比如根据ID查单条记录拼接内容),那连缓存都不用存——直接把生成文件所需的参数、过期时间做个加密签名,把参数和签名拼到下载链接里,请求下载时验证签名和过期时间,再实时生成文件内容输出。

核心优势:

  • 完全不占内存,不用担心缓存溢出问题
  • 链接自带防篡改机制,别人修改参数也无法下载

代码示例:

签名工具类

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Date;

public class SignatureUtils {
    // 密钥存在配置文件/环境变量里,绝对不能硬编码!
    private static final String SECRET_KEY = "your-strong-secret-key-here";
    private static final String HMAC_ALGORITHM = "HmacSHA256";

    // 生成签名:参数+过期时间+密钥
    public static String generateSignature(String params, long expireTime) throws NoSuchAlgorithmException, InvalidKeyException {
        String data = params + ":" + expireTime;
        Mac mac = Mac.getInstance(HMAC_ALGORITHM);
        mac.init(new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM));
        byte[] signatureBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getUrlEncoder().encodeToString(signatureBytes);
    }

    // 验证签名有效性
    public static boolean verifySignature(String params, long expireTime, String signature) throws NoSuchAlgorithmException, InvalidKeyException {
        String expectedSignature = generateSignature(params, expireTime);
        return expectedSignature.equals(signature);
    }
}

生成带签名的下载链接(在业务Servlet里)

public String generateDownloadUrl(String dataId) throws NoSuchAlgorithmException, InvalidKeyException {
    // 设置链接有效期为1小时
    long expireTime = new Date().getTime() + 3600 * 1000;
    String params = "dataId=" + dataId;
    String signature = SignatureUtils.generateSignature(params, expireTime);
    // 构造带参数、过期时间、签名的下载链接
    return "/api/download-dynamic?" + params + "&expire=" + expireTime + "&signature=" + signature;
}

动态下载的Servlet逻辑

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Date;

@WebServlet("/api/download-dynamic")
public class DynamicDownloadServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String dataId = req.getParameter("dataId");
        String expireStr = req.getParameter("expire");
        String signature = req.getParameter("signature");

        if (dataId == null || expireStr == null || signature == null) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "参数不全");
            return;
        }

        try {
            long expireTime = Long.parseLong(expireStr);
            // 先检查链接是否过期
            if (new Date().getTime() > expireTime) {
                resp.sendError(HttpServletResponse.SC_GONE, "链接已过期");
                return;
            }

            // 验证签名,防止参数被篡改
            String params = "dataId=" + dataId;
            if (!SignatureUtils.verifySignature(params, expireTime, signature)) {
                resp.sendError(HttpServletResponse.SC_FORBIDDEN, "签名无效");
                return;
            }

            // 根据参数实时生成txt内容
            byte[] txtContent = generateTxtContentByDataId(dataId);

            // 设置响应头并输出内容
            resp.setContentType("text/plain");
            resp.setHeader("Content-Disposition", "attachment; filename=\"dynamic-data.txt\"");
            resp.getOutputStream().write(txtContent);
            resp.getOutputStream().flush();
        } catch (Exception e) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "参数无效");
        }
    }

    // 模拟根据参数生成文件内容的业务方法
    private byte[] generateTxtContentByDataId(String dataId) {
        return ("数据ID: " + dataId + "\n生成时间: " + new Date()).getBytes(StandardCharsets.UTF_8);
    }
}

注意事项

  • 签名密钥要安全:密钥一定要存在配置文件或者环境变量里,绝对不能硬编码到代码中,防止泄露。
  • 缓存容量要限制:使用方案一时,务必给缓存设置最大容量,避免内存溢出。
  • 响应头要正确:下载时记得设置Content-DispositionContentType,否则浏览器可能直接显示内容而不是触发下载。

内容的提问来源于stack exchange,提问作者lucy

火山引擎 最新活动