偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

別怕泄露!」Spring Boot 秒生成簽名 URL,輕松搞定私有文件安全訪問!

開發(fā) 前端
簽名 URL 的本質(zhì),是將?請求方法、資源路徑、過期時間?等核心信息組合后,通過?加密簽名算法(如 HMAC-SHA256)計算出校驗值。?只有在簽名校驗通過、并且未過期時,才能訪問對應的私有文件。

在現(xiàn)代應用系統(tǒng)中,文件訪問是幾乎繞不開的功能點。無論是用戶上傳的頭像、合同 PDF,還是后臺生成的報表文件,系統(tǒng)都需要考慮如何在保證 安全 的前提下,實現(xiàn) 便捷訪問。

僅依賴用戶身份認證有時并不足夠,因為某些場景下,我們需要給外部系統(tǒng)或臨時用戶開放有限時間的訪問權(quán)限,而不可能為其建立長期有效的賬號和密碼。此時,簽名 URL(Signed URL)便成為最佳選擇。

簽名 URL 具備以下兩個關鍵特征:

  1. 帶有過期時間:一旦時間到期,鏈接自動失效,避免長期暴露。
  2. 包含數(shù)字簽名:只有服務端能生成正確的簽名,客戶端無法偽造,確保鏈接可信。

結(jié)合 Spring Boot 提供的靈活配置與加密工具,我們可以非常高效地實現(xiàn)這一機制。本文將帶你逐步完成從配置、簽名生成、文件驗證到前端測試頁面的完整流程。

簽名 URL 基礎機制

簽名 URL 的設計思路

簽名 URL 的本質(zhì),是將 請求方法、資源路徑、過期時間 等核心信息組合后,通過 加密簽名算法(如 HMAC-SHA256)計算出校驗值。 只有在簽名校驗通過、并且未過期時,才能訪問對應的私有文件。

這種設計有兩個顯著優(yōu)點:

  • 無需額外賬號體系:直接通過 URL 控制訪問權(quán)限。
  • 輕量安全:過期時間 + 簽名雙重保護,有效防止鏈接被篡改或長期傳播。

簽名 URL 的結(jié)構(gòu)

簽名 URL 的樣子和普通 HTTP 鏈接差不多,只是附帶了額外參數(shù):

https://oss.example.com/photos/architecture.png?expires=1755990064&sign=sefxxfx

關鍵參數(shù)解釋:

  • expires:Unix 時間戳,表示鏈接過期時間。
  • sign:加密簽名,確保鏈接未被篡改。

服務器端會在收到請求時:

  1. 檢查當前時間是否超過 expires;
  2. 使用同樣的算法重新計算簽名,和 sign 對比。

Spring Boot 實戰(zhàn)

下面我們基于 Spring Boot 來實現(xiàn)簽名 URL 生成與驗證

src/main/java/com/icoderoad/security/signurl
├── config
│   └── LinkProperties.java
├── util
│   └── SignatureUtil.java
├── service
│   └── LinkService.java
└── controller
    └── FileAccessController.java

配置類

package com.icoderoad.security.signurl.config;


import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;


@Component
@ConfigurationProperties(prefix = "pack.app")
public class LinkProperties {


    private String secretKey;
    private String algs;
    private long lifetimeSeconds;
    private String method;
    private String accessPath;


    // getters & setters
}

application.yml 配置:

pack:
  app:
    algs: HmacSHA256
    lifetime-seconds: 1800
    method: get
    secret-key: aaaabbbbccccdddd
    accessPath: /files

簽名工具類

package com.icoderoad.security.signurl.util;


import com.icoderoad.security.signurl.config.LinkProperties;
import org.springframework.stereotype.Component;


import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;


@Component
public class SignatureUtil {


    private final LinkProperties linkProperties;
    private final byte[] secret;


    public SignatureUtil(LinkProperties linkProperties) {
        this.linkProperties = linkProperties;
        this.secret = linkProperties.getSecretKey().getBytes(StandardCharsets.UTF_8);
    }


