1 前端直传

在客户端通过JavaScript代码完成签名,无需过多配置,即可实现直传,非常方便。
但是
客户端通过JavaScript把AccesssKey ID和AccessKey Secret写在代码里面有泄露的风险,
强烈建议使用服务端签名后直传或者STS临时授权访问OSS。

2 后端上传

(1) 原理

image.png
和数据直传到OSS相比,以上方法存在以下缺点:

  • 上传慢:用户数据需先上传到应用服务器,之后再上传到OSS,网络传输时间比直传到OSS多一倍。如果用户数据不通过应用服务器中转,而是直传到OSS,速度将大大提升。而且OSS采用BGP带宽,能保证各地各运营商之间的传输速度。
  • 扩展性差:如果后续用户数量逐渐增加,则应用服务器会成为瓶颈。
  • 费用高:需要准备多台应用服务器。由于OSS上行流量是免费的,如果数据直传到OSS,将节省多台应用服务器的费用。

    (2) 代码

    pip install oss2

  1. import oss2
  2. OSS_BASE_PATH = "https://cncms-server.oss-cn-beijing.aliyuncs.com/"
  3. end_point = 'https://oss-cn-beijing.aliyuncs.com'
  4. bucket_name = 'cncms-server'
  5. class OssFileStorage():
  6. def __init__(self, root_store_path: str):
  7. self.auth = oss2.Auth(access_key_id, access_key_secret)
  8. self.bucket = oss2.Bucket(self.auth, end_point, bucket_name)
  9. self.root_store_path = root_store_path # 根存储路径
  10. def file_upload(self, pk: str, category: str, file_name: str, file_content: str) -> dict:
  11. '''文件上传'''
  12. # 拼接文件路径
  13. object_path = self.root_store_path + "/" + pk + "/" + category + "/" + file_name
  14. # 上传文件
  15. self.bucket.put_object(object_path, file_content)
  16. # 转义
  17. quote_file_name = urllib.parse.quote(file_name)
  18. object_path = self.root_store_path + "/" + pk + "/" + category + "/" + quote_file_name
  19. file_url = OSS_BASE_PATH + object_path
  20. result = {
  21. "object_path": object_path,
  22. "file_url": file_url
  23. }
  24. return result
  25. def delete_file(self, file_url: str):
  26. '''删除文件'''
  27. object_path = file_url.replace(OSS_BASE_PATH, "")
  28. self.bucket.delete_object(object_path)
  29. def is_file_exist(self, filename):
  30. ret = self.bucket.object_exists(filename)
  31. return ret
  32. def generate_download_url(self, url):
  33. if not url:
  34. return None
  35. # 从数据库存储的完整路径中获取object_name
  36. object_name = url.replace(OSS_BASE_PATH, "")
  37. # 反转义, 避免路径中包含中文后不能加AccesskeyID
  38. object_name = urllib.parse.unquote(object_name)
  39. # 生成下载文件的签名URL,有效时间为1天。
  40. # 生成签名URL时,OSS默认会对Object完整路径中的正斜线(/)进行转义,从而导致生成的签名URL无法直接使用。
  41. # 设置slash_safe为True,OSS不会对Object完整路径中的正斜线(/)进行转义,此时生成的签名URL可以直接使用。
  42. download_url = self.bucket.sign_url('GET', object_name, 24*60*60, slash_safe=True)
  43. return download_url
  1. @bp_hard.route('/hardwares/export', methods=['GET'])
  2. @auth.login_required
  3. @auth.module_hard
  4. def export_hardware():
  5. """导出硬件"""
  6. xls_name = ['硬件']
  7. fit_hards = filter_hards(request, xls_name)
  8. # 生成excel文件
  9. file_name = '_'.join(xls_name) + '.xlsx'
  10. file_path = './temp/' + file_name
  11. xlsx_export.export_xlsx_hards(fit_hards, file_path)
  12. # 同步上传
  13. file_content = open(file_path, 'rb').read()
  14. ret = fs.file_upload('excel', 'hard', file_name, file_content)
  15. file_url = ret['file_url']
  16. result = {'fileUrl': fs.generate_download_url(file_url)}
  17. return jsonify(errno=RET.OK, errmsg='导出硬件数据成功', result=result)

3 服务端签名后, 前端上传

(1) 原理

