1
1
mirror of https://github.com/ZeroCatDev/ClassworksKV.git synced 2025-10-22 02:03:11 +00:00
ClassworksKV/docs/middleware.md
2025-10-02 12:07:50 +08:00

11 KiB
Raw Blame History

中间件系统文档

概述

本项目使用中间件系统来处理设备信息获取、权限验证和Token认证。所有与UUID相关的操作都通过统一的中间件处理。

中间件架构

1. 设备信息中间件 (deviceMiddleware)

文件位置: middleware/device.js

功能: 统一处理设备UUID自动获取或创建设备

使用场景:

  • 所有需要设备信息的接口
  • 不需要密码验证的读操作
  • 需要在后续中间件中访问设备信息的场景

工作流程:

  1. req.params.deviceUuidreq.params.namespacereq.body.deviceUuid 获取UUID
  2. 在数据库中查找设备
  3. 如果设备不存在,自动创建新设备
  4. 将设备信息存储到 res.locals.device

代码示例:

import { deviceMiddleware } from './middleware/device.js';

// 基本用法
router.get('/device/:deviceUuid/info', deviceMiddleware, (req, res) => {
  // 设备信息可从 res.locals.device 访问
  res.json(res.locals.device);
});

// 从body获取UUID
router.post('/device/create', deviceMiddleware, (req, res) => {
  // req.body.deviceUuid 会被自动处理
  res.json({ message: '设备已创建', device: res.locals.device });
});

数据访问:

const device = res.locals.device;
// device: {
//   id: 1,
//   uuid: 'device-uuid-123',
//   name: 'My Device',
//   password: 'hashed-password',
//   passwordHint: '提示信息',
//   accountId: null,
//   createdAt: Date,
//   updatedAt: Date
// }

2. 写权限验证中间件 (requireWriteAuth)

文件位置: middleware/tokenAuth.js

功能: 验证设备密码,控制写权限

依赖: 必须在 deviceMiddleware 之后使用

使用场景:

  • 所有需要修改数据的操作POST、PUT、DELETE
  • 需要验证设备密码的操作

工作流程:

  1. res.locals.device 获取设备信息
  2. 如果设备没有设置密码,直接允许操作
  3. 如果设备设置了密码:
    • req.body.passwordreq.query.password 获取密码
    • 验证密码是否正确
    • 密码正确:继续执行
    • 密码错误或未提供:返回 401 错误

代码示例:

import { deviceMiddleware } from './middleware/device.js';
import { requireWriteAuth } from './middleware/tokenAuth.js';

// 写操作需要密码验证
router.post('/device/:deviceUuid/data',
  deviceMiddleware,        // 第一步:获取设备信息
  requireWriteAuth,        // 第二步:验证写权限
  (req, res) => {
    // 验证通过,执行写操作
    res.json({ message: '数据已更新' });
  }
);

// 读操作不需要密码
router.get('/device/:deviceUuid/data',
  deviceMiddleware,        // 只需要设备信息
  (req, res) => {
    res.json({ data: 'some data' });
  }
);

密码提供方式:

// 方式1: 通过请求体
fetch('/device/uuid-123/data', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    password: 'device-password',
    data: 'new value'
  })
});

// 方式2: 通过查询参数
fetch('/device/uuid-123/data?password=device-password', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ data: 'new value' })
});

错误响应:

// 需要密码但未提供
{
  "statusCode": 401,
  "message": "此操作需要密码",
  "passwordHint": "提示信息"
}

// 密码错误
{
  "statusCode": 401,
  "message": "密码错误"
}

3. Token认证中间件 (tokenAuth)

文件位置: middleware/tokenAuth.js

功能: 基于应用安装Token进行认证

使用场景:

  • 应用访问KV数据
  • 需要应用级别认证的接口
  • 不依赖设备UUID的操作

工作流程:

  1. 从 Header、Query 或 Body 中获取 token
  2. 在数据库中查找对应的应用安装记录
  3. 验证 token 是否有效
  4. 将应用、设备信息存储到 res.locals

Token提供方式:

  1. Authorization Header (推荐):

    headers: {
      'Authorization': 'Bearer <token>'
    }
    
  2. Query参数:

    ?token=<token>
    
  3. Request Body:

    {
      "token": "<token>",
      "data": "..."
    }
    

代码示例:

import { tokenAuth } from './middleware/tokenAuth.js';

// Token认证的接口
router.get('/kv/:key', tokenAuth, (req, res) => {
  // 可访问:
  // - res.locals.appInstall (应用安装记录)
  // - res.locals.app (应用信息)
  // - res.locals.device (设备信息)
  // - res.locals.deviceId (设备ID)

  res.json({
    key: req.params.key,
    device: res.locals.device.uuid,
    app: res.locals.app.name
  });
});

数据访问:

const appInstall = res.locals.appInstall;
// appInstall: {
//   id: 'cuid',
//   deviceId: 1,
//   appId: 1,
//   token: 'unique-token',
//   note: '备注',
//   installedAt: Date,
//   updatedAt: Date,
//   app: { ... },
//   device: { ... }
// }

const app = res.locals.app;
// app: { id, name, description, developerName, ... }

const device = res.locals.device;
// device: { id, uuid, name, password, ... }

中间件组合使用

场景1: 基于UUID的读操作无需密码

router.get('/device/:deviceUuid/data',
  deviceMiddleware,
  (req, res) => {
    const device = res.locals.device;
    res.json({ device, data: '...' });
  }
);

场景2: 基于UUID的写操作需要密码