    public String signPath(String method, String path, long expires) throws Exception {
        String data = method + "|" + path + "|" + expires;
        String HMAC = linkProperties.getAlgs();
        Mac mac = Mac.getInstance(HMAC);
        mac.init(new SecretKeySpec(secret, HMAC));
        byte[] raw = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getUrlEncoder().withoutPadding().encodeToString(raw);
    }
}

簽名 URL 服務類

package com.icoderoad.security.signurl.service;


import com.icoderoad.security.signurl.config.LinkProperties;
import com.icoderoad.security.signurl.util.SignatureUtil;
import org.springframework.stereotype.Service;


import java.time.ZonedDateTime;


@Service
public class LinkService {


    private final SignatureUtil signatureUtil;
    private final LinkProperties linkProperties;


    public LinkService(SignatureUtil signatureUtil, LinkProperties linkProperties) {
        this.signatureUtil = signatureUtil;
        this.linkProperties = linkProperties;
    }


    public String generateLink(String filePath) throws Exception {
        String canonicalPath = filePath.startsWith("/") ? filePath : "/" + filePath;
        long expiresAt = ZonedDateTime.now()
                .plusSeconds(linkProperties.getLifetimeSeconds())
                .toEpochSecond();
        String signature = signatureUtil.signPath(linkProperties.getMethod(), canonicalPath, expiresAt);
        return String.format("/%s%s?expires=%d&sign=%s",
                linkProperties.getAccessPath().replaceFirst("^/", ""),
                canonicalPath, expiresAt, signature);
    }
}

文件訪問控制器

package com.icoderoad.security.signurl.controller;


import com.icoderoad.security.signurl.config.LinkProperties;
import com.icoderoad.security.signurl.service.LinkService;
import com.icoderoad.security.signurl.util.SignatureUtil;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;


import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.file.*;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.*;


@Controller
@RequestMapping("${pack.app.accessPath:/files}")
public class FileAccessController {


    private final SignatureUtil signatureUtil;
    private final LinkService linkService;
    private final LinkProperties linkProperties;


    public FileAccessController(SignatureUtil signatureUtil, LinkService linkService,
                                LinkProperties linkProperties) {
        this.signatureUtil = signatureUtil;
        this.linkService = linkService;
        this.linkProperties = linkProperties;
    }


    /** 展示頁面,生成文件簽名鏈接 */
    @GetMapping("")
    public String generateLinksForDirectory(Model model) throws Exception {
        String directoryPath = "/opt/data/images";
        List<String> links = new ArrayList<>();
        Path dirPath = Paths.get(directoryPath);


        if (Files.exists(dirPath) && Files.isDirectory(dirPath)) {
            Files.list(dirPath).filter(Files::isRegularFile).forEach(file -> {
                try {
                    String relativePath = dirPath.relativize(file).toString().replace("\\", "/");
                    links.add("http://localhost:8080" + linkService.generateLink(relativePath));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
        model.addAttribute("links", links);
        return "preview";
    }


    /** 訪問文件接口 */
    @GetMapping("/{*path}")
    public void fetchFile(@PathVariable("path") String path,
                          @RequestParam long expires,
                          @RequestParam String sign,
                          HttpServletResponse response) throws Exception {


        long now = Instant.now().getEpochSecond();
        if (now >= expires) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN, "鏈接已過期");
            return;
        }


        String expected = signatureUtil.signPath(linkProperties.getMethod(), path, expires);
        byte[] expectedBytes = Base64.getUrlDecoder().decode(expected);
        byte[] providedBytes = Base64.getUrlDecoder().decode(sign);


        if (!MessageDigest.isEqual(expectedBytes, providedBytes)) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN, "無效鏈接");
            return;
        }


        Path filePath = Paths.get("/opt/data/images/", path).normalize();
        Resource resource = new UrlResource(filePath.toUri());
        if (!resource.exists()) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件不存在");
            return;
        }