image.png
文档:
https://help.aliyun.com/document_detail/91848.html
image.png
第一个GET请求的响应

  1. {
  2. "accessid": "LTAI5tA7ijeV6BCwdeHWy5hQ",
  3. "host": "http://wstest1.oss-cn-beijing.aliyuncs.com",
  4. "policy": "eyJleHBpcmF0aW9uIjogIjIwMjItMDEtMDVUMDU6MjM6NTlaIiwgImNvbmRpdGlvbnMiOiBbWyJzdGFydHMtd2l0aCIsICIka2V5IiwgInVzZXItZGlyLXByZWZpeC8iXV19",
  5. "signature": "dzqZQaklHQcA6K4Rk+6pZ5/RP7M=",
  6. "expire": 1641360239,
  7. "dir": "user-dir-prefix/",
  8. "callback": "eyJjYWxsYmFja1VybCI6ICJodHRwOi8vNDcuMTA4LjE3MC4xOTU6NDcxMjMiLCAiY2FsbGJhY2tCb2R5IjogImZpbGVuYW1lPSR7b2JqZWN0fSZzaXplPSR7c2l6ZX0mbWltZVR5cGU9JHttaW1lVHlwZX0maGVpZ2h0PSR7aW1hZ2VJbmZvLmhlaWdodH0md2lkdGg9JHtpbWFnZUluZm8ud2lkdGh9IiwgImNhbGxiYWNrQm9keVR5cGUiOiAiYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkIn0="
  9. }

(2) 代码

1) appserver.py

  1. # -*- coding: UTF-8 -*-
  2. import socket
  3. import base64
  4. import sys
  5. import time
  6. import datetime
  7. import json
  8. import hmac
  9. from hashlib import sha1 as sha
  10. import httpserver
  11. # 请填写您的AccessKeyId。
  12. access_key_id = 'LTAI5tA7ijeV6BCwdeHWy5hQ'
  13. # 请填写您的AccessKeySecret。
  14. access_key_secret = 'bDikXRrncXK1A9JCMrwV1c89WN2ySr'
  15. # host的格式为 bucketname.endpoint ,请替换为您的真实信息。
  16. host = 'http://wstest1.oss-cn-beijing.aliyuncs.com'
  17. # callback_url为 上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
  18. callback_url = "http://192.168.1.103:47123"
  19. # 用户上传文件时指定的前缀。
  20. upload_dir = 'user-dir-prefix/'
  21. expire_time = 3000000
  22. def get_iso_8601(expire):
  23. gmt = datetime.datetime.utcfromtimestamp(expire).isoformat()
  24. gmt += 'Z'
  25. return gmt
  26. def get_token():
  27. now = int(time.time())
  28. expire_syncpoint = now + expire_time
  29. expire = get_iso_8601(expire_syncpoint)
  30. policy_dict = {}
  31. policy_dict['expiration'] = expire
  32. condition_array = []
  33. array_item = []
  34. array_item.append('starts-with')
  35. array_item.append('$key')
  36. array_item.append(upload_dir)
  37. condition_array.append(array_item)
  38. policy_dict['conditions'] = condition_array
  39. policy = json.dumps(policy_dict).strip()
  40. policy_encode = base64.b64encode(policy.encode())
  41. h = hmac.new(access_key_secret.encode(), policy_encode, sha)
  42. sign_result = base64.encodestring(h.digest()).strip()
  43. callback_dict = {}
  44. callback_dict['callbackUrl'] = callback_url
  45. callback_dict['callbackBody'] = 'filename=${object}&size=${size}&mimeType=${mimeType}' \
  46. '&height=${imageInfo.height}&width=${imageInfo.width}'
  47. callback_dict['callbackBodyType'] = 'application/x-www-form-urlencoded'
  48. callback_param = json.dumps(callback_dict).strip()
  49. base64_callback_body = base64.b64encode(callback_param.encode())
  50. token_dict = {}
  51. token_dict['accessid'] = access_key_id
  52. token_dict['host'] = host
  53. token_dict['policy'] = policy_encode.decode()
  54. token_dict['signature'] = sign_result.decode()
  55. token_dict['expire'] = expire_syncpoint
  56. token_dict['dir'] = upload_dir
  57. token_dict['callback'] = base64_callback_body.decode()
  58. result = json.dumps(token_dict)
  59. return result
  60. def get_local_ip():
  61. """
  62. 获取本机 IPV4 地址
  63. :return: 成功返回本机 IP 地址,否则返回空
  64. """
  65. try:
  66. csocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  67. csocket.connect(('8.8.8.8', 80))
  68. (addr, port) = csocket.getsockname()
  69. csocket.close()
  70. return addr
  71. except socket.error:
  72. return ""
  73. def do_POST(server):
  74. """
  75. 启用 POST 调用处理逻辑
  76. :param server: Web HTTP Server 服务
  77. :return:
  78. """
  79. print("********************* do_POST ")
  80. # get public key
  81. pub_key_url = ''
  82. try:
  83. pub_key_url_base64 = server.headers['x-oss-pub-key-url']
  84. pub_key = httpserver.get_pub_key(pub_key_url_base64)
  85. except Exception as e:
  86. print(str(e))
  87. print('Get pub key failed! pub_key_url : ' + pub_key_url)
  88. server.send_response(400)
  89. server.end_headers()
  90. return
  91. # get authorization
  92. authorization_base64 = server.headers['authorization']
  93. # get callback body
  94. content_length = server.headers['content-length']
  95. callback_body = server.rfile.read(int(content_length))
  96. # compose authorization string
  97. auth_str = ''
  98. pos = server.path.find('?')
  99. if -1 == pos:
  100. auth_str = server.path + '\n' + callback_body.decode()
  101. else:
  102. auth_str = httpserver.get_http_request_unquote(server.path[0:pos]) + server.path[pos:] + '\n' + callback_body
  103. result = httpserver.verrify(auth_str, authorization_base64, pub_key)
  104. if not result:
  105. print('Authorization verify failed!')
  106. print('Public key : %s' % (pub_key))
  107. print('Auth string : %s' % (auth_str))
  108. server.send_response(400)
  109. server.end_headers()
  110. return
  111. # response to OSS
  112. resp_body = '{"Status":"OK"}'
  113. server.send_response(200)
  114. server.send_header('Content-Type', 'application/json')
  115. server.send_header('Content-Length', str(len(resp_body)))
  116. server.end_headers()
  117. server.wfile.write(resp_body.encode())
  118. def do_GET(server):
  119. """
  120. 启用 Get 调用处理逻辑
  121. :param server: Web HTTP Server 服务
  122. :return:
  123. """
  124. print("********************* do_GET ")
  125. token = get_token()
  126. server.send_response(200)
  127. server.send_header('Access-Control-Allow-Methods', 'POST')
  128. server.send_header('Access-Control-Allow-Origin', '*')
  129. server.send_header('Content-Type', 'text/html; charset=UTF-8')
  130. server.end_headers()
  131. server.wfile.write(token.encode())
  132. if '__main__' == __name__:
  133. # 在服务器中, 0.0.0.0指的是本机上的所有IPV4地址, 如果一个主机有两个IP地址,
  134. # 192.168.1.1 和 10.1.2.1, 并且该主机上的一个服务监听的地址是0.0.0.0, 那么通过两个IP地址都能够访问该服务。
  135. # server_ip = get_local_ip() 若用户希望监听本机外网IPV4地址,则采用本行代码并注释掉下一行代码
  136. server_ip = "0.0.0.0"
  137. server_port = 47123
  138. if len(sys.argv) == 2:
  139. server_port = int(sys.argv[1])
  140. if len(sys.argv) == 3:
  141. server_ip = sys.argv[1]
  142. server_port = int(sys.argv[2])
  143. print("App server is running on http://%s:%s " % (server_ip, server_port))
  144. server = httpserver.MyHTTPServer(server_ip, server_port)
  145. server.serve_forever()

