Web直传阿里云OSS服务端临签名总结 2021-01-28

好记性,不如烂笔头,趁下班时间对今天研究的阿里云OSS存储服务,通过前端直传服务进行上传的功能,做个小小的总结,方便后期查看等。

一、背景

最近公司有新的需求,需要将文件上传到阿里云OSS,目前项目中的文件主要是存储到自己的服务器,这样很容易受服务器带宽、硬件的影响,加上服务器配置不高,应用服务很容易垮掉。之前也用过OSS,但是上传文件都是比较小的10MB以内的文件,采用的是生成数据流的方式。这种情况已经无法满足现在的应用场景,就又熟悉了一下SDK文档,主要的实现方式有:

方案一:先将文件传到自己的服务器后台,然后由后台存储到OSS,删掉原来的文件。这样其实很多的缺点:

1、 上传慢。先上传到应用服务器,再上传到OSS,网络传送多了一倍。如果数据直传到OSS,不走应用服务器,速度将大大提升,而且OSS是采用BGP带宽,能保证各地各运营商的速度。

2、 扩展性不好。如果后续用户多了,应用服务器会成为瓶颈。

3、 费用高。由于OSS上传流量是免费的。如果数据直传到OSS,不走应用服务器,那么将能省下几台应用服务器。

方案二:服务端签名,前端直传

方案三:STS临时授权访问OSS

https://help.aliyun.com/document_detail/32122.html

二、签名方式与STS临时授权

2.1服务端签名后直传

2.1.1背景

采用JavaScript客户端直接签名(参见JavaScript客户端签名直传)时,AccessKey ID和AcessKey Secret会暴露在前端页面,因此存在严重的安全隐患。因此,OSS提供了服务端签名后直传的方案。

2.1.2流程介绍

流程如下图所示:

验签流程

本示例中,Web端向服务端请求签名,然后直接上传,不会对服务端产生压力,而且安全可靠。但本示例中的服务端无法实时了解用户上传了多少文件,上传了什么文件。如果想实时了解用户上传了什么文件,可以采用服务端签名直传并设置上传回调

2.2STS临时授权

OSS 可以通过阿里云 STS (Security Token Service) 进行临时授权访问。阿里云 STS 是为云计算用户提供临时访问令牌的Web服务。通过 STS,您可以为第三方应用或子用户(即用户身份由您自己管理的用户)颁发一个自定义时效和权限的访问凭证。

2.2.1使用场景

(1)对于您本地身份系统所管理的用户,比如您的 App 的用户、您的企业本地账号、第三方 App ,将这部分用户称为联盟用户。这些联盟用户可能需要直接访问 OSS 资源。此外,还可以是您创建的能访问您的阿里云资源应用程序的用户。

(2)对于这部分联盟用户,通过阿里云 STS 服务为阿里云账号(或 RAM 用户)提供短期访问权限管理。您不需要透露云账号(或 RAM 用户)的长期密钥(如登录密码、AccessKey),只需要生成一个短期访问凭证给联盟用户使用即可。这个凭证的访问权限及有效期限都可以由您自定义。您不需要关心权限撤销问题,访问凭证过期后会自动失效。

(3)通过 STS 生成的凭证包括安全令牌(SecurityToken)、临时访问密钥(AccessKeyId, AccessKeySecret)。使用AccessKey 方法与您在使用阿里云账户或 RAM 用户 AccessKey 发送请求时的方法相同。需要注意的是在每个向 OSS 发送的请求中必须携带安全令牌。

2.2.2实现原理

以一个移动 App 举例。假设您是一个移动 App 开发者,打算使用阿里云 OSS 服务来保存App 的终端用户数据,并且要保证每个 App 用户之间的数据隔离,防止一个 App 用户获取到其它 App 用户的数据。你可以使用 STS 授权用户直接访问 OSS。

2.2.3使用 STS 授权用户直接访问 OSS 的流程如下:

STS 授权用户直接访问 OSS 的流程

1、App 用户登录。App 用户和云账号无关,它是 App 的终端用户,AppServer 支持 App 用户登录。对于每个有效的 App 用户来说,需要 AppServer 能定义出每个 App 用户的最小访问权限。