        String contentType = determineContentType(path);
        response.setContentType(contentType);
        Files.copy(resource.getFile().toPath(), response.getOutputStream());
        response.getOutputStream().flush();
    }


    private String determineContentType(String path) {
        if (path == null || !path.contains(".")) {
            return MediaType.APPLICATION_OCTET_STREAM_VALUE;
        }
        String extension = path.substring(path.lastIndexOf(".") + 1).toLowerCase();
        return switch (extension) {
            case "png" -> MediaType.IMAGE_PNG_VALUE;
            case "jpg", "jpeg" -> MediaType.IMAGE_JPEG_VALUE;
            case "pdf" -> MediaType.APPLICATION_PDF_VALUE;
            case "txt" -> MediaType.TEXT_PLAIN_VALUE;
            case "html" -> MediaType.TEXT_HTML_VALUE;
            default -> MediaType.APPLICATION_OCTET_STREAM_VALUE;
        };
    }
}

前端頁面(Thymeleaf + Bootstrap)

src/main/resources/templates/preview.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>簽名 URL 文件預覽</title>
    <link  rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-5">
    <h2 class="mb-4 text-center">簽名 URL 文件訪問測試</h2>


    <div class="card shadow-sm p-4">
        <h5 class="mb-3">生成的文件鏈接:</h5>
        <ul class="list-group">
            <li th:each="link : ${links}" class="list-group-item d-flex justify-content-between align-items-center">
                <span th:text="${link}"></span>
                <a th:href="${link}" class="btn btn-primary btn-sm" target="_blank">訪問</a>
            </li>
        </ul>
    </div>
</div>
</body>
</html>

效果:

  • 頁面會列出 /opt/data/images 目錄下的所有文件簽名 URL;
  • 點擊右側(cè)按鈕即可直接測試訪問。

測試流程

  1. 在 /opt/data/images 放入若干文件(jpg/png/pdf/txt)。
  2. 啟動 Spring Boot 項目。
  3. 瀏覽器訪問:
http://localhost:8080/files

頁面會展示簽名 URL 列表,點擊即可驗證訪問是否成功。

結(jié)論

通過本文完整實戰(zhàn),我們實現(xiàn)了 簽名 URL 的后端生成 + 前端預覽:

  • 后端負責安全計算簽名、校驗過期時間,保證文件訪問的合規(guī)性;
  • 前端通過 Thymeleaf + Bootstrap 渲染文件列表,用戶可以一鍵點擊測試。

這種方式既簡潔又高效,尤其適合需要 臨時文件分享 的業(yè)務場景,比如:

  • 生成臨時下載地址
  • 限時訪問合同、賬單、報表
  • 文件分享的安全保護

未來如果你要在生產(chǎn)環(huán)境結(jié)合 OSS/S3/CDN,只需要替換文件存儲目錄和 URL 生成規(guī)則即可無縫擴展。

責任編輯:武曉燕 來源: 路條編程
相關推薦

2024-08-09 08:52:26

2025-04-03 07:56:08

電子簽名合同系統(tǒng)Spring

2025-03-03 08:00:00

SpringBootEasyExcel數(shù)據(jù)導出

2025-04-10 08:03:31

Spring系統(tǒng)

2025-02-17 00:00:45

接口支付寶沙箱

2025-04-08 03:00:00

SpringDocker容器

2010-07-27 14:25:02

linux文件編碼

2020-04-23 15:59:04

SpringKafka集群

2021-09-30 06:31:12

Spring Boot配置密碼

2022-06-23 08:42:08

配置加密解密

2023-05-23 14:53:26

鴻蒙應用開發(fā)

2013-04-01 10:56:02

2009-08-17 08:45:34

Windows 7文件刪除

2025-05-13 07:13:25

2019-07-09 08:23:07

數(shù)據(jù)安全旅游網(wǎng)絡安全

2009-11-13 17:32:37

2025-10-09 02:20:00

2010-03-15 12:50:19

Python文件夾創(chuàng)建

2018-06-28 15:58:04

PDF

2009-12-11 15:37:58

Linux日志處理
點贊
收藏

51CTO技術棧公眾號