2) httpserver.py

  1. # -*- coding: utf-8 -*-
  2. """
  3. 兼容Python 2.X 与 3.X 版本
  4. """
  5. import appserver
  6. import base64
  7. import urllib.request
  8. from http.server import HTTPServer, BaseHTTPRequestHandler
  9. from Crypto.Signature import PKCS1_v1_5
  10. from Crypto.Hash import MD5
  11. from Crypto.PublicKey import RSA
  12. def verrify(auth_str, authorization_base64, pub_key):
  13. """
  14. 校验签名是否正确(MD5 + RAS)
  15. :param auth_str: 文本信息
  16. :param authorization_base64: 签名信息
  17. :param pub_key: 公钥
  18. :return: 若签名验证正确返回 True 否则返回 False
  19. """
  20. pub_key_load = RSA.importKey(pub_key)
  21. auth_md5 = MD5.new(auth_str.encode())
  22. result = False
  23. try:
  24. result = PKCS1_v1_5.new(pub_key_load).verify(auth_md5, base64.b64decode(authorization_base64.encode()))
  25. except Exception as e:
  26. print(e)
  27. result = False
  28. return result
  29. def get_http_request_unquote(url):
  30. return urllib.request.unquote(url)
  31. def get_pub_key(pub_key_url_base64):
  32. """ 抽取出 public key 公钥 """
  33. pub_key_url = base64.b64decode(pub_key_url_base64.encode())
  34. url_reader = urllib.request.urlopen(pub_key_url.decode())
  35. pub_key = url_reader.read()
  36. return pub_key
  37. class MyHTTPRequestHandler(BaseHTTPRequestHandler):
  38. def do_POST(self):
  39. """处理 POST 请求"""
  40. return appserver.do_POST(self)
  41. def do_GET(self):
  42. """处理 GET 请求"""
  43. appserver.do_GET(self)
  44. class MyHTTPServer(HTTPServer):
  45. def __init__(self, host, port):
  46. print("run app server by python3!")
  47. HTTPServer.__init__(self, (host, port), MyHTTPRequestHandler)