2、AppServer 请求 STS 服务获取一个安全令牌(SecurityToken)。在调用 STS 之前,AppServer 需要确定 App 用户的最小访问权限(用 Policy 语法描述)以及授权的过期时间。然后通过扮演角色(AssumeRole)来获取一个代表角色身份的安全令牌。

3、 STS 返回给 AppServer 一个有效的访问凭证,包括一个安全令牌(SecurityToken)、临时访问密钥(AccessKeyId, AccessKeySecret)以及过期时间。

4、AppServer 将访问凭证返回给 ClientApp。ClientApp 可以缓存这个凭证。当凭证失效时,ClientApp 需要向 AppServer 申请新的有效访问凭证。比如,访问凭证有效期为1小时,那么 ClientApp 可以每 30 分钟向 AppServer 请求更新访问凭证。

5、ClientApp 使用本地缓存的访问凭证去请求 Aliyun Service API。云服务会感知 STS 访问凭证,并会依赖 STS 服务来验证访问凭证,正确响应用户请求。

STS 安全令牌、角色管理和使用相关内容详情,请参考 RAM 角色管理。调用 STS 服务接口AssumeRole来获取有效访问凭证即可。

三、知识图谱

知识图谱

四、临时授权与验签方法两种方式的封装

4.1添加实体对象类

 /// <summary>
    /// GetToken()函数返回的STS凭据数据模型
    /// 参考:https://www.cnblogs.com/myhalo/p/6626530.html
    /// https://help.aliyun.com/document_detail/100624.html?spm=a2c4g.11186623.2.5.600c6d13A0lSIR
    /// </summary>
    public class StsTokenModel
    {
        public int status { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public string Region { get; set; }

        /// <summary>
        /// 访问密钥标识
        /// </summary>
        public string AccessKeyId { get; set; }

        /// <summary>
        /// 访问密钥
        /// </summary>
        public string AccessKeySecret { get; set; }

        /// <summary>
        /// 安全令牌
        /// </summary>
        public string SecurityToken { get; set; }

        /// <summary>
        /// 失效时间
        /// </summary>
        public string Expiration { get; set; }

        /// <summary>
        /// Bucket名称,即目录名称
        /// </summary>
        public string BucketName { get; set; }
    }

4.2 封装STS临时授权访问OSS工具类

/// <summary>
    /// OSS工具类 2021-01-28
    /// https://blog.csdn.net/weixin_39934264/article/details/102964004
    /// OSS临时验签:https://www.cnblogs.com/myhalo/p/6626530.html
    /// </summary>
    class OSS_Helper
    {

        const string accessKeyId = "你的";
        const string accessKeySecret = "你的";
        const string endpoint = "你的";
        const string bucketName = "你的";

        #region STS Token特有
        private const string REGION_CN_HANGZHOU = "cn-hangzhou";
        private const string STS_API_VERSION = "2021-01-28";
        private const string RoleArn = "你的";//【RAM角色管理】——【单机角色】
        private const int TokenExpireTime = 3600;//过期时间,单位为秒。 过期时间最小值为900秒,最大值为MaxSessionDuration设置的时间。默认值为3600秒。
        //这里是权限配置,请参考oss的文档【RAM 访问控制】——【用户】——【权限管理】
        /*private const string PolicyFile = @"{
          ""Statement"": [
            {
              ""Action"": [
                ""oss:PutObject""
              ],
              ""Effect"": ""Allow"",
              ""Resource"": [""acs:oss:*:*:bucketName/*"", ""acs:oss:*:*:bucketName""]
            }
          ],
          ""Version"": ""1""
        }";*/
        private const string PolicyFile = @"{
            ""Statement"": [
                    {
                        ""Action"": ""oss:*"",
                        ""Effect"": ""Allow"",
                        ""Resource"": ""*""
                    }
                ],
                ""Version"": ""1""
        }";
        #endregion

        OssClient client = null;
        public OSS_Helper()
        {
            // 由用户指定的OSS访问地址、阿里云颁发的AccessKeyId/AccessKeySecret构造一个新的OssClient实例。
            client = new OssClient(endpoint, accessKeyId, accessKeySecret);
        }

        /*简单上传:文件最大不能超过5GB。
          追加上传:文件最大不能超过5GB。
          断点续传上传:支持并发、断点续传、自定义分片大小。大文件上传推荐使用断点续传。最大不能超过48.8TB。
          分片上传:当文件较大时,可以使用分片上传,最大不能超过48.8TB。*/

        /// <summary>
        /// 简易文件上传
        /// </summary>
        /// <param name="objectName">OSS文件路径</param>
        /// <param name="localFilename">本地文件路径</param>
        public void Simple_Up(string objectName, string localFilename)
        {

            //var objectName = "Project/222.jpg";
            //var localFilename = @"C:\tiger.jpg";
            // 创建OssClient实例。
            try
            {
                // 上传文件。
                client.PutObject(bucketName, objectName, localFilename);
                NLogger.Error("Put object succeeded");
            }
            catch (Exception ex)
            {
                NLogger.Error("Put object failed, {0}", ex.Message);
            }
        }



        /// <summary>
        /// 分片上传
        /// </summary>
        /// <param name="objectName">OSS文件路径</param>
        /// <param name="localFilename">本地文件路径</param>
        public void Multipar_tUp(string objectName, string localFilename)
        {
            var uploadId = "";
            try
            {
                // 定义上传文件的名字和所属存储空间。在InitiateMultipartUploadRequest中,可以设置ObjectMeta,但不必指定其中的ContentLength。
                var request = new InitiateMultipartUploadRequest(bucketName, objectName);
                var result = client.InitiateMultipartUpload(request);
                uploadId = result.UploadId;
                // 打印UploadId。
                NLogger.Error("Init multi part upload succeeded");
                NLogger.Error("Upload Id:{0}", result.UploadId);
            }
            catch (Exception ex)
            {
                throw ex;
            }
            // 计算分片总数。
            var partSize = 1024 * 1024;
            var fi = new FileInfo(localFilename);
            var fileSize = fi.Length;
            var partCount = fileSize / partSize;
            if (fileSize % partSize != 0)
            {
                partCount++;
            }
            // 开始分片上传。partETags是保存partETag的列表,OSS收到用户提交的分片列表后,会逐一验证每个分片数据的有效性。 当所有的数据分片通过验证后,OSS会将这些分片组合成一个完整的文件。
            var partETags = new List<PartETag>();
            try
            {
                using (var fs = File.Open(localFilename, FileMode.Open))
                {
                    for (var i = 0; i < partCount; i++)
                    {
                        var skipBytes = (long)partSize * i;
                        // 定位到本次上传起始位置。
                        fs.Seek(skipBytes, 0);
                        // 计算本次上传的片大小,最后一片为剩余的数据大小。
                        var size = (partSize < fileSize - skipBytes) ? partSize : (fileSize - skipBytes);
                        var request = new UploadPartRequest(bucketName, objectName, uploadId)
                        {
                            InputStream = fs,
                            PartSize = size,
                            PartNumber = i + 1
                        };
                        // 调用UploadPart接口执行上传功能,返回结果中包含了这个数据片的ETag值。
                        var result = client.UploadPart(request);
                        partETags.Add(result.PartETag);
                        NLogger.Error("finish {0}/{1}", string.Format(partETags.Count.ToString(), partCount));
                    }
                    NLogger.Error("Put multi part upload succeeded");
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
            // 列举已上传的分片。
            try
            {
                var listPartsRequest = new ListPartsRequest(bucketName, objectName, uploadId);
                var listPartsResult = client.ListParts(listPartsRequest);
                Console.WriteLine("List parts succeeded");
                // 遍历所有分片。
                var parts = listPartsResult.Parts;
                foreach (var part in parts)
                {
                    Console.WriteLine("partNumber: {0}, ETag: {1}, Size: {2}", part.PartNumber, part.ETag, part.Size);
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
            // 完成分片上传。
            try
            {
                var completeMultipartUploadRequest = new CompleteMultipartUploadRequest(bucketName, objectName, uploadId);
                foreach (var partETag in partETags)
                {
                    completeMultipartUploadRequest.PartETags.Add(partETag);
                }
                var result = client.CompleteMultipartUpload(completeMultipartUploadRequest);
                NLogger.Error("complete multi part succeeded");
            }
            catch (Exception ex)
            {
                throw ex;
            }

        }

        /// <summary>
        /// 断点续传
        /// https://help.aliyun.com/document_detail/91101.html?spm=a2c4g.11186623.6.1372.10d13529n3VwoC
        /// </summary>
        /// <param name="objectName">OSS文件路径</param>
        /// <param name="localFilename">本地文件路径</param>
        /// <param name="checkpointDir">断点续传的中间状态</param>
        public void chkin_Up(string objectName, string localFilename, string checkpointDir)
        {
            try
            {
                // 通过UploadFileRequest设置多个参数。
                UploadObjectRequest request = new UploadObjectRequest(bucketName, objectName, localFilename)
                {
                    // 指定上传的分片大小。
                    PartSize = 1024 * 1024,
                    // 指定并发线程数。
                    ParallelThreadCount = 10,
                    // checkpointDir保存断点续传的中间状态,用于失败后继续上传。如果checkpointDir为null,断点续传功能不会生效,每次失败后都会重新上传。
                    CheckpointDir = checkpointDir,
                };
                // 断点续传上传。
                client.ResumableUploadObject(request);
                //NLogger.Error("Resumable upload object:{0} succeeded", objectName);
            }
            catch (OssException ex)
            {
                NLogger.Error("Failed with error code: {0}; Error info: {1}. \nRequestID:{2}\tHostID:{3}", string.Format(ex.ErrorCode, ex.Message, ex.RequestId, ex.HostId));
            }
            catch (Exception ex)
            {
                NLogger.Error("Failed with error info: {0}", ex.Message);
            }

        }

        public void Stream_Down(string objectName, string downloadFilename)
        {
            // objectName 表示您在下载文件时需要指定的文件名称,如abc/efg/123.jpg。
            //var objectName = "Project/cc.jpg";
            //var downloadFilename = @"D:\GG.jpg";
            // 创建OssClient实例。
            //var client = new OssClient(endpoint, accessKeyId, accessKeySecret);
            try
            {
                // 下载文件到流。OssObject 包含了文件的各种信息,如文件所在的存储空间、文件名、元信息以及一个输入流。
                var obj = client.GetObject(bucketName, objectName);
                using (var requestStream = obj.Content)
                {
                    byte[] buf = new byte[1024];
                    var fs = File.Open(downloadFilename, FileMode.OpenOrCreate);
                    var len = 0;
                    // 通过输入流将文件的内容读取到文件或者内存中。
                    while ((len = requestStream.Read(buf, 0, 1024)) != 0)
                    {
                        fs.Write(buf, 0, len);
                    }
                    fs.Close();
                }
                NLogger.Error("Get object succeeded");
            }
            catch (Exception ex)
            {
                NLogger.Error("Get object failed. {0}", string.Format(ex.Message));
            }
        }

        # region 上传下载进度条
        /// <summary>
        /// 进度条:进度条用于指示上传或下载的进度。
        /// https://help.aliyun.com/document_detail/91759.html?spm=a2c4g.11186623.2.9.7c8e5d88iz39Cd
        /// </summary>
        public static void GetObjectProgress()
        {
            var endpoint = "<yourEndpoint>";
            var accessKeyId = "<yourAccessKeyId>";
            var accessKeySecret = "<yourAccessKeySecret>";
            var bucketName = "<yourBucketName>";
            var objectName = "<yourObjectName>";
            // 创建OssClient实例。
            var client = new OssClient(endpoint, accessKeyId, accessKeySecret);
            try
            {
                var getObjectRequest = new GetObjectRequest(bucketName, objectName);
                getObjectRequest.StreamTransferProgress += streamProgressCallback;
                // 下载文件。
                var ossObject = client.GetObject(getObjectRequest);
                using (var stream = ossObject.Content)
                {
                    var buffer = new byte[1024 * 1024];
                    var bytesRead = 0;
                    while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        // 处理读取的数据(此处代码省略)。
                    }
                }
                Console.WriteLine("Get object:{0} succeeded", objectName);
            }
            catch (OssException ex)
            {
                NLogger.Error("Failed with error code: {0}; Error info: {1}. \nRequestID:{2}\tHostID:{3}",
                    string.Format(ex.ErrorCode, ex.Message, ex.RequestId, ex.HostId));
            }
            catch (Exception ex)
            {
                NLogger.Error("Failed with error info: {0}", ex.Message);
            }
        }
        private static void streamProgressCallback(object sender, StreamTransferProgressArgs args)
        {
            NLogger.Error("ProgressCallback - Progress: {0}%, TotalBytes:{1}, TransferredBytes:{2} ",
              string.Format((args.TransferredBytes * 100 / args.TotalBytes).ToString(), args.TotalBytes, args.TransferredBytes));
        }
        #endregion


        /// <summary>
        /// 删除指定的单个文件
        /// https://help.aliyun.com/document_detail/91924.html?spm=a2c4g.11186623.2.12.61937ff0mNZJra
        /// </summary>   
        /// <param name="key">待删除的文件名称</param>
        public void DeleteObject(string key)
        {
            try
            {   //bucketName:文件所在存储空间的名称
                client.DeleteObject(bucketName, key);
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        /// <summary>
        /// 批量删除文件
        /// </summary>
        public void DeleteBatchObject()
        {
            // 创建OssClient实例。
            try
            {
                var keys = new List<string>();
                var listResult = client.ListObjects(bucketName);
                foreach (var summary in listResult.ObjectSummaries)
                {
                    keys.Add(summary.Key);
                }
                // quietMode为true表示简单模式,为false表示详细模式。默认为详细模式。
                var quietMode = false;
                // DeleteObjectsRequest的第三个参数指定返回模式。
                var request = new DeleteObjectsRequest(bucketName, keys, quietMode);
                // 删除多个文件。
                var result = client.DeleteObjects(request);
                if ((!quietMode) && (result.Keys != null))
                {
                    foreach (var obj in result.Keys)
                    {
                        NLogger.Error("Delete successfully : {0} ", obj.Key);
                    }
                }
                NLogger.Error("Delete objects succeeded");
            }
            catch (Exception ex)
            {
                NLogger.Error("Delete objects failed. {0}", ex.Message);
            }
        }

        #region STS  Token验签   STS临时授权访问OSS
        /// <summary>
        /// https://help.aliyun.com/document_detail/28763.html?spm=a2c4g.11186623.2.13.3479606cQK2HOg
        /// </summary>
        /// <param name="accessKeyId"></param>
        /// <param name="accessKeySecret"></param>
        /// <param name="roleArn">指定角色的ARN。格式:acs:ram::$accountID:role/$roleName 。</param>
        /// <param name="roleSessionName">用户自定义参数。此参数用来区分不同的令牌,可用于用户级别的访问审计。</param>
        /// <param name="policy">权限策略。生成STS Token时可以指定一个额外的权限策略,以进一步限制STS Token的权限。若不指定则返回的Token拥有指定角色的所有权限。长度为1 ~1024个字符。</param>
        /// <param name="protocolType"></param>
        /// <param name="durationSeconds">过期时间,单位为秒。过期时间最小值为900秒,最大值为MaxSessionDuration设置的时间。默认值为3600秒。</param>
        /// <returns></returns>
        private AssumeRoleResponse assumeRole(String accessKeyId, String accessKeySecret, String roleArn,
           String roleSessionName, String policy, ProtocolType protocolType, long durationSeconds)
        {
            try
            {
                // 创建一个 Aliyun Acs Client, 用于发起 OpenAPI 请求
                IClientProfile profile = DefaultProfile.GetProfile(REGION_CN_HANGZHOU, accessKeyId, accessKeySecret);
                DefaultAcsClient client = new DefaultAcsClient(profile);

                // 创建一个 AssumeRoleRequest 并设置请求参数
                AssumeRoleRequest request = new AssumeRoleRequest();
                //request.Version = STS_API_VERSION;
                request.Method = MethodType.POST;
                //request.Protocol = protocolType;

                request.RoleArn = roleArn;
                request.RoleSessionName = roleSessionName;
                request.Policy = policy;
                request.DurationSeconds = durationSeconds;

                // 发起请求,并得到response
                AssumeRoleResponse response = client.GetAcsResponse(request);

                return response;
            }
            catch (ClientException e)
            {
                throw e;
            }
        }

        /// <summary>
        /// 获取token STS临时授权访问OSS 2021-01-28
        /// </summary>
        /// <returns></returns>
        public StsTokenModel GetToken()
        {
            // 只有 RAM用户(子账号)才能调用 AssumeRole 接口
            // 阿里云主账号的AccessKeys不能用于发起AssumeRole请求
            // 请首先在RAM控制台创建一个RAM用户,并为这个用户创建AccessKeys

            // RoleArn 需要在 RAM 控制台上获取
            // RoleSessionName 是临时Token的会话名称,自己指定用于标识你的用户,主要用于审计,或者用于区分Token颁发给谁
            // 但是注意RoleSessionName的长度和规则,不要有空格,只能有'-' '_' 字母和数字等字符
            // 具体规则请参考API文档中的格式要求
            //string roleSessionName = "alice-001";
            string roleSessionName = "HaiGongJiang-001";
            // 必须为 HTTPS
            try
            {
                AssumeRoleResponse stsResponse = assumeRole(accessKeyId, accessKeySecret, RoleArn, roleSessionName,
                        PolicyFile, ProtocolType.HTTPS, TokenExpireTime);

                return new StsTokenModel()
                {
                    status = 200,
                    Region = "oss-cn-beijing",//你的Region  注意 这个只要 空间名 不要 http:// 和 .aliyunoss.com !!,
                    AccessKeyId = stsResponse.Credentials.AccessKeyId,
                    AccessKeySecret = stsResponse.Credentials.AccessKeySecret,
                    Expiration = stsResponse.Credentials.Expiration,
                    SecurityToken = stsResponse.Credentials.SecurityToken,
                    BucketName = bucketName
                };

            }
            catch (ClientException e)
            {
                return new StsTokenModel() { status = 500};
            }
        }
        #endregion

        #region 前端验签方法policy,未使用
        public static string HmacSha1Sign(string text, string key)
        {
            Encoding encode = Encoding.UTF8;
            byte[] byteData = encode.GetBytes(text);
            byte[] byteKey = encode.GetBytes(key);
            HMACSHA1 hmac = new HMACSHA1(byteKey);
            CryptoStream cs = new CryptoStream(Stream.Null, hmac, CryptoStreamMode.Write);
            cs.Write(byteData, 0, byteData.Length);
            cs.Close();
            return Convert.ToBase64String(hmac.Hash);
        }

        public int ConvertDateTimeInt(System.DateTime time)
        {
            System.DateTime startTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            return (int)(time - startTime).TotalSeconds;
        }
        #endregion
    }

4.3 Controller层调用,暴露给前端

4.3.1STS临时授权

       /// <summary>
        /// 函数说明:获取STS  Token  STS临时授权访问OSS 2021-01-28
        /// 参考地址:https://help.aliyun.com/document_detail/100624.html?spm=a2c4g.11186623.2.5.600c6d13A0lSIR
        /// </summary>
        /// <returns>返回临时token认证信息https://help.aliyun.com/document_detail/28763.html?spm=a2c4g.11186623.2.13.3479606cQK2HOg</returns>
        [HttpGet]
        public Task<ResponseMessage> GetSTSsecurityToken()
        {
            var response = new MessageModel<StsTokenModel>();
            try
            {
                IUser user = (IUser)this.Request.GetRouteData().Values["User"];
                OSS_Helper oss = new OSS_Helper();
                var stsmodel = oss.GetToken();
                response.Data = stsmodel;
                return Task.FromResult(response.SetError(GlobalErrorCode.OK));
            }
            catch (Exception ex)
            {
                NLogger.Exception(ex);
                return Task.FromResult(response.SetError(GlobalErrorCode.SystemError));
            }
        }

4.3.2policy验签

/// <summary>
        /// 前端验签方法policy:需要前端验证,待测试,暂未使用该验签方式
        /// https://help.aliyun.com/document_detail/100680.html?spm=a2c4g.11186623.2.10.3479606cQK2HOg#section-an0-sb1-5sh
        /// </summary>
        /// <returns>返回签名认证信息</returns>
        [HttpGet]
        public IHttpActionResult GetToken()
        {
            const string accessKeyId = "你的";
            const string accessKeySecret = "你的";
            const string endpoint = "oss-cn-beijing.aliyuncs.com";
            const string bucketName = "你的";
            const string hostURL = "http://" + bucketName + "." + endpoint;//
           
            DateTime dt = DateTime.Now.AddMinutes(10).AddHours(-8);
            string expire = dt.ToString("yyyy-MM-ddTHH:mm:ssZ");
            string accessid = accessKeyId;
            string accesskey = accessKeySecret;
            string host = hostURL;
            long contentLength = 10737418240;// 最大上传文件大小(10M)
            long timestamp = (dt.ToUniversalTime().Ticks - 621355968000000000) / 10000000;
            string dir = $"website/upload/img/{DateTime.Now.ToString("yyyyMMdd")}/";
            string key = Guid.NewGuid().ToString("N");

            string policy = "{\"expiration\":\"" + expire + "\",\"conditions\":[[\"content-length-range\",0," + contentLength + "],[\"starts-with\",\"$key\",\"" +
                            dir + "\"]]}";
            string base64Policy = Convert.ToBase64String(System.Text.Encoding.Default.GetBytes(policy));
            string signature = OSS_Helper.HmacSha1Sign(base64Policy, accesskey);


            //文件名称不需要后续名,这里取不到后缀名
            var token = new
            {
                accessid = accessid,
                host = host,
                policy = base64Policy,
                signature = signature,
                expire = timestamp,
                dir = dir,
                filename = key
            };
           
            return Json(token);
        }

4.4前端调用

前端调用,请根据自身情况进行调用,下面是vue的示例

(1)安装web前端接口框架:

npm install ali-oss

(2)封装client.js工具类

//Client.js 
const OSS = require('ali-oss');

export default function Client(data) {
    //后台返回数据
    return new OSS({
        region: data.Region,
        bucket: data.BucketName,
        accessKeyId: data.AccessKeyId,
        accessKeySecret: data.AccessKeySecret,
        securityToken: data.SecurityToken
    })
}
client.js工具类

(3)完整示例如下:

<template>
  <div class="upload_temp">
      <!-- 上传文件 -->
        <el-upload
              class="upload-demo"
              ref="upload"
              drag
              :before-upload="beforeUpload"
              :on-success="handleSuccess"
              :http-request="handleHttpRequest"
              :headers="uploadHeaders"
              :limit="files"
              :disabled="disabled"
              multiple
              action
              :file-list="fileList"
            >
              <i class="el-icon-upload"></i>
            <div class="el-upload__text">
                将文件拖到此处,或
                <em>点击上传</em>
            </div>
            <div slot="tip" class="el-upload__tip">上传文件大小不能超过 1G</div>
        </el-upload>
        
        
        <div @click="download_file()" style='margin-top:200px;'>下载</div>
            
  </div>
</template>

<script>
    import Client from "@/utils/client";
    const OSS = require('ali-oss');
export default {
    name: 'upload_temp',
    data () {
        return {
            dataObj: {},
            fileList: [],
            files: 10,
            uploadHeaders: {
                authorization: "*"
            },
            disabled: false,
            fileName: ""
        }
    },
    methods:{
        
        getAliToken() {
            return new Promise((resolve, reject) => {
            //请求后台接口返回授权数据
                this.$axios.get(this.$axios.defaults.baseURL + 'api/File/GetSTSsecurityToken')
                    .then((res) => {
                        console.log(JSON.parse(JSON.stringify(res.data)))
                        if(res.data.ErrorCode == 200){
                            this.dataObj = {
                              region: res.data.Data.Region,
                              bucket: res.data.Data.BucketName,
                              accessKeyId: res.data.Data.AccessKeyId,
                              accessKeySecret: res.data.Data.AccessKeySecret,
                              stsToken: res.data.Data.SecurityToken
                            };
                            resolve(true);
                        }
                    })
                    .catch(function (error) {
                        console.log(error);
                        reject(false);
                    });
                
                // this.dataObj = {
                //  region: "oss-cn-beijing",
                //  bucket:"testhgj",
                //  accessKeyId:"LTAI4G9C5TAVuWL4TEgdvJUk",
                //  accessKeySecret: "7qIhJ5p4rWw1vkj6snFptyvHfyiF15",
                //  // security: response.data.security
                // };
                // resolve(true);
                
            });
            
        },
        beforeUpload(file) {
          return new Promise((resolve, reject) => {
            this.getAliToken()
              .then(response => {
                if (response) {
                  resolve(response);
                } else {
                  reject(response);
                }
              })
              .catch(err => {
                console.log(err);
                reject(err);
              });
          });
        },
        async handleHttpRequest(option) {
          console.log(option);
          //上传OSS
          try {
            let vm = this;
            vm.disabled = true;
            console.log(JSON.parse(JSON.stringify(this.dataObj)))
            const client = new OSS(this.dataObj)
            const file = option.file;
            await client
              .multipartUpload(option.file.name, file, {
                progress: async function(p) {
                  let e = {};
                  e.percent = p * 100;
                  option.onProgress(e);
                }
              })
              .then(({ res }) => {
                console.log(res);
                if (res.statusCode === 200) {
                  // option.onSuccess(ret)
                  console.log(res.requestUrls);
                  return res.requestUrls;
                } else {
                  vm.disabled = false;
                  option.onError("上传失败");
                }
              })
              .catch(error => {
                vm.disabled = false;
                option.onError("上传失败");
              });
          } catch (error) {
            console.error(error);
            this.disabled = false;
            option.onError("上传失败");
          }
        },
        handleSuccess(response, file, fileList) {
          console.log(response);
          console.log(file);
          console.log(fileList);
        },
        
        // 下载阿里oss文件方法
        download_file(){
            var key = "Project/edraw-max_cn_setup_full5676.exe";
            const client = Client(this.dataObj);
            var url = client.signatureUrl(key, {
                response: {
                    'content-disposition': 'attachment; filename="' + 'test_file.exe' + '"'
                }
            })
            const a = document.createElement('a'); // 创建a标签
            a.setAttribute('download', '');// download属性
            a.setAttribute('target', 'blank');// download属性
            a.setAttribute('href',url);// href链接 
            a.click();// 自执行点击事件
        },
            
    },
    watch: {
        url(val) {
          if (val) {
            this.urls.push(val);
          }
        }
      },
    mounted:function() {
        
    }

}
</script>

<style scoped>
    .upload_temp{width:80%;margin:auto;height: auto;background-color: #F4F4F4;padding: 23px 0 30px 0;}
    
    /* 上传 */
    .upload_temp .avatar-uploader .el-upload {border: 1px dashed #d9d9d9;border-radius: 6px;cursor: pointer;position: relative;overflow: hidden;}
    .upload_temp .avatar-uploader .el-upload:hover { border-color: #409EFF;}
    .upload_temp .avatar-uploader-icon {font-size: 28px;color: #8c939d;width: 90px;height: 90px;line-height: 90px;text-align: center;border: 1px solid #ddd;}
</style>

五、总结

我们在项目中最终采用的是STS临时授权方案。

STS的优势如下:

  • 您无需透露您的长期密钥(AccessKey)给第三方应用,只需生成一个访问令牌并将令牌交给第三方应用。您可以自定义这个令牌的访问权限及有效期限。
  • 您无需关心权限撤销问题,访问令牌过期后自动失效。

关于STS的介绍请查阅阿里云官方文档:

https://help.aliyun.com/document_detail/32093.html?spm=a2c4g.11186623.6.1409.13107d9ckOKzS6

六、参考资料

在这边不得不吐槽一下OSS的API,是真的很烂,基本找不到好的方法,都是基于百度才做出来的,当然,我使用的方法估计还有一些坑,只是能实现了我的功能。

STS临时授权访问OSS

https://help.aliyun.com/document_detail/100624.html?spm=a2c4g.11186623.2.5.600c6d13A0lSIR

vue直传OSS

https://blog.csdn.net/qq_33270001/article/details/88999189

el-upload组件结合上传阿里云OSS实现更优交互

https://blog.csdn.net/fifteen718/article/details/85259438

Web直传OSS

https://blog.csdn.net/weixin_33907511/article/details/91479830

OSS文件上传(页面直传)

https://blog.csdn.net/linlin_0904/article/details/84583676

请问STS和签名带Policy的差别

https://developer.aliyun.com/ask/205943?spm=a2c6h.13524658

STS临时授权访问OSS

https://www.cnblogs.com/ggband/p/10218851.html

vue+element+sts临时授权上传大文件到阿里云OSS时踩过的坑。

https://blog.csdn.net/aiguo94/article/details/111832776

Vue上传阿里云OSS(STS方式)

https://blog.csdn.net/qq_35775675/article/details/92797782

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容