php实现阿里OSS 服务端签名后直传

后端 2019-03-02

目的:使用web直传方式可减少与当前业务服务器的交互,减少服务器的存储空间。

首先通过官网了解web直传的基本原理
15484047101472_zh-CN.png

可以看出服务端签名后直传的原理大致如下:

  1. 用户发送上传Policy请求到应用服务器。
  2. 应用服务器返回上传Policy和签名给用户。
  3. 用户直接上传数据到OSS。

后端功能代码

创建 alioss直传文件获取签名方法并设置回调

获取签名类

<?php
namespace app\index\controller;
use think\Controller;
use think\Request;

class Alioss 
{

    public function AliossPolicy(Request $request)
    {
  
        $oss = array();
        $oss = config('aliyun_oss');//alioss 配置参数 

        $data = $request->get();//url中get方式传来的信息  可根据实际情况进行获取

        $id= $oss['KeyId'];
        $key= $oss['KeySecret'];
        $host = 'https://'.$oss['Bucket'].'.'.$oss['Endpoint'];

        $callbackUrl = 'https://'.$_SERVER['HTTP_HOST'].DS.'index/index/callback';//回调地址
        $callback_param = array('callbackUrl'=>$callbackUrl, 
                    'callbackBody'=>'filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}', 
                    'callbackBodyType'=>"application/x-www-form-urlencoded");
        $callback_string = json_encode($callback_param);

        $base64_callback_body = base64_encode($callback_string);
        $now = time();
        $expire = 10; //设置该policy超时时间是10s. 即这个policy过了这个有效时间,将不能访问
        $end = $now + $expire;
        $expiration = $this->gmt_iso8601($end);

        $dir = '/uploads/'.date('Ym',time()).'/';//文件在oss中保存目录

        //最大文件大小.用户可以自己设置
        $condition = array(0=>'content-length-range', 1=>0, 2=>1048576000);
        $conditions[] = $condition; 

        //表示用户上传的数据,必须是以$dir开始, 不然上传会失败,这一步不是必须项,只是为了安全起见,防止用户通过policy上传到别人的目录
        $start = array(0=>'starts-with', 1=>'$key', 2=>$dir);
        $conditions[] = $start; 

        $arr = array('expiration'=>$expiration,'conditions'=>$conditions);
        //echo json_encode($arr);
        //return;
        $policy = json_encode($arr);
        $base64_policy = base64_encode($policy);
        $string_to_sign = $base64_policy;
        $signature = base64_encode(hash_hmac('sha1', $string_to_sign, $key, true));

        $response = array();
        $response['accessid'] = $id;
        $response['host'] = $host;
        $response['policy'] = $base64_policy;
        $response['signature'] = $signature;
        $response['expire'] = $end;
        $response['callback'] = $base64_callback_body;
        $response['dir'] = $dir;
        return json($response);
    }

    private function gmt_iso8601($time) {
        $dtStr = date("c", $time);
        $mydatetime = new \DateTime($dtStr);
        $expiration = $mydatetime->format(\DateTime::ISO8601);
        $pos = strpos($expiration, '+');
        $expiration = substr($expiration, 0, $pos);
        return $expiration."Z";
    }
}

回调类

<?php
namespace app\index\controller;

use think\Controller;
use think\Request;

class Alioss
{
    public function callback(Request $request)
    {
        // 1.获取OSS的签名header和公钥url header
        $authorizationBase64 = "";
        $pubKeyUrlBase64 = "";
        /*
        * 注意:如果要使用HTTP_AUTHORIZATION头,你需要先在apache或者nginx中设置rewrite,以apache为例,修改
        * 配置文件/etc/httpd/conf/httpd.conf(以你的apache安装路径为准),在DirectoryIndex index.php这行下面增加以下两行
            RewriteEngine On
            RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization},last]
        * */
        if (isset($_SERVER['HTTP_AUTHORIZATION']))
        {
            $authorizationBase64 = $_SERVER['HTTP_AUTHORIZATION'];
        }
        if (isset($_SERVER['HTTP_X_OSS_PUB_KEY_URL']))
        {
            $pubKeyUrlBase64 = $_SERVER['HTTP_X_OSS_PUB_KEY_URL'];
        }
        if ($authorizationBase64 == '' || $pubKeyUrlBase64 == '')
        {
            header("http/1.1 403 Forbidden");
            exit();
        }

        // 2.获取OSS的签名
        $authorization = base64_decode($authorizationBase64);

        // 3.获取公钥
        $pubKeyUrl = base64_decode($pubKeyUrlBase64);
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $pubKeyUrl);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
        $pubKey = curl_exec($ch);
        if ($pubKey == "")
        {
            //header("http/1.1 403 Forbidden");
            exit();
        }

