为保护资源被滥用,CDN 配置 Referer 和 IP 的黑白名单来判断是否是合法用户及来源。然后 Referer 可被 轻松伪造,实际使用中仍存在隐患。为了更好地保护源站资源,CDN 支持加密 URL 的功能,可以通过 高级鉴权 的方式来正确响应合法请求、拒绝非法请求。
工作原理
在控制台为需要保护的 “加速项目” 启用“高级鉴权”,并设置一个名为鉴权 Key 的“暗号”,利用与 CDN 同样策略的方式向合法用户提供利加密的 URL,并通过 ts 参数设定该 URL 的有效时间戳;
用户在访问资源时会根据下面的 规则 判断请求的合法性:
1
是否有签名
- 是 -
继续判断下一条规则 - 否 -
返回 403 Forbidden;
2
当前是否早于 _ts 参数规定的时间戳
- 是 -
继续判断下一条规则 - 否 -
返回 403 Forbidden;
3
判断 _btf_tk 是否合法(防止 _ts 被伪造)
- 是 -
200 并返回资源 - 否 -
返回 403 Forbidden。
典型范例
一个正确的典型高级防盗链 URL 应该类似于:常规 URL:http://tests3origin.dogecast.com/image.jpg?其中:_btf_tk=2e1864801d8cb957a9e40b59ca4321d7&_ts=1701091625 参数 URL:http://tests3origin.dogecast.com/image.jpg?_btf_tk=d843dd7aa694772a35389d8df73fdeb1&_ts=1701091625&fmt=info
- ts: 为 UTC 时间戳,意为:该 URL 的时效时间(晚于该时间 后链接将返回 403)
- token(常规 URL): 用 tokenKey + path (本例为 /image.jpg) + ts (本例为 1701091625) 计算 MD5
- token(参数 URL): 用 tokenKey + path (本例为 /image.jpg) + ? + args (本例为 _ts=1701091625fmt=info) 计算 MD5
参数 URL 签名规则说明
- args 由
_ts=时间戳开头,后面依次拼接其余参数(_btf_tk本身不参与),参数之间 不加&; - URL 中的参数顺序须与签名时的顺序一致,参数之间正常使用
&连接; - 参数值须先 URL 编码(如
;、"、空格、中文),且签名与 URL 中使用 同一个 编码结果。
代码示例:
下面是部分主流语言实现上述 典型范例 中的高级鉴权 URL 的示例代码,每个示例都会同时输出 常规 URL 和 参数 URL。 它们应该可在各个 playground 中被正确执行。
安全提示:下面虽然列出了部分客户端语言的示例,但缤纷云建议您永远 只在服务端生成 高级鉴权链接并分发给客户端。
因为这可以:
- 最大程度保护 tokenKey 的安全;
- 更高效、安全地随时替换 tokenKey(不必迫使用户更新客户端)。
PHP 语言版本
<?php
function createLink($fileName, $params = []) {
$deadLine = time() + 60; # 链接在未来的 60秒 内有效
$tokenKey = "testsecretkey"; # 缤纷云后台设置的 鉴权Key
$domain = "http://tests3origin.dogecast.com";
$args = "";
$query = "";
foreach ($params as $k => $v) {
$pair = $k . "=" . rawurlencode($v);
$args .= $pair; # 签名:参数之间不加 &
$query .= "&" . $pair; # URL:参数之间用 & 连接
}
$md5RawString = empty($params)
? $tokenKey . $fileName . $deadLine
: $tokenKey . $fileName . "?_ts=" . $deadLine . $args;
$md5Result = md5($md5RawString);
return $domain . $fileName . "?_btf_tk=" . $md5Result . "&_ts=" . $deadLine . $query;
}
$path = "/image.jpg";
echo "常规 URL:", createLink($path), "\n";
echo "参数 URL:", createLink($path, ["fmt" => "info"]), "\n";
Go 语言版本
time.Now().Unix() 有效性提示
- 大部分 Golang playground 并不支持获取时间戳 ———— time.Now().Unix() 将会得到一个错误的早期时间;
- 请注意选择部分第三方支持该特性的 Golang playground;
- 以下 golang 示例代码可以在 Programiz 得到正确地执行。
package main
import (
"crypto/md5"
"encoding/hex"
"fmt"
"net/url"
"strings"
"time"
)
type Param struct{ Key, Value string }
func encode(v string) string {
return strings.ReplaceAll(url.QueryEscape(v), "+", "%20")
}
func createLink(fileName string, params []Param) string {
deadLine := time.Now().Unix() + 60 // 链接在未来的 60秒 内有效
tokenKey := "testsecretkey" // 缤纷云后台设置的 鉴权Key
domain := "http://tests3origin.dogecast.com"
args, query := "", ""
for _, p := range params {
pair := p.Key + "=" + encode(p.Value)
args += pair // 签名:参数之间不加 &
query += "&" + pair // URL:参数之间用 & 连接
}
var rawString string
if len(params) == 0 {
rawString = fmt.Sprintf("%s%s%d", tokenKey, fileName, deadLine)
} else {
rawString = fmt.Sprintf("%s%s?_ts=%d%s", tokenKey, fileName, deadLine, args)
}
sum := md5.Sum([]byte(rawString))
md5Result := hex.EncodeToString(sum[:])
return fmt.Sprintf("%s%s?_btf_tk=%s&_ts=%d%s", domain, fileName, md5Result, deadLine, query)
}
func main() {
path := "/image.jpg"
fmt.Println("常规 URL:", createLink(path, nil))
fmt.Println("参数 URL:", createLink(path, []Param{{"fmt", "info"}}))
}
Python3 语言版本
import hashlib
import time
from urllib.parse import quote
def create_link(fileName, params=None):
params = params or {}
deadLine = int(time.time()) + 60 # 链接在未来的 60秒 内有效
tokenKey = "testsecretkey" # 缤纷云后台设置的 鉴权Key
domain = "http://tests3origin.dogecast.com"
pairs = [f"{k}={quote(str(v), safe='')}" for k, v in params.items()]
if pairs:
rawString = f"{tokenKey}{fileName}?_ts={deadLine}{''.join(pairs)}" # 签名:参数之间不加 &
else:
rawString = f"{tokenKey}{fileName}{deadLine}"
md5Result = hashlib.md5(rawString.encode()).hexdigest()
query = "".join("&" + p for p in pairs) # URL:参数之间用 & 连接
return f"{domain}{fileName}?_btf_tk={md5Result}&_ts={deadLine}{query}"
path = "/image.jpg"
print("常规 URL:", create_link(path))
print("参数 URL:", create_link(path, {"fmt": "info"}))
JS 语言版本
const crypto = require('crypto');
function createLink(fileName, params = {}) {
let deadLine = Math.floor(Date.now() / 1000) + 60; // 链接在未来的 60秒 内有效
let tokenKey = "testsecretkey"; // 缤纷云后台设置的 鉴权Key
let domain = "http://tests3origin.dogecast.com";
let pairs = Object.entries(params).map(([k, v]) => `${k}=${encodeURIComponent(v)}`);
let rawString = pairs.length
? tokenKey + fileName + "?_ts=" + deadLine + pairs.join("") // 签名:参数之间不加 &
: tokenKey + fileName + deadLine;
let md5Result = crypto.createHash('md5').update(rawString).digest('hex');
let query = pairs.map(p => "&" + p).join(""); // URL:参数之间用 & 连接
return domain + fileName + "?_btf_tk=" + md5Result + "&_ts=" + deadLine + query;
}
const path = "/image.jpg";
console.log("常规 URL:", createLink(path));
console.log("参数 URL:", createLink(path, { "fmt": "info" }));
Java 语言版本
import java.math.BigInteger;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
String path = "/image.jpg";
Map<String, String> params = new LinkedHashMap<>();
params.put("fmt", "info");
System.out.println("常规 URL: " + createLink(path, Collections.emptyMap()));
System.out.println("参数 URL: " + createLink(path, params));
}
public static String createLink(String fileName, Map<String, String> params) {
long deadline = Instant.now().getEpochSecond() + 60; // 链接在未来的 60秒 内有效
String tokenKey = "testsecretkey"; // 缤纷云后台设置的 鉴权Key
String domain = "http://tests3origin.dogecast.com";
StringBuilder extraArgs = new StringBuilder();
StringBuilder query = new StringBuilder();
for (Map.Entry<String, String> e : params.entrySet()) {
String pair = e.getKey() + "=" + encode(e.getValue());
extraArgs.append(pair); // 签名:参数之间不加 &
query.append("&").append(pair); // URL:参数之间用 & 连接
}
String rawString = params.isEmpty()
? tokenKey + fileName + deadline
: tokenKey + fileName + "?_ts=" + deadline + extraArgs;
String md5Result = getMd5(rawString);
return domain + fileName + "?_btf_tk=" + md5Result + "&_ts=" + deadline + query;
}
public static String encode(String v) {
return URLEncoder.encode(v, StandardCharsets.UTF_8).replace("+", "%20");
}
public static String getMd5(String input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] messageDigest = md.digest(input.getBytes(StandardCharsets.UTF_8));
BigInteger no = new BigInteger(1, messageDigest);
String hashtext = no.toString(16);
while (hashtext.length() < 32) {
hashtext = "0" + hashtext;
}
return hashtext;
}
catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
rust 语言版本
extern crate chrono;
extern crate crypto;
use chrono::prelude::*;
use crypto::digest::Digest;
use crypto::md5::Md5;
use urlencoding::encode;
fn create_link(file_name: &str, params: &[(&str, &str)]) -> String {
let deadline = Utc::now().timestamp() + 60; // 链接在未来的 60秒 内有效
let token_key = "testsecretkey"; // 缤纷云后台设置的 鉴权Key
let domain = "http://tests3origin.dogecast.com";
let pairs: Vec<String> = params
.iter()
.map(|(k, v)| format!("{}={}", k, encode(v)))
.collect();
let raw_string = if pairs.is_empty() {
format!("{}{}{}", token_key, file_name, deadline)
} else {
format!("{}{}?_ts={}{}", token_key, file_name, deadline, pairs.concat()) // 签名:参数之间不加 &
};
let mut hasher = Md5::new();
hasher.input_str(&raw_string);
let md5_result = hasher.result_str();
let query: String = pairs.iter().map(|p| format!("&{}", p)).collect(); // URL:参数之间用 & 连接
format!("{}{}?_btf_tk={}&_ts={}{}", domain, file_name, md5_result, deadline, query)
}
fn main() {
let path = "/image.jpg";
println!("常规 URL: {}", create_link(path, &[]));
println!("参数 URL: {}", create_link(path, &[("fmt", "info")]));
}
[dependencies]
chrono = "0.4"
rust-crypto = "0.2"
urlencoding = "2"
swift 语言版本
import CommonCrypto
import Foundation
func MD5(string: String) -> String {
let length = Int(CC_MD5_DIGEST_LENGTH)
var digest = [UInt8](repeating: 0, count: length)
if let data = string.data(using: String.Encoding.utf8) {
data.withUnsafeBytes { _ = CC_MD5($0.baseAddress, CC_LONG(data.count), &digest) }
}
return digest.map { String(format: "%02x", $0) }.joined()
}
func encode(_ v: String) -> String {
let allowed = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~")
return v.addingPercentEncoding(withAllowedCharacters: allowed) ?? v
}
func createLink(_ fileName: String, _ params: [(String, String)] = []) -> String {
let deadline = Int(Date().timeIntervalSince1970) + 60
let tokenKey = "testsecretkey"
let domain = "http://tests3origin.dogecast.com"
let pairs = params.map { "\($0.0)=\(encode($0.1))" }
let rawString = pairs.isEmpty
? "\(tokenKey)\(fileName)\(deadline)"
: "\(tokenKey)\(fileName)?_ts=\(deadline)\(pairs.joined())" // 签名:参数之间不加 &
let md5Result = MD5(string: rawString)
let query = pairs.map { "&\($0)" }.joined() // URL:参数之间用 & 连接
return "\(domain)\(fileName)?_btf_tk=\(md5Result)&_ts=\(deadline)\(query)"
}
let path = "/image.jpg"
print("常规 URL:", createLink(path))
print("参数 URL:", createLink(path, [("fmt", "info")]))
kotlin 语言版本
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.time.Instant
fun encode(v: String): String = URLEncoder.encode(v, StandardCharsets.UTF_8).replace("+", "%20")
fun createLink(fileName: String, params: List<Pair<String, String>> = emptyList()): String {
val deadline = Instant.now().epochSecond + 60
val tokenKey = "testsecretkey"
val domain = "http://tests3origin.dogecast.com"
val pairs = params.map { (k, v) -> "$k=${encode(v)}" }
val rawString = if (pairs.isEmpty()) {
"$tokenKey$fileName$deadline"
} else {
"$tokenKey$fileName?_ts=$deadline${pairs.joinToString("")}" // 签名:参数之间不加 &
}
val md = MessageDigest.getInstance("MD5")
md.update(rawString.toByteArray())
val md5Result = md.digest().joinToString("") { "%02x".format(it) }
val query = pairs.joinToString("") { "&$it" } // URL:参数之间用 & 连接
return "$domain$fileName?_btf_tk=$md5Result&_ts=$deadline$query"
}
fun main() {
val path = "/image.jpg"
println("常规 URL: ${createLink(path)}")
println("参数 URL: ${createLink(path, listOf("fmt" to "info"))}")
}