写点什么

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

作者:Chris Zhang

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


  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. 边界测试

  3. 包含特殊字符的 URL

  4. 带端口号的 URL

  5. 带复杂查询参数的 URL

  6. 时间验证:检查不同时间戳生成的签名是否符合预期

  7. 编码验证:确保 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. 可直接用于生产:已在真实项目中使用


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


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


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); }}
复制代码


发布于: 刚刚阅读数: 7
用户头像

Chris Zhang

关注

还未添加个人签名 2022-07-18 加入

还未添加个人简介

评论

发布
暂无评论
七牛云存储基于时间戳防盗链的算法JAVA实现_Java_Chris Zhang_InfoQ写作社区