router.post('/device/:deviceUuid/data',
  deviceMiddleware,      // 获取设备信息
  requireWriteAuth,      // 验证密码
  (req, res) => {
    // 执行写操作
    res.json({ message: '成功' });
  }
);

场景3: 基于Token的操作

router.get('/kv/:key',
  tokenAuth,             // Token认证自动获取设备信息
  (req, res) => {
    const device = res.locals.device;
    const app = res.locals.app;
    res.json({ device, app, data: '...' });
  }
);

场景4: 批量路由保护

const router = express.Router();

// 所有该路由下的接口都需要设备信息
router.use(deviceMiddleware);

// 具体接口
router.get('/info', (req, res) => {
  res.json(res.locals.device);
});

router.post('/update', requireWriteAuth, (req, res) => {
  res.json({ message: '更新成功' });
});

最佳实践

1. 中间件顺序很重要

// ✅ 正确:先获取设备信息,再验证权限
router.post('/data', deviceMiddleware, requireWriteAuth, handler);

// ❌ 错误requireWriteAuth 依赖 deviceMiddleware
router.post('/data', requireWriteAuth, deviceMiddleware, handler);

2. 选择合适的认证方式

// 用户直接操作设备 → 使用 deviceMiddleware + requireWriteAuth
router.post('/device/:deviceUuid/config', deviceMiddleware, requireWriteAuth, handler);

// 应用代表用户操作 → 使用 tokenAuth
router.post('/kv/:key', tokenAuth, handler);

3. 读操作不需要密码

// ✅ 读操作只需要设备信息
router.get('/device/:deviceUuid/data', deviceMiddleware, handler);

// ❌ 读操作不需要密码验证
router.get('/device/:deviceUuid/data', deviceMiddleware, requireWriteAuth, handler);

4. 错误处理

router.post('/data', deviceMiddleware, requireWriteAuth,
  async (req, res, next) => {
    try {
      // 业务逻辑
      const device = res.locals.device;
      // ...
      res.json({ success: true });
    } catch (error) {
      next(error); // 传递给全局错误处理器
    }
  }
);

5. 密码提示信息

// 设置设备时提供密码提示
await prisma.device.update({
  where: { uuid: deviceUuid },
  data: {
    password: hashedPassword,
    passwordHint: '您的生日8位数字' // 提供友好的提示
  }
});

常见问题

Q1: 为什么设备不存在时会自动创建?

A: 这是为了简化客户端逻辑。客户端只需要生成UUID并使用无需先调用创建接口。首次访问时会自动创建设备记录。

Q2: 读操作为什么不需要密码?

A: 根据项目需求只有写操作需要密码保护。读操作允许任何知道UUID的人访问。如果需要保护读操作可以在路由中添加 requireWriteAuth 中间件。

Q3: deviceMiddleware 和 tokenAuth 有什么区别?

A:

  • deviceMiddleware: 基于UUID获取设备信息适合用户直接操作
  • tokenAuth: 基于应用Token认证适合应用代表用户操作包含应用级别的权限控制

Q4: 如何撤销某个设备的访问权限?

A:

  1. 基于UUID的访问修改设备密码
  2. 基于Token的访问删除对应的 AppInstall 记录

Q5: 密码错误但操作不需要密码是否可以继续?

A: 不可以。requireWriteAuth 中间件会检查:

  • 如果设备没有密码 → 直接通过
  • 如果设备有密码但未提供 → 拒绝
  • 如果设备有密码但错误 → 拒绝

如果操作不需要密码,不要使用 requireWriteAuth 中间件。


迁移指南

从旧的认证系统迁移

旧代码:

router.post('/kv/:namespace/:key', authMiddleware, handler);

新代码:

// 选项1: 使用 deviceMiddleware (如果通过URL传递UUID)
router.post('/device/:deviceUuid/kv/:key',
  deviceMiddleware,
  requireWriteAuth,
  handler
);

// 选项2: 使用 tokenAuth (推荐,更安全)
router.post('/kv/:key', tokenAuth, handler);

客户端更新

旧方式:

// UUID + 密码
fetch('/kv/device-uuid/mykey', {
  method: 'POST',
  headers: {
    'x-namespace-password': 'password'
  },
  body: JSON.stringify({ data: 'value' })
});

新方式选项1 - UUID:

fetch('/device/device-uuid/kv/mykey', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    password: 'password',
    data: 'value'
  })
});

新方式选项2 - Token推荐:

// 先获取token
const authResponse = await fetch('/apps/1/authorize', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    deviceUuid: 'device-uuid',
    password: 'password'
  })
});
const { token } = await authResponse.json();

// 使用token操作
fetch('/kv/mykey', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ data: 'value' })
});

技术细节

密码存储

密码使用 bcrypt 进行哈希处理,存储在 Device.password 字段。

加密函数 (utils/crypto.js):

import bcrypt from 'bcryptjs';

export async function hashDevicePassword(password) {
  return await bcrypt.hash(password, 10);
}

export async function verifyDevicePassword(password, hash) {
  return await bcrypt.compare(password, hash);
}

性能优化

  • 使用整数ID (deviceId) 作为外键查询效率高于字符串UUID
  • 设备信息查询结果缓存在 res.locals,避免重复查询
  • 密码验证使用 bcrypt 的异步方法,不阻塞事件循环

安全考虑

  1. 密码使用 bcrypt 加密存储
  2. Token 使用 cuid 生成,具有高随机性
  3. 支持密码提示功能,不暴露实际密码
  4. 写操作强制密码验证(如果设置了密码)
  5. 所有中间件使用 errors.catchAsync 包装,统一错误处理

参考