auth.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. # -*- coding: utf-8 -*-
  2. import hmac
  3. import hashlib
  4. import time
  5. from . import utils
  6. from .compat import urlquote, to_bytes, is_py2
  7. from .headers import *
  8. import logging
  9. from .credentials import StaticCredentialsProvider
  10. AUTH_VERSION_1 = 'v1'
  11. AUTH_VERSION_2 = 'v2'
  12. logger = logging.getLogger(__name__)
  13. def make_auth(access_key_id, access_key_secret, auth_version=AUTH_VERSION_1):
  14. if auth_version == AUTH_VERSION_2:
  15. logger.debug("Init Auth V2: access_key_id: {0}, access_key_secret: ******".format(access_key_id))
  16. return AuthV2(access_key_id.strip(), access_key_secret.strip())
  17. else:
  18. logger.debug("Init Auth v1: access_key_id: {0}, access_key_secret: ******".format(access_key_id))
  19. return Auth(access_key_id.strip(), access_key_secret.strip())
  20. class AuthBase(object):
  21. """用于保存用户AccessKeyId、AccessKeySecret,以及计算签名的对象。"""
  22. def __init__(self, credentials_provider):
  23. self.credentials_provider = credentials_provider
  24. def _sign_rtmp_url(self, url, bucket_name, channel_name, expires, params):
  25. credentials = self.credentials_provider.get_credentials()
  26. if credentials.get_security_token():
  27. params['security-token'] = credentials.get_security_token()
  28. expiration_time = int(time.time()) + expires
  29. canonicalized_resource = "/%s/%s" % (bucket_name, channel_name)
  30. canonicalized_params = []
  31. if params:
  32. items = params.items()
  33. for k, v in items:
  34. if k != "OSSAccessKeyId" and k != "Signature" and k != "Expires" and k != "SecurityToken":
  35. canonicalized_params.append((k, v))
  36. canonicalized_params.sort(key=lambda e: e[0])
  37. canon_params_str = ''
  38. for k, v in canonicalized_params:
  39. canon_params_str += '%s:%s\n' % (k, v)
  40. p = params if params else {}
  41. string_to_sign = str(expiration_time) + "\n" + canon_params_str + canonicalized_resource
  42. logger.debug('Sign Rtmp url: string to be signed = {0}'.format(string_to_sign))
  43. h = hmac.new(to_bytes(credentials.get_access_key_secret()), to_bytes(string_to_sign), hashlib.sha1)
  44. signature = utils.b64encode_as_string(h.digest())
  45. p['OSSAccessKeyId'] = credentials.get_access_key_id()
  46. p['Expires'] = str(expiration_time)
  47. p['Signature'] = signature
  48. return url + '?' + '&'.join(_param_to_quoted_query(k, v) for k, v in p.items())
  49. class ProviderAuth(AuthBase):
  50. """签名版本1
  51. 默认构造函数同父类AuthBase,需要传递credentials_provider
  52. """
  53. _subresource_key_set = frozenset(
  54. ['response-content-type', 'response-content-language',
  55. 'response-cache-control', 'logging', 'response-content-encoding',
  56. 'acl', 'uploadId', 'uploads', 'partNumber', 'group', 'link',
  57. 'delete', 'website', 'location', 'objectInfo', 'objectMeta',
  58. 'response-expires', 'response-content-disposition', 'cors', 'lifecycle',
  59. 'restore', 'qos', 'referer', 'stat', 'bucketInfo', 'append', 'position', 'security-token',
  60. 'live', 'comp', 'status', 'vod', 'startTime', 'endTime', 'x-oss-process',
  61. 'symlink', 'callback', 'callback-var', 'tagging', 'encryption', 'versions',
  62. 'versioning', 'versionId', 'policy', 'requestPayment', 'x-oss-traffic-limit', 'qosInfo', 'asyncFetch',
  63. 'x-oss-request-payer', 'sequential', 'inventory', 'inventoryId', 'continuation-token', 'callback',
  64. 'callback-var', 'worm', 'wormId', 'wormExtend', 'replication', 'replicationLocation',
  65. 'replicationProgress', 'transferAcceleration']
  66. )
  67. def _sign_request(self, req, bucket_name, key):
  68. credentials = self.credentials_provider.get_credentials()
  69. if credentials.get_security_token():
  70. req.headers[OSS_SECURITY_TOKEN] = credentials.get_security_token()
  71. req.headers['date'] = utils.http_date()
  72. signature = self.__make_signature(req, bucket_name, key, credentials)
  73. req.headers['authorization'] = "OSS {0}:{1}".format(credentials.get_access_key_id(), signature)
  74. def _sign_url(self, req, bucket_name, key, expires):
  75. credentials = self.credentials_provider.get_credentials()
  76. if credentials.get_security_token():
  77. req.params['security-token'] = credentials.get_security_token()
  78. expiration_time = int(time.time()) + expires
  79. req.headers['date'] = str(expiration_time)
  80. signature = self.__make_signature(req, bucket_name, key, credentials)
  81. req.params['OSSAccessKeyId'] = credentials.get_access_key_id()
  82. req.params['Expires'] = str(expiration_time)
  83. req.params['Signature'] = signature
  84. return req.url + '?' + '&'.join(_param_to_quoted_query(k, v) for k, v in req.params.items())
  85. def __make_signature(self, req, bucket_name, key, credentials):
  86. if is_py2:
  87. string_to_sign = self.__get_string_to_sign(req, bucket_name, key)
  88. else:
  89. string_to_sign = self.__get_bytes_to_sign(req, bucket_name, key)
  90. logger.debug('Make signature: string to be signed = {0}'.format(string_to_sign))
  91. h = hmac.new(to_bytes(credentials.get_access_key_secret()), to_bytes(string_to_sign), hashlib.sha1)
  92. return utils.b64encode_as_string(h.digest())
  93. def __get_string_to_sign(self, req, bucket_name, key):
  94. resource_string = self.__get_resource_string(req, bucket_name, key)
  95. headers_string = self.__get_headers_string(req)
  96. content_md5 = req.headers.get('content-md5', '')
  97. content_type = req.headers.get('content-type', '')
  98. date = req.headers.get('date', '')
  99. return '\n'.join([req.method,
  100. content_md5,
  101. content_type,
  102. date,
  103. headers_string + resource_string])
  104. def __get_headers_string(self, req):
  105. headers = req.headers
  106. canon_headers = []
  107. for k, v in headers.items():
  108. lower_key = k.lower()
  109. if lower_key.startswith('x-oss-'):
  110. canon_headers.append((lower_key, v))
  111. canon_headers.sort(key=lambda x: x[0])
  112. if canon_headers:
  113. return '\n'.join(k + ':' + v for k, v in canon_headers) + '\n'
  114. else:
  115. return ''
  116. def __get_resource_string(self, req, bucket_name, key):
  117. if not bucket_name:
  118. return '/' + self.__get_subresource_string(req.params)
  119. else:
  120. return '/{0}/{1}{2}'.format(bucket_name, key, self.__get_subresource_string(req.params))
  121. def __get_subresource_string(self, params):
  122. if not params:
  123. return ''
  124. subresource_params = []
  125. for key, value in params.items():
  126. if key in self._subresource_key_set:
  127. subresource_params.append((key, value))
  128. subresource_params.sort(key=lambda e: e[0])
  129. if subresource_params:
  130. return '?' + '&'.join(self.__param_to_query(k, v) for k, v in subresource_params)
  131. else:
  132. return ''
  133. def __param_to_query(self, k, v):
  134. if v:
  135. return k + '=' + v
  136. else:
  137. return k
  138. def __get_bytes_to_sign(self, req, bucket_name, key):
  139. resource_bytes = self.__get_resource_string(req, bucket_name, key).encode('utf-8')
  140. headers_bytes = self.__get_headers_bytes(req)
  141. content_md5 = req.headers.get('content-md5', '').encode('utf-8')
  142. content_type = req.headers.get('content-type', '').encode('utf-8')
  143. date = req.headers.get('date', '').encode('utf-8')
  144. return b'\n'.join([req.method.encode('utf-8'),
  145. content_md5,
  146. content_type,
  147. date,
  148. headers_bytes + resource_bytes])
  149. def __get_headers_bytes(self, req):
  150. headers = req.headers
  151. canon_headers = []
  152. for k, v in headers.items():
  153. lower_key = k.lower()
  154. if lower_key.startswith('x-oss-'):
  155. canon_headers.append((lower_key, v))
  156. canon_headers.sort(key=lambda x: x[0])
  157. if canon_headers:
  158. return b'\n'.join(to_bytes(k) + b':' + to_bytes(v) for k, v in canon_headers) + b'\n'
  159. else:
  160. return b''
  161. class Auth(ProviderAuth):
  162. """签名版本1
  163. """
  164. def __init__(self, access_key_id, access_key_secret):
  165. credentials_provider = StaticCredentialsProvider(access_key_id.strip(), access_key_secret.strip())
  166. super(Auth, self).__init__(credentials_provider)
  167. class AnonymousAuth(object):
  168. """用于匿名访问。
  169. .. note::
  170. 匿名用户只能读取public-read的Bucket,或只能读取、写入public-read-write的Bucket。
  171. 不能进行Service、Bucket相关的操作,也不能罗列文件等。
  172. """
  173. def _sign_request(self, req, bucket_name, key):
  174. pass
  175. def _sign_url(self, req, bucket_name, key, expires):
  176. return req.url + '?' + '&'.join(_param_to_quoted_query(k, v) for k, v in req.params.items())
  177. def _sign_rtmp_url(self, url, bucket_name, channel_name, expires, params):
  178. return url + '?' + '&'.join(_param_to_quoted_query(k, v) for k, v in params.items())
  179. class StsAuth(object):
  180. """用于STS临时凭证访问。可以通过官方STS客户端获得临时密钥(AccessKeyId、AccessKeySecret)以及临时安全令牌(SecurityToken)。
  181. 注意到临时凭证会在一段时间后过期,在此之前需要重新获取临时凭证,并更新 :class:`Bucket <oss2.Bucket>` 的 `auth` 成员变量为新
  182. 的 `StsAuth` 实例。
  183. :param str access_key_id: 临时AccessKeyId
  184. :param str access_key_secret: 临时AccessKeySecret
  185. :param str security_token: 临时安全令牌(SecurityToken)
  186. :param str auth_version: 需要生成auth的版本,默认为AUTH_VERSION_1(v1)
  187. """
  188. def __init__(self, access_key_id, access_key_secret, security_token, auth_version=AUTH_VERSION_1):
  189. logger.debug("Init StsAuth: access_key_id: {0}, access_key_secret: ******, security_token: ******".format(access_key_id))
  190. credentials_provider = StaticCredentialsProvider(access_key_id, access_key_secret, security_token)
  191. self.__auth = ProviderAuthV2(credentials_provider) if auth_version == AUTH_VERSION_2 else ProviderAuth(credentials_provider)
  192. def _sign_request(self, req, bucket_name, key):
  193. self.__auth._sign_request(req, bucket_name, key)
  194. def _sign_url(self, req, bucket_name, key, expires):
  195. return self.__auth._sign_url(req, bucket_name, key, expires)
  196. def _sign_rtmp_url(self, url, bucket_name, channel_name, expires, params):
  197. return self.__auth._sign_rtmp_url(url, bucket_name, channel_name, expires, params)
  198. def _param_to_quoted_query(k, v):
  199. if v:
  200. return urlquote(k, '') + '=' + urlquote(v, '')
  201. else:
  202. return urlquote(k, '')
  203. def v2_uri_encode(raw_text):
  204. raw_text = to_bytes(raw_text)
  205. res = ''
  206. for b in raw_text:
  207. if isinstance(b, int):
  208. c = chr(b)
  209. else:
  210. c = b
  211. if (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z')\
  212. or (c >= '0' and c <= '9') or c in ['_', '-', '~', '.']:
  213. res += c
  214. else:
  215. res += "%{0:02X}".format(ord(c))
  216. return res
  217. _DEFAULT_ADDITIONAL_HEADERS = set(['range',
  218. 'if-modified-since'])
  219. class ProviderAuthV2(AuthBase):
  220. """签名版本2,默认构造函数同父类AuthBase,需要传递credentials_provider
  221. 与版本1的区别在:
  222. 1. 使用SHA256算法,具有更高的安全性
  223. 2. 参数计算包含所有的HTTP查询参数
  224. """
  225. def _sign_request(self, req, bucket_name, key, in_additional_headers=None):
  226. """把authorization放入req的header里面
  227. :param req: authorization信息将会加入到这个请求的header里面
  228. :type req: oss2.http.Request
  229. :param bucket_name: bucket名称
  230. :param key: OSS文件名
  231. :param in_additional_headers: 加入签名计算的额外header列表
  232. """
  233. credentials = self.credentials_provider.get_credentials()
  234. if credentials.get_security_token():
  235. req.headers[OSS_SECURITY_TOKEN] = credentials.get_security_token()
  236. if in_additional_headers is None:
  237. in_additional_headers = _DEFAULT_ADDITIONAL_HEADERS
  238. additional_headers = self.__get_additional_headers(req, in_additional_headers)
  239. req.headers['date'] = utils.http_date()
  240. signature = self.__make_signature(req, bucket_name, key, additional_headers, credentials)
  241. if additional_headers:
  242. req.headers['authorization'] = "OSS2 AccessKeyId:{0},AdditionalHeaders:{1},Signature:{2}"\
  243. .format(credentials.get_access_key_id(), ';'.join(additional_headers), signature)
  244. else:
  245. req.headers['authorization'] = "OSS2 AccessKeyId:{0},Signature:{1}".format(credentials.get_access_key_id(), signature)
  246. def _sign_url(self, req, bucket_name, key, expires, in_additional_headers=None):
  247. """返回一个签过名的URL
  248. :param req: 需要签名的请求
  249. :type req: oss2.http.Request
  250. :param bucket_name: bucket名称
  251. :param key: OSS文件名
  252. :param int expires: 返回的url将在`expires`秒后过期.
  253. :param in_additional_headers: 加入签名计算的额外header列表
  254. :return: a signed URL
  255. """
  256. credentials = self.credentials_provider.get_credentials()
  257. if credentials.get_security_token():
  258. req.params['security-token'] = credentials.get_security_token()
  259. if in_additional_headers is None:
  260. in_additional_headers = set()
  261. additional_headers = self.__get_additional_headers(req, in_additional_headers)
  262. expiration_time = int(time.time()) + expires
  263. req.headers['date'] = str(expiration_time) # re-use __make_signature by setting the 'date' header
  264. req.params['x-oss-signature-version'] = 'OSS2'
  265. req.params['x-oss-expires'] = str(expiration_time)
  266. req.params['x-oss-access-key-id'] = credentials.get_access_key_id()
  267. signature = self.__make_signature(req, bucket_name, key, additional_headers, credentials)
  268. req.params['x-oss-signature'] = signature
  269. return req.url + '?' + '&'.join(_param_to_quoted_query(k, v) for k, v in req.params.items())
  270. def __make_signature(self, req, bucket_name, key, additional_headers, credentials):
  271. if is_py2:
  272. string_to_sign = self.__get_string_to_sign(req, bucket_name, key, additional_headers)
  273. else:
  274. string_to_sign = self.__get_bytes_to_sign(req, bucket_name, key, additional_headers)
  275. logger.debug('Make signature: string to be signed = {0}'.format(string_to_sign))
  276. h = hmac.new(to_bytes(credentials.get_access_key_secret()), to_bytes(string_to_sign), hashlib.sha256)
  277. return utils.b64encode_as_string(h.digest())
  278. def __get_additional_headers(self, req, in_additional_headers):
  279. # we add a header into additional_headers only if it is already in req's headers.
  280. additional_headers = set(h.lower() for h in in_additional_headers)
  281. keys_in_header = set(k.lower() for k in req.headers.keys())
  282. return additional_headers & keys_in_header
  283. def __get_string_to_sign(self, req, bucket_name, key, additional_header_list):
  284. verb = req.method
  285. content_md5 = req.headers.get('content-md5', '')
  286. content_type = req.headers.get('content-type', '')
  287. date = req.headers.get('date', '')
  288. canonicalized_oss_headers = self.__get_canonicalized_oss_headers(req, additional_header_list)
  289. additional_headers = ';'.join(sorted(additional_header_list))
  290. canonicalized_resource = self.__get_resource_string(req, bucket_name, key)
  291. return verb + '\n' +\
  292. content_md5 + '\n' +\
  293. content_type + '\n' +\
  294. date + '\n' +\
  295. canonicalized_oss_headers +\
  296. additional_headers + '\n' +\
  297. canonicalized_resource
  298. def __get_resource_string(self, req, bucket_name, key):
  299. if bucket_name:
  300. encoded_uri = v2_uri_encode('/' + bucket_name + '/' + key)
  301. else:
  302. encoded_uri = v2_uri_encode('/')
  303. logger.info('encoded_uri={0} key={1}'.format(encoded_uri, key))
  304. return encoded_uri + self.__get_canonalized_query_string(req)
  305. def __get_canonalized_query_string(self, req):
  306. encoded_params = {}
  307. for param, value in req.params.items():
  308. encoded_params[v2_uri_encode(param)] = v2_uri_encode(value)
  309. if not encoded_params:
  310. return ''
  311. sorted_params = sorted(encoded_params.items(), key=lambda e: e[0])
  312. return '?' + '&'.join(self.__param_to_query(k, v) for k, v in sorted_params)
  313. def __param_to_query(self, k, v):
  314. if v:
  315. return k + '=' + v
  316. else:
  317. return k
  318. def __get_canonicalized_oss_headers(self, req, additional_headers):
  319. """
  320. :param additional_headers: 小写的headers列表, 并且这些headers都不以'x-oss-'为前缀.
  321. """
  322. canon_headers = []
  323. for k, v in req.headers.items():
  324. lower_key = k.lower()
  325. if lower_key.startswith('x-oss-') or lower_key in additional_headers:
  326. canon_headers.append((lower_key, v))
  327. canon_headers.sort(key=lambda x: x[0])
  328. return ''.join(v[0] + ':' + v[1] + '\n' for v in canon_headers)
  329. def __get_bytes_to_sign(self, req, bucket_name, key, additional_header_list):
  330. verb = req.method.encode('utf-8')
  331. content_md5 = req.headers.get('content-md5', '').encode('utf-8')
  332. content_type = req.headers.get('content-type', '').encode('utf-8')
  333. date = req.headers.get('date', '').encode('utf-8')
  334. canonicalized_oss_headers = self.__get_canonicalized_oss_headers_bytes(req, additional_header_list)
  335. additional_headers = ';'.join(sorted(additional_header_list)).encode('utf-8')
  336. canonicalized_resource = self.__get_resource_string(req, bucket_name, key).encode('utf-8')
  337. return verb + b'\n' +\
  338. content_md5 + b'\n' +\
  339. content_type + b'\n' +\
  340. date + b'\n' +\
  341. canonicalized_oss_headers +\
  342. additional_headers + b'\n' +\
  343. canonicalized_resource
  344. def __get_canonicalized_oss_headers_bytes(self, req, additional_headers):
  345. """
  346. :param additional_headers: 小写的headers列表, 并且这些headers都不以'x-oss-'为前缀.
  347. """
  348. canon_headers = []
  349. for k, v in req.headers.items():
  350. lower_key = k.lower()
  351. if lower_key.startswith('x-oss-') or lower_key in additional_headers:
  352. canon_headers.append((lower_key, v))
  353. canon_headers.sort(key=lambda x: x[0])
  354. return b''.join(to_bytes(v[0]) + b':' + to_bytes(v[1]) + b'\n' for v in canon_headers)
  355. class AuthV2(ProviderAuthV2):
  356. """签名版本2,与版本1的区别在:
  357. 1. 使用SHA256算法,具有更高的安全性
  358. 2. 参数计算包含所有的HTTP查询参数
  359. """
  360. def __init__(self, access_key_id, access_key_secret):
  361. credentials_provider = StaticCredentialsProvider(access_key_id.strip(), access_key_secret.strip())
  362. super(AuthV2, self).__init__(credentials_provider)