使用云存储服务搭建图片上传站点的实现方法

笔者在写文章时,通常会将图片上传到各种云存储服务(OSS)上,并通过自己搭建的简单站点https://imgbed.sugarat.top/来管理图片的访问。为了保证图片的访问速度和质量,笔者会使用CDN来访问这些图片。

笔者本着尽可能简单,减少对后端依赖的原则,上传令牌是在本地(Node.js)生成并设置一个过期时间,在浏览器中直接粘贴,存放在 LocalStorage 中,过期就在本地重新生成一次就行。

然而,生成上传令牌的过程中还存在两个麻烦点:① 依赖 Node.js 环境 ② 关键的秘钥存储在本地文件中。

本次迭代的目标是解决这两个麻烦点。

生成原理

又拍云

参考文档:

token 认证生成

简化成 JS 代码如下

// 基本配置
const operator = '账号'
const password = '密码'
const method = 'PUT' // 上传时用到的请求方法
const urlPrefix = 'bucketName/sourcePrefix' // 资源在OSS上的桶名称和公共路径前缀
const expire = Math.floor(Date.now() / 1000) + 3600 // 过期时间 1小时后过期

// 计算token
const token = base64(hmacSha1(MD5(password), `${method}&${urlPrefix}&${expire}`))

// 最终上传用到的请求头
const Authorization = `UPYUN ${operator}:${token}`

依赖的算法

  • base64:将数据转换为 ASCII 字符串的编码
  • HMAC_SHA-1:基于 SHA-1 哈希算法的消息认证码,用于验证消息的完整性和真实性
  • MD5:哈希函数,用于生成数据的数字指纹

七牛云

参考文档:

上传凭证,URL安全的Base64编码

简化成 JS 代码如下

// 基本配置
const accessKey = 'ACCESS_KEY'
const secretKey = 'SECRET_KEY'
const bucket = 'BUCKET_NAME' // OSS 桶名称
const expires = Math.floor(Date.now() / 1000) + 3600 // 过期时间 1小时后过期

const encodedFlags = base64ToUrlSafe(base64(JSON.stringify({
  scope: bucket,
  deadline: expires,
})))
const encodedSign = base64ToUrlSafe(base64(hmacSha1(secretKey, encodedFlags)))

// 最终上传用到的令牌
const uploadToken = `${accessKey}:${encodedSign}:${encodedFlags}`

其中 base64ToUrlSafe 是 “URL安全的Base64编码” 相关的方法

URL安全的Base64编码适用于以URL方式传递Base64编码结果的场景。该编码方式的基本过程是先将内容以Base64格式编码为字符串,然后检查该结果字符串,将字符串中的加号 + 换成中划线 - ,并且将斜杠 / 换成下划线 _

// 实现如下
function base64ToUrlSafe(v: string) {
  return v.replace(/\//g, '_').replace(/\+/g, '-')
}

其它依赖算法和又拍云基本一致 hmacSha1 base64

加密方法的实现

这里分别介绍浏览器和 Node.js 环境下的简单实现。

前端浏览器侧实现

base64 和 HMAC_SHA-1 算法都有现成的实现,分别可以使用浏览器提供的 btoa Crypto API。

function base64(value: string) {
  return btoa(value)
}

HMAC_SHA-1

在阅读 MDN: Crypto API 文档时可以看到 Crypto.subtle 的描述。

从字面意思不难看出就是我们需要的API。

HMAC 的例子中,就可以找到我们需要的关键信息:

关键代码如下

const encoder = new TextEncoder()
const encoded = encoder.encode(value)
const signature = await window.crypto.subtle.sign('HMAC', key, encoded)

其中 key 是我们需要的密钥,可以用 SubtleCrypto.importKey() 导入生成。

const encoder = new TextEncoder()
const key = await window.crypto.subtle.importKey(
  'raw',
  encoder.encode(password), // password 是我们的密钥
  { name: 'HMAC', hash: { name: 'SHA-1' } },
  false,
  ['sign'],
)

最终我们的方法实现如下。

async function hmacSha1(key: string, value: string) {
  const encoder = new TextEncoder()

  const cryptoKey = await window.crypto.subtle.importKey(
    'raw',
    encoder.encode(key),
    { name: 'HMAC', hash: { name: 'SHA-1' } },
    false,
    ['sign'],
  )

  const data = encoder.encode(value)
  const hashBuffer = await window.crypto.subtle.sign('HMAC', cryptoKey, data)

  return arrayBufferToBase64(hashBuffer) // 返回 base64 格式的结果
}

function arrayBufferToBase64(buffer: ArrayBuffer) {
  const uint8Array = new Uint8Array(buffer)
  const base64String = String.fromCharCode(...uint8Array)
  return btoa(base64String)
}

MD5

MD5 可以使用开源库 spark-md5

import SparkMD5 from 'spark-md5'

export function MD5(str: string): string {
  return SparkMD5.hash(str)
}

Node.js 实现

Node.js 环境下,可以直接使用内置 node:crypto 模块提供的各种加密算法,十分方便。

HMAC_SHA-1

import crypto from 'crypto'

function hmacSha1(key: string, value: string) {
  const hmac = crypto.createHmac('sha1', key)
  hmac.update(value) // 设置用于计算校验值的字符串
  return hmac.digest('base64') // 计算校验值,并按照 base64 返回
}

MD5

import crypto from 'crypto'

function MD5(value: string) {
  const md5 = crypto.createHash('md5')
  md5.update(value) // 设置用于计算 MD5 值的字符串
  return md5.digest('hex') // 计算 MD5 值,并直接以十六进制字符串返回

安全问题

针对存储账号和密码等敏感信息,可以使用浏览器提供的账号密码管理能力存储。例如 Chrome 中提供的 PasswordCredential 相关API。

调用后就可以唤起存储的弹窗。

最后

总结一下:浏览器中也可以使用 window.crypto 提供的API,完成常用的加密算法调用,同时也可以在 Web Worker 中使用,可以有效提升性能。当前这一版图床,应该也还不是最终版,后续计划将部分管理功能以某种可能得形式完成纯静态的支持。

欢迎评论区交流想法和意见

标签:游戏攻略