|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | + |
| 3 | +import hmac |
| 4 | +import time |
| 5 | +from hashlib import sha1 |
| 6 | + |
| 7 | +from requests.auth import AuthBase |
| 8 | + |
| 9 | +from .compat import urlparse, json, b |
| 10 | +from .utils import urlsafe_base64_encode |
| 11 | + |
| 12 | + |
| 13 | +# 上传策略,参数规格详见 |
| 14 | +# http://developer.qiniu.com/docs/v6/api/reference/security/put-policy.html |
| 15 | +_policy_fields = set([ |
| 16 | + 'callbackUrl', # 回调URL |
| 17 | + 'callbackBody', # 回调Body |
| 18 | + 'callbackHost', # 回调URL指定的Host |
| 19 | + 'callbackBodyType', # 回调Body的Content-Type |
| 20 | + 'callbackFetchKey', # 回调FetchKey模式开关 |
| 21 | + |
| 22 | + 'returnUrl', # 上传端的303跳转URL |
| 23 | + 'returnBody', # 上传端简单反馈获取的Body |
| 24 | + |
| 25 | + 'endUser', # 回调时上传端标识 |
| 26 | + 'saveKey', # 自定义资源名 |
| 27 | + 'insertOnly', # 插入模式开关 |
| 28 | + |
| 29 | + 'detectMime', # MimeType侦测开关 |
| 30 | + 'mimeLimit', # MimeType限制 |
| 31 | + 'fsizeLimit', # 上传文件大小限制 |
| 32 | + |
| 33 | + 'persistentOps', # 持久化处理操作 |
| 34 | + 'persistentNotifyUrl', # 持久化处理结果通知URL |
| 35 | + 'persistentPipeline', # 持久化处理独享队列 |
| 36 | +]) |
| 37 | + |
| 38 | +_deprecated_policy_fields = set([ |
| 39 | + 'asyncOps' |
| 40 | +]) |
| 41 | + |
| 42 | + |
| 43 | +class Auth(object): |
| 44 | + """七牛安全机制类 |
| 45 | +
|
| 46 | + 该类主要内容是七牛上传凭证、下载凭证、管理凭证三种凭证的签名接口的实现,以及回调验证。 |
| 47 | +
|
| 48 | + Attributes: |
| 49 | + __access_key: 账号密钥对中的accessKey,详见 https://portal.qiniu.com/setting/key |
| 50 | + __secret_key: 账号密钥对重的secretKey,详见 https://portal.qiniu.com/setting/key |
| 51 | + """ |
| 52 | + |
| 53 | + def __init__(self, access_key, secret_key): |
| 54 | + """初始化Auth类""" |
| 55 | + self.__checkKey(access_key, secret_key) |
| 56 | + self.__access_key = access_key |
| 57 | + self.__secret_key = b(secret_key) |
| 58 | + |
| 59 | + def __token(self, data): |
| 60 | + data = b(data) |
| 61 | + hashed = hmac.new(self.__secret_key, data, sha1) |
| 62 | + return urlsafe_base64_encode(hashed.digest()) |
| 63 | + |
| 64 | + def token(self, data): |
| 65 | + return '{0}:{1}'.format(self.__access_key, self.__token(data)) |
| 66 | + |
| 67 | + def token_with_data(self, data): |
| 68 | + data = urlsafe_base64_encode(data) |
| 69 | + return '{0}:{1}:{2}'.format(self.__access_key, self.__token(data), data) |
| 70 | + |
| 71 | + def token_of_request(self, url, body=None, content_type=None): |
| 72 | + """带请求体的签名(本质上是管理凭证的签名) |
| 73 | +
|
| 74 | + Args: |
| 75 | + url: 待签名请求的url |
| 76 | + body: 待签名请求的body |
| 77 | + content_type: 待签名请求的body的Content-Type |
| 78 | +
|
| 79 | + Returns: |
| 80 | + 管理凭证 |
| 81 | + """ |
| 82 | + parsed_url = urlparse(url) |
| 83 | + query = parsed_url.query |
| 84 | + path = parsed_url.path |
| 85 | + data = path |
| 86 | + if query != '': |
| 87 | + data = ''.join([data, '?', query]) |
| 88 | + data = ''.join([data, "\n"]) |
| 89 | + |
| 90 | + if body: |
| 91 | + mimes = [ |
| 92 | + 'application/x-www-form-urlencoded' |
| 93 | + ] |
| 94 | + if content_type in mimes: |
| 95 | + data += body |
| 96 | + |
| 97 | + return '{0}:{1}'.format(self.__access_key, self.__token(data)) |
| 98 | + |
| 99 | + @staticmethod |
| 100 | + def __checkKey(access_key, secret_key): |
| 101 | + if not (access_key and secret_key): |
| 102 | + raise ValueError('invalid key') |
| 103 | + |
| 104 | + def private_download_url(self, url, expires=3600): |
| 105 | + """生成私有资源下载链接 |
| 106 | +
|
| 107 | + Args: |
| 108 | + url: 私有空间资源的原始URL |
| 109 | + expires: 下载凭证有效期,默认为3600s |
| 110 | +
|
| 111 | + Returns: |
| 112 | + 私有资源的下载链接 |
| 113 | + """ |
| 114 | + deadline = int(time.time()) + expires |
| 115 | + if '?' in url: |
| 116 | + url += '&' |
| 117 | + else: |
| 118 | + url += '?' |
| 119 | + url = '{0}e={1}'.format(url, str(deadline)) |
| 120 | + |
| 121 | + token = self.token(url) |
| 122 | + return '{0}&token={1}'.format(url, token) |
| 123 | + |
| 124 | + def upload_token(self, bucket, key=None, expires=3600, policy=None, strict_policy=True): |
| 125 | + """生成上传凭证 |
| 126 | +
|
| 127 | + Args: |
| 128 | + bucket: 上传的空间名 |
| 129 | + key: 上传的文件名,默认为空 |
| 130 | + expires: 上传凭证的过期时间,默认为3600s |
| 131 | + policy: 上传策略,默认为空 |
| 132 | +
|
| 133 | + Returns: |
| 134 | + 上传凭证 |
| 135 | + """ |
| 136 | + if bucket is None or bucket == '': |
| 137 | + raise ValueError('invalid bucket name') |
| 138 | + |
| 139 | + scope = bucket |
| 140 | + if key is not None: |
| 141 | + scope = '{0}:{1}'.format(bucket, key) |
| 142 | + |
| 143 | + args = dict( |
| 144 | + scope=scope, |
| 145 | + deadline=int(time.time()) + expires, |
| 146 | + ) |
| 147 | + |
| 148 | + if policy is not None: |
| 149 | + self.__copy_policy(policy, args, strict_policy) |
| 150 | + |
| 151 | + return self.__upload_token(args) |
| 152 | + |
| 153 | + def __upload_token(self, policy): |
| 154 | + data = json.dumps(policy, separators=(',', ':')) |
| 155 | + return self.token_with_data(data) |
| 156 | + |
| 157 | + def verify_callback(self, origin_authorization, url, body, content_type='application/x-www-form-urlencoded'): |
| 158 | + """回调验证 |
| 159 | +
|
| 160 | + Args: |
| 161 | + origin_authorization: 回调时请求Header中的Authorization字段 |
| 162 | + url: 回调请求的url |
| 163 | + body: 回调请求的body |
| 164 | + content_type: 回调请求body的Content-Type |
| 165 | +
|
| 166 | + Returns: |
| 167 | + 返回true表示验证成功,返回false表示验证失败 |
| 168 | + """ |
| 169 | + token = self.token_of_request(url, body, content_type) |
| 170 | + authorization = 'QBox {0}'.format(token) |
| 171 | + return origin_authorization == authorization |
| 172 | + |
| 173 | + @staticmethod |
| 174 | + def __copy_policy(policy, to, strict_policy): |
| 175 | + for k, v in policy.items(): |
| 176 | + if k in _deprecated_policy_fields: |
| 177 | + raise ValueError(k + ' has deprecated') |
| 178 | + if (not strict_policy) or k in _policy_fields: |
| 179 | + to[k] = v |
| 180 | + |
| 181 | + |
| 182 | +class RequestsAuth(AuthBase): |
| 183 | + def __init__(self, auth): |
| 184 | + self.auth = auth |
| 185 | + |
| 186 | + def __call__(self, r): |
| 187 | + token = None |
| 188 | + if r.body is not None and r.headers['Content-Type'] == 'application/x-www-form-urlencoded': |
| 189 | + token = self.auth.token_of_request(r.url, r.body, 'application/x-www-form-urlencoded') |
| 190 | + else: |
| 191 | + token = self.auth.token_of_request(r.url) |
| 192 | + r.headers['Authorization'] = 'QBox {0}'.format(token) |
| 193 | + return r |
0 commit comments