搞事情之使用七牛云的注意事项
0

原文地址:PJ 的 iOS 开发日常

前言

本博客最初所采用的图床就是七牛,当时因为第一次使用图床之类的服务,没有进行一个比较好的筛选,并且没有考虑过多的细节,所以直接采用了七牛。经过一段时间后,因为博客访问量上去了,超出七牛每月的免费流量额度,平均每个月花费在 10 元左右。

因为写博客是一件非常费脑费精力的事情,再纠结钱就十分的没意思了,遂开始了寻找一个免费既好用的图床。一番搜寻后,发现了这个图床,用了快一年。在这一年的时间中从未出现过任何的异常情况,而且也提供 http 服务,原本打算把毕设相关的图片资源也使用这个图床,但考虑到该图床所提供的 http 服务能了较为有限,遂放弃。

最后继续调研,看了又拍云、七牛云、阿里云 OSS 、腾讯云 OSS,最后对比完还是选择了七牛云。这里要强烈向大家图床阿里云 OSS 服务,调用方式及其繁琐!仅仅至少上传一个图片而已,所写代码之啰嗦,实在难以让我提起兴趣。更遗憾的是,所有的 SDK 均未提供 Swift 版本,这都 9102 年了!!!

准备工作

七牛云的图床免费流量额度每个月只有 10 GB,超出这个范围后就会进行计费,团队初步估计 pv/uv 再怎么次,每天至少都会稳定在百人左右,所以要做一定的防刷手段。

存储区域

这部分的内容主要针对进行上传资源时提升服务的可靠性,如果产品的上传策略是图片先过一遍自己的服务器再上传至七牛,需要根据服务器架设地点进行选择;如果产品策略是客户端上传,则考虑目标群体大部分所在区域。不过就目前情况来看,PIGPEN 因为只涉及到图片资源的上传和下载,且大部分用户在华北地区,故选择了华北地区。当然,最后的收费也会提升一些。

空间选择

七牛云提供了公开空间私有空间

  • 公开空间:可通过文件对象的 URL 直接访问;
  • 私有空间:文件对象的访问则必须获得拥有者的授权才能访问。

公开空间,符合博客、社区、论坛等属性产品的定位,但不适合做内容闭环的产品,比如微信、QQ 以及 PIGPEN 等,并不希望产品中所产生的内容被产品外的用户所知,反而私有空间是正确选择,可对需要访问的资源设置 IP 访问次数和黑白名单限制、超时限制、浏览器是否能直接访问等等防刷手段。PIGPNE 选择了私有空间。

域名管理

加速域名

七牛云的测试域名 30 天后会进行自动回收,所以各位同学确定一定要使用七牛云作为图床后,请尽快配对加速域名。该加速域名要求必须通过中国大陆 ICP 备案。推荐重新配置一个新的二级域名作为七牛云的加速域名。

需要注意的是,配置完加速域名后,七牛会让我们增加一条 CNAME 解析,当访问这个域名时,实际上转发给记录值中所填写的真实域名。配置完毕后,如下图所示:

在腾讯云中配置的七牛云加速域名.png

假设我们在七牛上配置完毕加速域名,生成的加速域名为 aha.pjhubs.com.qiniudns.com,在“主机记录”部分,需要填入的应该是我们所新增的二级域名 aha。配置妥当后,一般等待十分钟左右再去七牛控制台中查看相关信息即可。

访问控制

对于资源计费的图床,最担心的就是资源被刷了,好在七牛在四种访问控制的方法:Referer 防盗链时间戳防盗链回源鉴权IP 黑白名单

Referer 防盗链

Referer 防盗链:这里的 Referer 指的是 HTTP 头部的一个字段,也称为 HTTP 来源地址(HTTP Referer),用来表示从哪儿链接到目前的网页,采用的格式是 URL。换句话说,借着 HTTP Referer 头部网页可以检查访客从哪里而来,这也常被用来对付伪造的跨网站请求。(来源七牛官方文档)

如果我们直接通过浏览器打开一个链接,此时该请求的 Referer 字段为空,这是一个“凭空产生”的 HTTP 请求,并不是从一个地方链接过去的。

经过一番的思考,设置白名单且允许空 Referer 进行访问。原意是想着不允许空 Referer 访问的,但是在客户端上对图片资源发起的请求就是空 Referer 啊~遂放弃。

IP 黑白名单

这是解决 IP 盗刷的最终解决手段,但目前还未遇到真实盗刷场景,后续再补~

图片处理

七牛居然非常贴心的提供了一定的图片处理功能,对于一个内容产出方,最需要的就是图片资源的各种缩略图、统一水印等等功能,而且全部都是免费功能!!!

七牛的图片处理.png

实际操作

整套七牛的图片上传流程可用下图进行概括:
七牛云私有空间图片访问全流程.png

后端

PIGPEN 整体后端都是基于 python 技术栈,所以选择了七牛云的 python SDK。通过 pip3 install qiniu 后,根据已有业务编写了以下两个方法:

from qiniu import Auth

def create_upload_image_token(count, key):
    """
    七牛不支持多图上传,根据官方文档描述,只能在业务层循环针对每个图生成对应 token
    :param count: 需要生成的 token 个数
    :param key: 文件名前缀
    :return 生成的 token 列表
    """

    jsons = []
    while count > 0:
        # 构建鉴权对象
        q = Auth(settings.QINIU_ACCESS_KEY, settings.QINIU_SECRET_KEY)
        # 要上传的空间
        bucket_name = '你的私有空间名'
        # 文件名
        k = bucket_name + key + str(int(time.time())) + str(count) + '.jpeg'
        token = q.upload_token(bucket_name, k)

        json = {
            'img_token': token,
            'img_key': k
        }

        jsons.append(json)
        count -= 1

    return jsons

def create_full_image_url(keys):
    """
    拼接获取完成后的图片 url
    :param keys: 从客户端发送来的 keys,遍历出的每一个 key 代表一个文件名
    :return image_urls: 返回拼接完成后的图片 url 数组
    """
    q = Auth(settings.QINIU_ACCESS_KEY, settings.QINIU_SECRET_KEY)
    bucket_name = 'pigpenimg.pjhubs.com'

    image_urls = []
    for index, key in enumerate(keys):
        base_url = 'http://%s/%s' % (bucket_name, key)
        private_url = q.private_download_url(base_url, expires=3600)

        image_urls.append(private_url)

    return image_urls

在构造 token 的方法 create_upload_image_token 中,一开始我并没有做文件名的指定,导致最终生成的图片资源链接并没有后缀名,这就导致了在客户端进行资源请求时,加载失败,但通过浏览器进行访问却是正常的。出现这种问题你可以选择跟我一样的方法:在该方法中指定文件名,并加上资源类型后缀

在业务层中,在接受客户端上传的 keys API 时,考虑到了多图上传的情况,在针对多 keys 情况考虑了到底是直接转为 json 请求体直接上传,还是把多个 keys 进行拼接的问题,按道理,后端接口部分应该直接解析客户端发送的 json 请求体,直接从中解析出所需要的数据,但不知道 python 具体应该怎么操作,这部分内容拖得也比较久了,就先不优化了,所以最终采用了第二种方案。

客户端

正如前文所说,原意是打算找到一家提供 Swift 版本 SDK 的服务商就直接用了,但是经过一番的调研后,发现根本没有,不管大小公司都没有提供 Swift 版本!!!真是太气人了!

最后没办法,只能选择业内老油条七牛,好在七牛提供 Objective-C 版本 SDK 调用方式足够简洁。PIGPEN 客户端整体基于 CocoaPods 进行依赖管理,只需要在依赖配置文件 podfile 中增加 use_frameworks! 即可把七牛 SDK 通过动态链接库的方式直接引入,而不用进行桥接:

platform :ios, '10.0'
target 'PIGPEN' do
    use_frameworks!

    pod 'Qiniu'
    ...
end

为了方便使用七牛的上传服务,抽离出了获取生成的 token 和通过 key 换取授权图片 url 的两个方法,需要注意的是七牛的 iOS SDK 并未直接提供 UIImage 的上传方法,而是通过 Data 或者 PHAsset 类型参数进行上传。

原本是想着直接把 UIImage 转为 Data 后进行上传,但不知是因为七牛 Objective-C SDK 转换成的 Swift 方法有问题还是其它的一些原因,部分参数一直未识别,最后只能更换为了使用 PHAsset 资源进行上传。调通后发现,在 PIGPEN 中实际上就是通过读取相册中的部分照片进行上传,而从相册中获取照片全都是 PHAsset 类型的数据,如果要转为 UIImage 中间还会经过一个异步操作,确实是多了一些不必要的操作。关于 iOS 中的自定义相册,如果你感兴趣可以参考这篇文章

//
//  PJImageUpload.swift
//  PIGPEN
//
//  Created by PJHubs on 2019/1/2.
//  Copyright © 2019 PJHubs. All rights reserved.
//

import Foundation
import Photos
import Qiniu

class PJImageUploader {
    /// 上传图片
    class func upload(assets: [PHAsset],
                      complateHandler: @escaping (([String], [String]) -> Void),
                      failuredHandler: @escaping ((PJNetwork.Error) -> Void)) {
        PJNetwork.shared.requstWithGet(path: URL.upload.rawValue,
                                       parameters: ["imageCount": String(assets.count)],
                                       complement: { (dataDict) in
                                        if dataDict["msgCode"]?.intValue == 0 {
                                            var dataDict = dataDict["msg"]!
                                            let tokens = dataDict["upload_tokens"].arrayValue
                                            // `setKey` 方法参数
                                            var keys = ""
                                            // complateHandler 闭包回调参数
                                            var complateKeys = [String]()
                                            for c_i in 0..<assets.count {
                                                let token = tokens[c_i]["img_token"].string
                                                let key = tokens[c_i]["img_key"].string
                                                complateKeys.append(key!)
                                                // 七牛上传
                                                QNUploadManager()?.put(assets[c_i],
                                                                       key: key,
                                                                       token: token,
                                                                       complete: { (info, key, respDict) in
                                                                        guard let respDict = respDict else { return }
                                                                        // key 即为文件名。拼接完成后一次性丢给 API
                                                                        let key = respDict["key"]
                                                                        keys += "," + String(key as! String)

                                                                        if c_i == assets.count - 1 {
                                                                            keys.removeFirst()
                                                                            setKey(key: keys, complateHandler: { (imgUrls) in
                                                                                complateHandler(imgUrls, complateKeys)
                                                                            }, failuredHandler: { (error) in
                                                                                falierHandler(error)
                                                                            })
                                                                        }
                                                }, option: nil)
                                            }
                                        } else {
                                            let error = PJNetwork.Error(errorCode: dataDict["msgCode"]?.intValue,
                                                                        errorMsg: dataDict["msg"]?.string)
                                            failuredHandler(error)
                                        }
        }) { (errorString) in
            falierHandler(PJNetwork.Error(errorCode: nil, errorMsg: errorString))
        }
    }

    /// 上传 keys 且换回图片完整 url
    class func setKey(key: String,
                complateHandler: @escaping ((([String]) -> Void)),
                failuredHandler: @escaping ((PJNetwork.Error) -> Void)) {
        PJNetwork.shared.requstWithPost(path: URL.setKey.rawValue,
                                        parameters: ["keys": key],
                                        complement: { (dataDict) in
                                            if dataDict["msgCode"]?.intValue == 0 {
                                                var dataDict = dataDict["msg"]!
                                                let keys = dataDict["image_urls"].array
                                                if keys != nil {
                                                    var k = [String]()
                                                    for key in keys! {
                                                        k.append(key.string!)
                                                    }
                                                    complateHandler(k)
                                                }
                                            }
        }) { (errorString) in
            let error = PJNetwork.Error(errorCode: nil, errorMsg: errorString)
            failuredHandler(error)
        }
    }
}

// MARK: - URL
extension PJImageUploader {
    enum URL: String {
        case upload = "realPet/uploadToken"
        case setKey = "realPet/setKeys"
    }
}

总结

以上就是使用七牛云作为 PIGPEN 图床的对接历程,出现的问题主要在加速域名的配置和私有空间图片的访问上,中间也发了几次工单和七牛云工程师询问了几个问题,整体来看反应速度十分迅速,不管是工作日还是周末,工单回复最长回复耗时不到 10 分钟!

最后再吐槽一句,国内的服务商什么时候才能提供 Swift 版本的 SDK 啊!

优秀的人遵守规则,顶尖的人创造规则

讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!