七牛云存储基于时间戳防盗链的算法JAVA实现

在开始分享我的实现之前,不得不先抨击一下当前技术文章领域的一个普遍现象:天下文章一般抄。在准备实现七牛云时间戳防盗链功能时,我搜索了大量中文资料,发现一个令人沮丧的事实:

  1. 90%的文章内容雷同,甚至连错误都一样
  2. 几乎没有验证过程,直接搬运官方文档或他人代码
  3. 代码示例漏洞百出,无法实际运行
  4. 关键细节缺失,如URL编码的特殊处理、时间戳的16进制转换等

最令人震惊的是,这些文章被不断转载,错误被不断放大,形成了一个"错误信息闭环"。开发者按照这些文章实现后,发现无法正常工作,却找不到原因。

七牛官方文档

https://developer.qiniu.com/fusion/3841/timestamp-hotlinking-prevention-fusion

一个经过验证的正确实现

与上述情况不同,今天我分享的是一个经过严格验证的Java实现。这个实现:

  1. 与七牛官方工具生成的签名进行逐字节对比,确保完全一致
  2. 处理了所有边界情况,如URL中的特殊字符、端口处理等
  3. 代码清晰可读,有完整的注释和文档
  4. 可直接用于生产环境,已在真实项目中验证
    在这里插入图片描述

核心算法解析

1. URL编码处理

public static String urlEncode(String s) {
    try {
        return URLEncoder.encode(s, StandardCharsets.UTF_8.name())
                .replace("+", "%20")
                .replace("%2F", "/");
    } catch (Exception e) {
        return s;
    }
}

关键点

  • 使用标准URL编码
  • 将"+“替换为”%20"(七牛特殊要求)
  • 保留"/"不编码(与一般URL编码不同)

2. 时间戳转换

public static String toHexLower(long timestamp) {
    return Long.toHexString(timestamp);
}

将十进制时间戳转为16进制小写字符串,这是七牛防盗链的特殊要求。

3. MD5签名计算

public static String md5Hex(String s) {
    try {
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] bytes = md.digest(s.getBytes(StandardCharsets.UTF_8));
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b & 0xff));
        }
        return sb.toString();
    } catch (Exception e) {
        throw new RuntimeException("MD5计算失败", e);
    }
}

标准的MD5计算,确保结果为32位小写16进制字符串。

4. 签名生成

public static String sign(String key, String t, String path) {
    String toSign = key + urlEncode(path) + t;
    return "sign=" + md5Hex(toSign) + "&t=" + t;
}

拼接密钥、编码后的路径和16进制时间戳,计算MD5作为签名。

5. 完整URL生成

public static String signUrl(String url, String key, long deadline) {
    try {
        String t = toHexLower(deadline);
        URL u = new URL(url);
        
        String path = u.getPath();
        String query = u.getQuery();

        String signPart = sign(key, t, path);

        String newQuery = (query == null || query.isEmpty()) ? signPart : query + "&" + signPart;

        StringBuilder sb = new StringBuilder();
        sb.append(u.getProtocol()).append("://").append(u.getHost());
        if (u.getPort() != -1) {
            sb.append(":").append(u.getPort());
        }
        sb.append(urlEncode(path)).append("?").append(newQuery);

        return sb.toString();
    } catch (Exception e) {
        throw new RuntimeException("签名URL生成失败", e);
    }
}

处理原始URL的各个部分,确保生成的签名URL格式正确。

验证方法

为确保实现的正确性,我采用了以下验证方法:

  1. 与七牛官方工具对比:使用相同参数生成签名URL,确保完全一致
  2. 边界测试
    • 包含特殊字符的URL
    • 带端口号的URL
    • 带复杂查询参数的URL
  3. 时间验证:检查不同时间戳生成的签名是否符合预期
  4. 编码验证:确保URL编码处理符合七牛要求

使用示例

public static void main(String[] args) {
    String key = "你的七牛密钥";
    String url = "http://xxx.yyy.com/DIR1/dir2/vodfile.mp4?v=1.1";
    long deadline = getNextHourTimestamp();
    System.out.println("过期时间:" + deadline);

    String signedUrl = signUrl(url, key, deadline);
    System.out.println("签名后的URL: " + signedUrl);
}

在这里插入图片描述

总结

与网上大量未经验证的"搬运"文章不同,本文分享的实现:

  1. 完全可运行:代码完整,无缺失部分
  2. 经过严格验证:与官方工具对比一致
  3. 处理所有边界情况:考虑到了各种URL格式
  4. 可直接用于生产:已在真实项目中使用

希望这个实现能帮助开发者避免踩坑,也呼吁技术写作者:请验证你的代码再分享,不要成为错误信息的传播者。

:完整代码如下所示,可直接复制使用。如有任何问题,欢迎讨论交流。

package cn.zhangyou710.qiniu;

import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.ZoneId;
import java.time.ZonedDateTime;

/**
 * @author ZhangYou
 * @description
 * @date 2025/6/3
 */
public class QiniuUrlTimestampSigner {

    // URL encode,斜杠不编码
    public static String urlEncode(String s) {
        try {
            return URLEncoder.encode(s, StandardCharsets.UTF_8.name())
                    .replace("+", "%20")
                    .replace("%2F", "/");
        } catch (Exception e) {
            // 不应发生,直接返回原字符串
            return s;
        }
    }

    // 将十进制时间戳转16进制小写字符串
    public static String toHexLower(long timestamp) {
        return Long.toHexString(timestamp);
    }

    // MD5签名计算
    public static String md5Hex(String s) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] bytes = md.digest(s.getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            for (byte b : bytes) {
                sb.append(String.format("%02x", b & 0xff));
            }
            return sb.toString();
        } catch (Exception e) {
            throw new RuntimeException("MD5计算失败", e);
        }
    }

    // 生成签名参数部分 sign=xxx&t=xxx
    public static String sign(String key, String t, String path) {
        String toSign = key + urlEncode(path) + t;
        return "sign=" + md5Hex(toSign) + "&t=" + t;
    }

    // 根据URL和过期时间生成带签名的URL
    public static String signUrl(String url, String key, long deadline) {
        try {
            String t = toHexLower(deadline);
            System.out.println("16进制时间格式:" + t);
            URL u = new URL(url);

            String path = u.getPath();
            String query = u.getQuery();

            String signPart = sign(key, t, path);

            String newQuery = (query == null || query.isEmpty()) ? signPart : query + "&" + signPart;

            // 拼接完整URL,包含端口判断
            StringBuilder sb = new StringBuilder();
            sb.append(u.getProtocol()).append("://").append(u.getHost());
            if (u.getPort() != -1) {
                sb.append(":").append(u.getPort());
            }
            sb.append(urlEncode(path)).append("?").append(newQuery);

            return sb.toString();

        } catch (Exception e) {
            throw new RuntimeException("签名URL生成失败", e);
        }
    }

    public static long getNextHourTimestamp() {
        ZonedDateTime now = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
        ZonedDateTime nextHour = now.plusHours(1).withMinute(0).withSecond(0).withNano(0);
        return nextHour.toEpochSecond();
    }


    // 简单调用示例
    public static void main(String[] args) {
        String key = "db883caa36aab4b82cbac7aac7b9efaba29908b9";
        String url = "http://xxx.yyy.com/DIR1/dir2/vodfile.mp4?v=1.1";
        long deadline = getNextHourTimestamp();
        System.out.println("过期时间:" + deadline);

        String signedUrl = signUrl(url, key, deadline);
        System.out.println("签名后的URL: " + signedUrl);
    }
}

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