本文以Python语言为例,讲解在服务端通过Python代码完成签名,并且设置上传回调,然后通过表单直传数据到OSS。

前提条件

  • 应用服务器对应的域名可通过公网访问。
  • 确保应用服务器已经安装Python 2.6以上版本(执行python --version命令进行查看)。
  • 确保PC端浏览器支持JavaScript。

步骤1:配置应用服务器

  1. 下载应用服务器源码(Python版本)。
  2. 本示例中以Ubuntu 16.04为例,将源码解压到/home/aliyun/aliyun-oss-appserver-python目录下。
  3. 进入该目录,打开源码文件appserver.py,修改如下的代码片段:
    # 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
    access_key_id = 'yourAccessKeyId'
    access_key_secret = 'yourAccessKeySecret'
    # 填写Host地址,格式为https://bucketname.endpoint。
    host = 'https://examplebucket.oss-cn-hangzhou.aliyuncs.comm';
    # 设置上传回调URL,即回调服务器地址,用于处理应用服务器与OSS之间的通信。OSS会在文件上传完成后,把文件上传信息通过此回调URL发送给应用服务器。
    callback_url = "https://192.168.0.0:8888";
    # 设置上传到OSS文件的前缀,可置空此项。置空后,文件将上传至Bucket的根目录下。
    upload_dir = 'exampledir/'

步骤2:配置客户端

  1. 下载客户端源码
  2. 解压客户端源码文件,本示例解压到D:\aliyun\aliyun-oss-appserver-js目录。
  3. 进入该目录,打开upload.js文件,找到下面的代码语句:
    // serverUrl是用户获取签名和Policy等信息的应用服务器的URL,请对应替换以下IP地址和Port端口信息。
    serverUrl = 'http://192.0.2.0:8888'
  4. severUrl改成应用服务器的地址,客户端可以通过它可以获取签名直传Policy等信息。如本例中可修改为:serverUrl = 'https://192.168.0.0:8888'

步骤3:修改CORS

客户端进行表单直传到OSS时,会从浏览器向OSS发送带有Origin的请求消息。OSS对带有Origin头的请求消息会进行跨域规则(CORS)的验证。因此需要为Bucket设置跨域规则以支持Post方法。

  1. 登录OSS管理控制台
  2. 单击Bucket列表,然后单击目标Bucket名称。
  3. 在左侧导航栏,选择权限管理 > 跨域设置,然后在跨域设置区域,单击设置
  4. 单击创建规则,配置如下图所示。
    说明 为了您的数据安全,实际使用时,来源建议填写实际允许访问的域名。更多配置信息请参见设置跨域访问

步骤4:体验上传回调

  1. 启动应用服务器。
    /home/aliyun-oss-appserver-python目录下,执行python appserver.py 11.22.33.44 1234命令启动应用服务器。
    说明 请将IP地址和端口改成您配置的应用服务器的IP地址和端口。
  2. 启动客户端。
    在PC侧的客户端源码目录中,打开index.html文件。
    注意 index.html文件不保证兼容IE 10以下版本浏览器,若使用IE 10以下版本浏览器出现问题时,您需要自行调试。
  3. 上传文件。
    单击选择文件,选择指定类型的文件后,单击开始上传。上传成功后,显示回调服务器返回的内容。

应用服务器核心代码解析

应用服务器源码包含了签名直传服务以及上传回调服务,完整示例代码如下:

# -*- coding: UTF-8 -*-

import socket
import base64
import sys
import time
import datetime
import json
import hmac
from hashlib import sha1 as sha

import httpserver

# 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
access_key_id = 'yourAccessKeyId'
access_key_secret = 'yourAccessKeySecret'
# 填写Host地址,格式为https://bucketname.endpoint。
host = 'https://examplebucket.oss-cn-hangzhou.aliyuncs.com';
# 设置上传回调URL,即回调服务器地址,用于处理应用服务器与OSS之间的通信。OSS会在文件上传完成后,把文件上传信息通过此回调URL发送给应用服务器。
callback_url = "https://192.168.0.0:8888";
# 设置上传到OSS文件的前缀,可置空此项。置空后,文件将上传至Bucket的根目录下。
upload_dir = 'exampledir/'
expire_time = 30


def get_iso_8601(expire):
    gmt = datetime.datetime.utcfromtimestamp(expire).isoformat()
    gmt += 'Z'
    return gmt


