Skip to content

Instantly share code, notes, and snippets.

@Zayrick
Last active August 7, 2025 10:08
Show Gist options
  • Save Zayrick/545ff76535c2f4f5fb382606b93e07b3 to your computer and use it in GitHub Desktop.
Save Zayrick/545ff76535c2f4f5fb382606b93e07b3 to your computer and use it in GitHub Desktop.
小米IoT平台Cloudflare Worker:一个无需本地部署的智能家居设备控制方案,支持设备属性设置、获取和方法调用,适用于各类小米智能设备的远程操作。

小米 IoT 设备控制 Cloudflare Worker

通过 Cloudflare Workers 转发并签名调用 https://api.io.mi.com ,简化小米 IoT 设备(米家)控制流程。

功能概述

  1. 路径过滤:仅接受以 /app/ 开头的 POST 请求。
  2. 鉴权字段提取:自动从请求体中提取 userIdserviceTokendeviceIdsecurityToken 四个字段,data字段参与签名计算。
  3. HMAC-SHA256 签名:按米家官方协议生成 _noncesignedNoncesignature 三个参数。
  4. 请求转发:将组装好的 application/x-www-form-urlencoded 请求转发至 https://api.io.mi.com,并将原始响应返回给调用方。
  5. CORS 支持:默认向所有响应追加 Access-Control-Allow-* 头,方便在浏览器直接调用。

快速开始

  1. 登录 Cloudflare Dashboard → Workers & Pages → Create application
  2. 选择 创建 Worker(或编辑默认的 Hello World Worker)。
  3. 1.js 中的全部代码复制到在线编辑器,覆盖默认示例代码。
  4. 点击 Save and Deploy 即可。
  5. 部署完成后会生成形如 https://<worker-subdomain>.<your-account>.workers.dev 的访问地址。

请求格式

路径

POST https://<worker-url>/app/<mi-api-path>

示例:/app/miotspec/prop/set

JSON 请求体

{
  "userId": "1234567890",
  "serviceToken": "XXXX",        // 小米账户Cookie 中的 serviceToken
  "deviceId": "XXXX",            // PassportDeviceId
  "securityToken": "XXXX",       // Ssecurity
  "data": {
    "did": "123456789",
    "siid": 2,
    "piid": 1,
    "value": true
  }
}

响应

脚本会保持上游接口的原始返回,只额外附加 CORS 头。

注意事项

  • 安全性
    • 建议仅在测试或低风险环境使用。请妥善保管 serviceToken 等敏感信息。
    • Cloudflare Workers 日志可能会记录请求体,如需避免请自行裁剪。
// 小米IoT平台设备控制 Cloudflare Worker
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
/**
* 处理 HTTP 请求
* 1. 仅接受以 /app/ 开头的 POST 请求
* 2. 解析 body 中的 userId / serviceToken / deviceId / securityToken 以及除去这些字段后的其余内容
* 3. 构造米家接口所需签名并转发到 https://api.io.mi.com 保留全部响应
*/
async function handleRequest(request) {
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
}
// 预检
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders })
}
if (request.method !== 'POST') {
return new Response('仅支持 POST', {
status: 405,
headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=UTF-8' }
})
}
const url = new URL(request.url)
const pathname = url.pathname
if (!pathname.startsWith('/app/')) {
return new Response('路径必须以 /app/ 开头', {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=UTF-8' }
})
}
try {
// 读取并解析请求体
const bodyJson = await request.json()
const {
userId,
serviceToken,
deviceId,
securityToken,
data: dataField
} = bodyJson || {}
if (!userId || !serviceToken || !deviceId || !securityToken) {
return new Response('缺少身份验证信息', {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=UTF-8' }
})
}
if (dataField === undefined) {
return new Response('缺少 data 字段', {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=UTF-8' }
})
}
// 计算签名
const miPath = pathname.replace(/^\/app/, '') // 例如 /miotspec/prop/set
const nonce = generateNonce()
const signedNonce = await generateSignedNonce(securityToken, nonce)
const dataString = typeof dataField === 'string' ? dataField : JSON.stringify(dataField)
const signature = await generateSignature(miPath, signedNonce, nonce, dataString)
// 组装请求参数
const formParams = new URLSearchParams({
_nonce: nonce,
data: dataString,
signature
})
// 构造请求头
const headers = {
'User-Agent': 'APP/com.xiaomi.mihome APPV/6.0.103 iosPassportSDK/3.9.0 iOS/14.4 miHSTS',
'x-xiaomi-protocal-flag-cli': 'PROTOCAL-HTTP2',
'Content-Type': 'application/x-www-form-urlencoded',
'Cookie': `serviceToken=${serviceToken}; userId=${userId}; PassportDeviceId=${deviceId}`
}
// 转发到米家接口
const remoteResponse = await fetch(`https://api.io.mi.com${pathname}`, {
method: 'POST',
headers,
body: formParams
})
// 构造返回,保持原始内容,仅追加 CORS 头
const respHeaders = new Headers(remoteResponse.headers)
Object.entries(corsHeaders).forEach(([k, v]) => respHeaders.set(k, v))
return new Response(remoteResponse.body, {
status: remoteResponse.status,
headers: respHeaders
})
} catch (err) {
return new Response('处理请求时出错: ' + err.message, {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=UTF-8' }
})
}
}
// ================= 工具函数 =================
function generateNonce() {
const randomBytes = new Uint8Array(8)
crypto.getRandomValues(randomBytes)
const timeBytes = new Uint8Array(4)
const time = Math.floor(Date.now() / 1000 / 60)
timeBytes[0] = (time >> 24) & 0xff
timeBytes[1] = (time >> 16) & 0xff
timeBytes[2] = (time >> 8) & 0xff
timeBytes[3] = time & 0xff
const combined = new Uint8Array(12)
combined.set(randomBytes)
combined.set(timeBytes, 8)
return btoa(String.fromCharCode.apply(null, combined))
}
async function generateSignedNonce(secret, nonce) {
const secretBytes = base64ToUint8Array(secret)
const nonceBytes = base64ToUint8Array(nonce)
const combined = new Uint8Array(secretBytes.length + nonceBytes.length)
combined.set(secretBytes)
combined.set(nonceBytes, secretBytes.length)
const hash = await crypto.subtle.digest('SHA-256', combined)
return btoa(String.fromCharCode.apply(null, new Uint8Array(hash)))
}
async function generateSignature(url, signedNonce, nonce, data) {
const signString = `${url}&${signedNonce}&${nonce}&data=${data}`
const keyBytes = base64ToUint8Array(signedNonce)
const key = await crypto.subtle.importKey(
'raw',
keyBytes,
{ name: 'HMAC', hash: { name: 'SHA-256' } },
false,
['sign']
)
const signature = await crypto.subtle.sign(
'HMAC',
key,
new TextEncoder().encode(signString)
)
return btoa(String.fromCharCode.apply(null, new Uint8Array(signature)))
}
function base64ToUint8Array(base64) {
const binary = atob(base64)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i)
}
return bytes
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment