这是我参与更文挑战的第2天,活动详情查看: 更文挑战
本文实现的项目地址。
将用户的一些静态资源文件、日志文件放到七牛上,应该是很多公司的通用做法。七牛也提供了图形化工具 Kodo Browser,方便用户浏览存储在七牛服务器中的文件。
当有定制化需求的时候,也可以通过 kodo-browser 的 GitHub 源码 进行定制。我们为了可以方便的查看加密后的日志文件,就需要对工具进行定制(可以查看美团 Logan 的 Swift / Object-C 解密程序)。但是由于本人没有 Notes 等相关的知识,所以只能另辟蹊径,使用 Swift + SwiftUI 来实现类似于 Mac 上 Finder 的功能。
想要获取七牛上的文件,首先就要搞明白的是七牛的网络协议是怎样的。好消息是七牛提供了 API 文档,坏消息是我看了半天结果什么也看不懂。所以我只能看 Java SDK 来分析网络协议。
七牛提供了很多种语言的 SDK,直接使用 API 的场景很少,所以感觉七牛在 API 文档的维护方面做得并不好,当然也不排除出本人能力有限。
下面是 Swift 实现的三个网络协议:
- 获取 Bucket 列表
- 获取文件列表
- 获取文件信息
import Foundation
import Moya
let RsQboxBaseURL = URL(string: "https://rs.qbox.me")!
let RsQiniuBaseURL = URL(string: "https://rs.qiniu.com")!
let RsfQiniuBaseURL = URL(string: "https://rsf.qbox.me")!
public enum QiniuRequest {
case buckets
case statInfo(bucket: String, fileKey: String?)
case list(bucket: String, prefix: String)
}
extension QiniuRequest: TargetType {
public var baseURL: URL {
switch self {
case .buckets:
return RsQiniuBaseURL
case .statInfo:
return RsQboxBaseURL
case .list:
return RsfQiniuBaseURL
}
}
public var path: String {
switch self {
case .list:
return "/list"
case .buckets:
return "/buckets"
case let .statInfo(bucket, fileKey):
let key = UrlSafeBase64.encodedEntry(bucket: bucket, fileKey: fileKey) ?? ""
return "/stat/\(key)"
}
}
public var task: Task {
switch self {
case .buckets:
return .requestPlain
case .statInfo:
return .requestPlain
case .list:
return .requestParameters(parameters: getPram() ?? [String : String](), encoding: URLEncoding.default)
}
}
func getPram() -> [String : String]? {
switch self {
case let .list(bucket, prefix):
var pram = [String: String]()
pram["bucket"] = bucket
pram["delimiter"] = "/"
if prefix.count > 0 {
pram["prefix"] = prefix
}
return pram
default:
return nil
}
}
public var headers: [String : String]? {
var header = [String : String]()
var absoluteString = self.absoluteURL.absoluteString
if let parameters = getPram(), parameters.count > 0 {
absoluteString += "?"
var components: [(String, String)] = []
for key in parameters.keys.sorted(by: <) {
let value = parameters[key]!
components += queryComponents(fromKey: key, value: value)
}
absoluteString += components.map { "\($0)=\($1)" }.joined(separator: "&")
}
let url = URL(string: absoluteString) ?? absoluteURL
debugPrint("url:\(url)")
header["Authorization"] = QiniuTool.getToken(url: url)
return header
}
public var method: Moya.Method {
switch self {
case .statInfo,
.list,
.buckets:
return .get
}
}
}
extension QiniuRequest {
/// Creates a percent-escaped, URL encoded query string components from the given key-value pair recursively.
///
/// - Parameters:
/// - key: Key of the query component.
/// - value: Value of the query component.
///
/// - Returns: The percent-escaped, URL encoded query string components.
public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] {
var components: [(String, String)] = []
switch value {
case let dictionary as [String: Any]:
for (nestedKey, value) in dictionary {
components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value)
}
default:
components.append((escape(key), escape("\(value)")))
}
return components
}
/// Creates a percent-escaped string following RFC 3986 for a query string key or value.
///
/// - Parameter string: `String` to be percent-escaped.
///
/// - Returns: The percent-escaped `String`.
public func escape(_ string: String) -> String {
string.addingPercentEncoding(withAllowedCharacters: .afURLQueryAllowed) ?? string
}
}
可以看出这三个网络请求,居然用了三个不同的 host,应该是为了缓解服务器的压力。
七牛的网络协议
七牛的网络协议是在 header 中加入 Authorization 的鉴权信息。Authorization 的生成过程为:
- 获取完整的请求 url 路径;
- 将所有的请求参数进行排序,拼接在 url 路径后面,再拼接一个换行符 "\n",然后进行加密;
- 最后拼接成 "QBox QiniuAccessKey:加密结果" 格式的字符串。
下面是对字符串进行加密过程:
class QiniuTool {
static func getToken(url: URL) -> String? {
var text = ""
text += url.path
if let query = url.query {
text += "?\(query)"
}
text += "\n"
var digest = text.hmac(algorithm: .SHA1, key: QiniuConfig.QiniuSecretKey)
digest = digest.replacingOccurrences(of: "/", with: "_")
digest = digest.replacingOccurrences(of: "+", with: "-")
return "QBox \(QiniuConfig.QiniuAccessKey):\(digest)"
}
}
七牛的临时访问链接
为了方便下载文件,可以生成一条临时访问链接。
生成七牛的临时访问链接并不需要请求服务,而是在访问链接后面加上加密后的 token,当访问服务器的时候,服务器再对 token 进行验证。
class QiniuTool {
static func getPublicUrl(bucket: String = QiniuConfig.BucketDomain, key: String) -> String? {
let e = Int(Date().timeIntervalSince1970 + 3600)
let url = "https://\(bucket)/\(key)?e=\(e)"
var r = url.hmac(algorithm: .SHA1, key: QiniuConfig.QiniuSecretKey)
r = r.replacingOccurrences(of: "/", with: "_")
r = r.replacingOccurrences(of: "+", with: "-")
return "\(url)&token=\(QiniuConfig.QiniuAccessKey):\(r)"
}
}
以前我以为这种链接需要在服务器生成才安全,其实完成可以在客户端生成。
再次提醒
可以查看本文完整项目地址。
当有定制化需求的时候,可以通过 kodo-browser 的 GitHub 源码 进行定制,或者进行使用七牛提供好的 SDK 会省去很多麻烦事。