def get_token():
    now = int(time.time())
    expire_syncpoint = now + expire_time
    expire_syncpoint = 1612345678
    expire = get_iso_8601(expire_syncpoint)

    policy_dict = {}
    policy_dict['expiration'] = expire
    condition_array = []
    array_item = []
    array_item.append('starts-with');
    array_item.append('$key');
    array_item.append(upload_dir);
    condition_array.append(array_item)
    policy_dict['conditions'] = condition_array
    policy = json.dumps(policy_dict).strip()
    policy_encode = base64.b64encode(policy.encode())
    h = hmac.new(access_key_secret.encode(), policy_encode, sha)
    sign_result = base64.encodestring(h.digest()).strip()

    callback_dict = {}
    callback_dict['callbackUrl'] = callback_url;
    callback_dict['callbackBody'] = 'filename=${object}&size=${size}&mimeType=${mimeType}' \
                                    '&height=${imageInfo.height}&width=${imageInfo.width}';
    callback_dict['callbackBodyType'] = 'application/x-www-form-urlencoded';
    callback_param = json.dumps(callback_dict).strip()
    base64_callback_body = base64.b64encode(callback_param.encode());

    token_dict = {}
    token_dict['accessid'] = access_key_id
    token_dict['host'] = host
    token_dict['policy'] = policy_encode.decode()
    token_dict['signature'] = sign_result.decode()
    token_dict['expire'] = expire_syncpoint
    token_dict['dir'] = upload_dir
    token_dict['callback'] = base64_callback_body.decode()
    result = json.dumps(token_dict)
    return result


def get_local_ip():
    """
    获取本机IPV4地址。
    :return: 成功获取,则返回本机IP地址。获取失败,则返回为空。
    """
    try:
        csocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        csocket.connect(('198.51.100.0', 80))
        (addr, port) = csocket.getsockname()
        csocket.close()
        return addr
    except socket.error:
        return ""


def do_POST(server):
    """
    启用POST调用处理逻辑。
    :param server: Web HTTP Server服务
    :return:
    """
    print("********************* do_POST ")
    # get public key
    pub_key_url = ''

    try:
        pub_key_url_base64 = server.headers['x-oss-pub-key-url']
        pub_key = httpserver.get_pub_key(pub_key_url_base64)
    except Exception as e:
        print(str(e))
        print('Get pub key failed! pub_key_url : ' + pub_key_url)
        server.send_response(400)
        server.end_headers()
        return

    # get authorization
    authorization_base64 = server.headers['authorization']

    # get callback body
    content_length = server.headers['content-length']
    callback_body = server.rfile.read(int(content_length))

    # compose authorization string
    auth_str = ''
    pos = server.path.find('?')
    if -1 == pos:
        auth_str = server.path + '\n' + callback_body.decode()
    else:
        auth_str = httpserver.get_http_request_unquote(server.path[0:pos]) + server.path[pos:] + '\n' + callback_body

    result = httpserver.verrify(auth_str, authorization_base64, pub_key)

    if not result:
        print('Authorization verify failed!')
        print('Public key : %s' % (pub_key))
        print('Auth string : %s' % (auth_str))
        server.send_response(400)
        server.end_headers()
        return

    # response to OSS
    resp_body = '{"Status":"OK"}'
    server.send_response(200)
    server.send_header('Content-Type', 'application/json')
    server.send_header('Content-Length', str(len(resp_body)))
    server.end_headers()
    server.wfile.write(resp_body.encode())


def do_GET(server):
    """
    启用Get调用处理逻辑
    :param server: Web HTTP Server服务
    :return:
    """
    print("********************* do_GET ")
    token = get_token()
    server.send_response(200)
    server.send_header('Access-Control-Allow-Methods', 'POST')
    server.send_header('Access-Control-Allow-Origin', '*')
    server.send_header('Content-Type', 'text/html; charset=UTF-8')
    server.end_headers()
    server.wfile.write(token.encode())


if '__main__' == __name__:
    # 在服务器中, 0.0.0.0指的是本机上的所有IPV4地址。
    # 如果一个主机有两个IP地址,例如192.0.2.0和192.0.2.254, 并且该主机上的一个服务监听的地址是0.0.0.0, 则通过这两个IP地址均能够访问该服务。
    # server_ip = get_local_ip() 如果您希望监听本机外网IPV4地址,则采用本行代码并注释掉下一行代码。
    server_ip = "0.0.0.0"
    server_port = 8080
    if len(sys.argv) == 2:
        server_port = int(sys.argv[1])
    if len(sys.argv) == 3:
        server_ip = sys.argv[1]
        server_port = int(sys.argv[2])
    print("App server is running on http://%s:%s " % (server_ip, server_port))

    server = httpserver.MyHTTPServer(server_ip, server_port)
    server.serve_forever()            

关于上传回调的API接口说明,请参见Callback