瀏覽器中生成 OSS 令牌 | Web Crypto API

粥里有勺糖發表於2024-10-06

筆者寫文章的時候,都會把圖片透過自己搭建的一個簡單站點 https://imgbed.sugarat.top/ 把圖片上傳到各種雲的物件儲存服務(OSS)上。

然後透過CDN訪問,保證圖片有可靠的訪問速度和質量。

本著儘可能簡單,減少對後端依賴的原則,上傳令牌是在本地(Node.js)生成並設定一個過期時間,在瀏覽器中直接貼上,存放在 LocalStorage 中,過期就在本地重新生成一次就行。

但現在生成的時候也還有2個麻煩點:① 依賴 Node.js 環境 ② 關鍵的秘鑰儲存在本地檔案中

本次迭代就是把這2個麻煩點解決掉!

生成原理

又拍雲

參考文件: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, '-')
}

其它依賴演算法和又拍雲基本一致 hmacSha1base64

加密方法的實現

這裡分別介紹瀏覽器和 Node.js 環境下的簡單實現。

前端瀏覽器側實現

base64 和 HMAC_SHA-1 演算法都有現成的實現,分別可以使用瀏覽器提供的 btoaCrypto 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 中使用,可以有效提升效能。

當前這一版圖床,應該也還不是最終版,後續計劃將部分管理功能以某種可能得形式完成純靜態的支援。

歡迎評論區交流想法&意見

相關文章