如何正确的使用对象存储

508 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情

OSS 简介

TL;DR,所以我简化为两个QA。

Q: OSS 是什么?

A: 翻译过来叫做对象存储,依托于其兄弟 CDN 可以提供可靠,快速的存储,分发服务。

Q: OSS 有哪些使用场景?

A:

  • 存储,分发
  • 备份、归档
  • 富媒体处理
  • 静态资源托管

架构图

就从七牛抠一张图下来,在所有的 OSS 服务中,七牛的文档是写的最好的。这点阿里云,腾讯云的官方文档明显在划水。

image-20220425130202030.png

后端

后端需要进行的操作大概分成以下几个步骤

配置

这里是一个最基础的配置,这里会提供下面几个操作需要的信息。

qiniu:
  ak: xxxxxx
  sk: xxxxxx
  dev:
    host: 这里是 oss  host
    bucket: abc-image-stg
    callback_url: 服务器处理回调的地址
    callback_body: 'key=$(key)&fsize=$(fsize)&info=$(imageInfo)&uid=$(x:uid)'
  prod:
    host: 这里是 oss  host
    bucket: abc-image-prod
    callback_url: 服务器处理回调的地址
    callback_body: 'key=$(key)&fsize=$(fsize)&info=$(imageInfo)&uid=$(x:uid)'

生成 upload token

这里简单的写一写逻辑,关键的地方就是构建一个包含 callback_urlcallback_body 的 PutPolicy

class Video
  def self.uptoken
    env = ENV['RACK_ENV'] == 'production' ? 'prod' : 'dev'
    put_policy = Qiniu::Auth::PutPolicy.new(NORAML_CONFIG['qiniu']["#{env}"]['bucket'], nil, 3600 * 12, global: 'global')
    #构建回调策略,这里上传文件到七牛后, 七牛将文件名和文件大小回调给业务服务器.
    put_policy.callback_url = NORAML_CONFIG['qiniu']["#{env}"]['callback_url']
    put_policy.callback_body = NORAML_CONFIG['qiniu']["#{env}"]['callback_body']
    #生成上传 Token
    uptoken = Qiniu::Auth.generate_uptoken(put_policy)
  end
end
​
// GET
// 这个接口是给前端提供的,会生成一个合法的 token 供 SDK 使用。
on 'uptoken' do
  as_json do
    { data: { uptoken: Video.uptoken } }
  end
end

处理回调

在拿到以上的 upload token 之后前端上传文件,OSS 端会发起一个回调,频率以及次数会在文档中说明,所以我们后端需要一个处理回调内容的接口,在这里我们在自己的数据库中存储文件的元信息,比如 key,ext,size这些。

举个例子,上一块提到的 callback_url 就是你服务器处理回调的地址,callback_body就是对方 OSS 服务器给你服务器 POST 的内容。(⚠️ 这里是可以带上鉴权的。所以不需要担心安全的问题。重复一遍。你不需要担心安全的问题。)

Video 数据结构
==================================================
id                                         int(11)
key                                   varchar(255)
ext                                   varchar(255)
avinfo                                        text
created_at                                datetime
updated_at                                datetime
zip_key                               varchar(255)
cleaned                                 tinyint(1)
cut_key                               varchar(255)
--------------------------------------------------
​
// POST
on param('key'), param('ext'),param('info') do |key, ext, avinfo|
  video = Video.find_or_create(key: key) do |v|
    v.ext = ext
    v.avinfo = avinfo
  end
  as_json {{key: video.key}}
end

生成 download url

这里会生成一个带上 token https://image.abc.com?e=blahblah这种形式的地址。

def full_download_url
  env = ENV['RACK_ENV'] == 'production' ? 'prod' : 'dev'
  Qiniu::Auth.authorize_download_url "#{NORAML_CONFIG['qiniu']["#{env}"]['host']}/#{self.key}", {expires_in: 3600 * 2 }
end

前端

使用服务器提供的 upload token 去上传

按照文档上,这里抠一点七牛的文档来举例子说明

const observable = qiniu.upload(file, key, token, putExtra, config)
const subscription = observable.subscribe(observer) // 上传开始
// or
const subscription = observable.subscribe(next, error, complete) // 这样传参形式也可以
subscription.unsubscribe() // 上传取消

可见这里是没有使用到 ak 和 sk 这样敏感的信息的,这两个信息无论如何都是不应该出现在前端服务中的(包括客户端)。

剩下的就是处理业务逻辑

在上传完了之后,会有一个 key 之类的返回,这是在 callback_url 接口中预先定义的。一般来说,你也只需要 key 就可以了,因为这个在你的后端服务中已经有了一条 Video 的记录。

总结

在群里中我发现有几个主要的观点:

  1. 直传的话相关的密码什么的信息都会存在前端,禁止一切前端直传,为了安全,这点(后端)开销是可以被允许的。
  1. 前端直传确实有风险。另外如果oss和后端信息不同步,可能会存在脏文件。如果要解决脏文件,就增加了oss治理的成本。
  1. 正常流程确实是这样,但是用户量多,一定会有黑天鹅事件发生:上传完了,但是用户断网断电了,或者用户关浏览器了,数据库没收到。这就导致了数据不同步。更大风险主要是,token泄露,用户恶意上传多次大文件,但是你不知道,导致你oss费用损失。

在这里评论一下:

  1. 在上述的例子中,不存在 ak,sk 在前端中泄漏的问题;这里上传也不占用服务器的资源,通过上传服务器,再由服务器上传 OSS 是多此一举;如果你说无法保证 token 安全的问题,同样你也无法保证你的服务器端鉴权策略一定是可靠的。
  2. 通过回调的方式,服务器端会有一条记录,记录着你这一条信息,因为 OSS 会通知你 8 次,你完全没有必要担心数据丢失的问题,如果 8 次你都没存下来,那应该反思一下你自己的代码是否有问题。你自己的服务器有了这条 key 的记录,oss 治理成本无疑是显著的降低的。
  3. 这一点来说,是完全忽略了 OSS 的最佳实践,而是用自己摸索出的一条能用,但是不好的实践。这一点和上面的解释是一样的。