        // 4.获取回调body
        $body = file_get_contents('php://input');

        // 5.拼接待签名字符串
        $authStr = '';
        $path = $_SERVER['REQUEST_URI'];
        $pos = strpos($path, '?');
        if ($pos === false)
        {
            $authStr = urldecode($path)."\n".$body;
        }
        else
        {
            $authStr = urldecode(substr($path, 0, $pos)).substr($path, $pos, strlen($path) - $pos)."\n".$body;
        }

        // 6.验证签名
        $ok = openssl_verify($authStr, $authorization, $pubKey, OPENSSL_ALGO_MD5);
        if ($ok == 1)
        {
            $param = $request->param();  
            //根据返回的信息 $param 根据需要保存自数据库


        }
        else{
            exit();
        }
    }
}

前端javascript功能代码

 //policyurl 获取签名的地址
    var policyurl = '/index/index/policy',
        accessid = '',
        accesskey = '',
        host = '',
        policyBase64 = '',
        signature = '',
        callbackbody = '',
        filename = '',
        key = '',
        expire = 0,
        now = timestamp = Date.parse(new Date()) / 1000;

    function send_request() {
        autoname = random_string(10);
        var xmlhttp = null;
        if (window.XMLHttpRequest) {
            xmlhttp = new XMLHttpRequest();
        } else if (window.ActiveXObject) {
            xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
        }

        if (xmlhttp != null) {
            xmlhttp.open("GET", policyurl + autoname, false);
            xmlhttp.send(null);
            return xmlhttp.responseText
        } else {
            alert('浏览器不支持此操作');
        }
    };

    function upload(){
        now = timestamp = Date.parse(new Date()) / 1000;
        if (expire < now + 3) {
            body = send_request()
            var obj = eval("(" + body + ")");
            host = obj['host']
            policyBase64 = obj['policy']
            accessid = obj['accessid']
            signature = obj['signature']
            expire = parseInt(obj['expire'])
            callbackbody = obj['callback']
            key = obj['dir']
        }

        var file = $("#selectfiles")[0].files[0];//
        var suffix = get_suffix(file.name);

        //组装发送数据
        var request = new FormData();
        request.append('name', file.name);
        request.append("key", key + autoname + suffix);//文件名字,可设置路径
        request.append("policy", policyBase64);//policy规定了请求的表单域的合法性
        request.append("OSSAccessKeyId", accessid);//Bucket 拥有者的Access Key Id。
        request.append("success_action_status", '200');// 让服务端返回200,不然,默认会返回204
        request.append("callback", callbackbody);//回调,非必选,可以在policy中自定义
        request.append("Signature", signature);//根据Access Key Secret和policy计算的签名信息,OSS验证该签名信息从而验证该Post请求的合法性
        request.append('file', file);//需要上传的文件 file  

        $.ajax({
            url: host,  
            data: request,
            processData: false,//默认true,设置为 false,不需要进行序列化处理
            cache: false,//设置为false将不会从浏览器缓存中加载请求信息
            async: false,//发送同步请求
            contentType: false,//避免服务器不能正常解析文件---------具体的可以查下这些参数的含义
            dataType: 'json',//不涉及跨域  写json即可
            type: 'post',
            success: function (callbackHost, request) { //callbackHost:success,request中就是 回调的一些信息,包括状态码什么的 
                var fileurl = callbackHost.fullurl; //上传完成后的文件oss地址
            },
            error: function (returndata) {
                alert("upload failed");
            }
        });
    }

    function random_string(len) {
        len = len || 32;
        var chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
        var maxPos = chars.length;
        var pwd = '';
        for (i = 0; i < len; i++) {
            pwd += chars.charAt(Math.floor(Math.random() * maxPos));
        }
        return Date.parse(new Date()) / 1000 + '_' + pwd;
    }
    
    function get_suffix(filename) {
        pos = filename.lastIndexOf('.')
        suffix = ''
        if (pos != -1) {
            suffix = filename.substring(pos)
        }
        return suffix;
    }