安全快速下载¶
模块: mod_secdownload
- 目录
- 安全快速下载
描述¶
已废弃 mod_secdownload 应替换为 lua mod_secdownload
有多种方式处理安全下载机制
- 使用Web服务器和内部HTTP认证
- 使用应用程序进行认证并发送文件
通过应用程序
这两种方式都有局限性
Web服务器- + 快速下载
- + 无额外系统负载
- -- 认证处理不灵活
应用程序
- + 集成到整体布局中
- + 非常灵活的权限管理
- -- 下载会占用应用程序线程/进程
结合这两种方式的一个简单方法可以是
1. 应用认证用户并检查权限以
下载文件。
2. 应用将用户重定向到Web服务器可访问的文件
进行后续下载。
3. Web服务器将文件传输给用户。
由于Web服务器对应用程序中使用的权限一无所知
,因此生成的URL将对所有
知道该URL的用户可用。
mod_secdownload通过引入一种方式来解决此问题,即
在指定时间内验证URL。应用程序必须
生成一个令牌和时间戳,Web服务器在允许
文件由Web服务器下载之前会对其进行检查。
Web服务器。
生成的URL必须具有以下格式
<uri-prefix>/<token>/<timestamp-in-hex>/<rel-path> which looks like "yourserver.com/bf32df9cdb54894b22e09d0ed87326fc/435cc8cc/secure.tar.gz" <token> is an MD5 of 1. a secret string (user supplied) 2. <rel-path> (starts with /) 3. <timestamp-in-hex>
如您所见,令牌根本不与用户绑定。唯一
的限制因素是时间戳,它用于在给定超时(secdownload.timeout)后
使URL失效。
重要¶
务必选择与示例中使用的密钥不同的
密钥,因为这是令牌中唯一不
被用户所知的部分。
确保令牌也是十六进制的。根据
您使用的编程语言,可能没有额外的
步骤。例如,在PHP中,MD5函数
返回摘要的十六进制值。但是,如果您使用
Java或Python等语言,则需要额外的步骤来将
摘要转换为十六进制(请参见下面的Python示例)。
如果用户尝试通过选择随机令牌来伪造URL,
将发送状态 403 'Forbidden'。
如果达到超时,将发送状态 410 'Gone'。
这在早期版本中曾是 408 '请求超时'。
如果令牌和超时有效,<rel-path> 将附加到
已配置的 (secdownload.document-root) 并传递给
正常的内部文件传输功能。这可能导致
状态 200 或 404。
选项¶
secdownload.secret = <string> secdownload.document-root = <string> secdownload.uri-prefix = <string> (default: /) secdownload.timeout = <short> (default: 60 seconds) secdownload.algorithm = <string> ("md5", "hmac-sha1", "hmac-sha256") secdownload.path-segments = <number> (since 1.4.46) secdownload.hash-querystr = "enable" | "disable" (since 1.4.46)
示例¶
您的应用程序必须生成正确的URL。
PHP 示例¶
<?php $secret = "verysecret"; $uri_prefix = "/dl/"; # filename # please note file name starts with "/" $f = "/secret-file.txt"; # current timestamp $t = time(); $t_hex = sprintf("%08x", $t); $m = md5($secret.$f.$t_hex); # generate link printf('<a href="%s%s/%s%s">%s</a>', $uri_prefix, $m, $t_hex, $f, $f); ?>
Ruby On Rails 示例,在辅助函数上下文中使用¶
def gen_sec_link(rel_path) rel_path.sub!(/^([^\/])/,'/\1') # Make sure it had a leading slash s_secret = 'verysecret' # Secret string uri_prefix = '/dl/' # Arbitrary download prefix timestamp = "%08x" % Time.now.to_i # Timestamp, to hex token = MD5::md5(s_secret + rel_path + timestamp).to_s # Token Creation '%s%s/%s%s' % [uri_prefix, token, timestamp, rel_path] # Return the properly formatted string end
因此在视图或辅助函数中
<%= link_to "Private Image", gen_sec_link("path/from/download-area/someimage.img") %>
Perl 示例¶
use strict; use Digest::MD5 qw< md5_hex >; my $request = "whatever.txt"; print gen_sec_link($request), "\n"; sub gen_sec_link { my $relpath = shift; $relpath =~ s:^([^/]):/$1:; # make sure it has a leading slash my $prefix = "/static/"; # download prefix my $secret = "verysecret"; # secret string my $hextime = sprintf "%08x", time; my $token = md5_hex($secret.$relpath.$hextime); return sprintf "%s%s/%s%s", $prefix, $token, $hextime, $relpath }
Python 示例,可与 Django 或任何其他 Python Web 框架一起使用¶
def gen_sec_link(rel_path): import time, hashlib secret = 'verysecret' uri_prefix = '/dl/' hextime = "%08x" % time.time() token = hashlib.md5(secret + rel_path + hextime).hexdigest() return '%s%s/%s%s' % (uri_prefix, token, hextime, rel_path)
注意: 当Django与非ASCII文件名一起使用时(Python 2.x中的hashlib不能正确处理非ASCII字符串,会抛出UnicodeDecodeError异常,除非提供UTF-8编码的字节字符串而不是默认的unicode对象)
def gen_securelink(rel_path): import time, hashlib from django.utils.http import urlquote rel_path = '/%s/%s' % (self.series.directory, self.filename) secret = "flylight" uri_prefix = "http://media.ongoing.ru/download/" hextime = "%08x" % time.time() token = hashlib.md5((secret + rel_path + hextime).encode('utf-8')).hexdigest() return '%s%s/%s%s' % (uri_prefix, token, hextime, urlquote(rel_path))
(当然,urlquote() 可以替换为 urllib 方法。)
C# 示例¶
//import library using System.Security.Cryptography; using System.Text; //function : public string GetHash(string hashMe) // function get MD5 { MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider(); UTF7Encoding encoder = new UTF7Encoding(); Byte[] encStringBytes; encStringBytes = encoder.GetBytes(hashMe); encStringBytes = md5.ComputeHash(encStringBytes); string strHex = string.Empty; foreach (byte b in encStringBytes) strHex += String.Format("{0:x2}", b); return strHex; } public string GetCurrentEpochTimeInHex() // function get current epoch time in HEX { DateTime EpochTime = new DateTime(1970, 1, 1, 0, 0, 0); TimeSpan diff = DateTime.UtcNow - EpochTime; return ((long)diff.TotalSeconds).ToString("x"); } public string GenerateSecureLink(string rel_path) // ex : rel_path = "/secret-file.txt"; { string t_hex = GetCurrentEpochTimeInHex(); string serect = "verysecret"; string uri_prefix = "/dl/"; string m = GetHash(serect + rel_path + t_hex); return String.Format("{0}{1}/{2}{3}", uri_prefix, m, t_hex, rel_path); }
C# 中生成 MD5 的参考:http://ok-cool.com/posts/read/125-php-md5-not-the-same-as-net-md5/
JAVA 示例¶
//Usage String url = new LighttpdAuthUrl("verysecret","/dl/",videoName).toString(); //Import Packages import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class LighttpdAuthUrl { private final String token; private final String timestamp; private final String authpath; private final String filepath; public LighttpdAuthUrl(String secret, String authpath, String filepath) { this(secret, authpath, filepath, System.currentTimeMillis()); } public LighttpdAuthUrl(String secret, String authpath, String filepath, long time) { this.timestamp = Long.toHexString(time/1000L); this.authpath = authpath; this.filepath = sanitizeFilePath(filepath); this.token = generateToken(secret); } public String sanitizeFilePath(String fpath) { if (fpath.charAt(0) != '/') { fpath = '/' + fpath; } return fpath; } private String generateToken(String secret) { return toMD5(secret+filepath+timestamp); } public static String toMD5(String source) { MessageDigest digest = null; try { digest = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } digest.update(source.getBytes()); byte[] hash = digest.digest(); return byteArrayToHexString(hash); } public static String byteToHexString(byte aByte) { String hex = Integer.toHexString(0xFF & aByte); return ((hex.length() == 1)?"0":"")+hex; } public static String byteArrayToHexString(byte[] hash) { StringBuffer hexString = new StringBuffer(); for (int i=0;i<hash.length;i++) { hexString.append(byteToHexString(hash[i])); } return hexString.toString(); } public static boolean isBlank(String str) { return str == null || str.length() == 0; } public String getToken() { return token; } public String getTimestamp() { return timestamp; } public String toString() { return toUriString(); } public String toUriString() { return authpath + token + "/" + timestamp + filepath; } }
Web服务器¶
服务器必须以相同方式配置。URI前缀和
密钥必须匹配::
server.modules = ( ..., "mod_secdownload", ... ) secdownload.secret = "verysecret" secdownload.document-root = "/home/www/servers/download-area/" secdownload.uri-prefix = "/dl/" secdownload.timeout = 10