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

479 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 中间件系统文档
## 概述
本项目使用中间件系统来处理设备信息获取、权限验证和Token认证。所有与UUID相关的操作都通过统一的中间件处理。
## 中间件架构
### 1. 设备信息中间件 (`deviceMiddleware`)
**文件位置**: `middleware/device.js`
**功能**: 统一处理设备UUID自动获取或创建设备
**使用场景**:
- 所有需要设备信息的接口
- 不需要密码验证的读操作
- 需要在后续中间件中访问设备信息的场景
**工作流程**:
1.`req.params.deviceUuid``req.params.namespace``req.body.deviceUuid` 获取UUID
2. 在数据库中查找设备
3. 如果设备不存在,自动创建新设备
4. 将设备信息存储到 `res.locals.device`
**代码示例**:
```javascript
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 });
});
```
**数据访问**:
```javascript
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.password``req.query.password` 获取密码
- 验证密码是否正确
- 密码正确:继续执行
- 密码错误或未提供:返回 401 错误
**代码示例**:
```javascript
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' });
}
);
```
**密码提供方式**:
```javascript
// 方式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' })
});
```
**错误响应**:
```json
// 需要密码但未提供
{
"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** (推荐):
```javascript
headers: {
'Authorization': 'Bearer <token>'
}
```
2. **Query参数**:
```javascript
?token=<token>
```
3. **Request Body**:
```javascript
{
"token": "<token>",
"data": "..."
}
```
**代码示例**:
```javascript
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
});
});
```
**数据访问**:
```javascript
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的读操作无需密码
```javascript
router.get('/device/:deviceUuid/data',
deviceMiddleware,
(req, res) => {
const device = res.locals.device;
res.json({ device, data: '...' });
}
);
```
### 场景2: 基于UUID的写操作需要密码
```javascript
router.post('/device/:deviceUuid/data',
deviceMiddleware, // 获取设备信息
requireWriteAuth, // 验证密码
(req, res) => {
// 执行写操作
res.json({ message: '成功' });
}
);
```
### 场景3: 基于Token的操作
```javascript
router.get('/kv/:key',
tokenAuth, // Token认证自动获取设备信息
(req, res) => {
const device = res.locals.device;
const app = res.locals.app;
res.json({ device, app, data: '...' });
}
);
```
### 场景4: 批量路由保护
```javascript
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. 中间件顺序很重要
```javascript
// ✅ 正确:先获取设备信息,再验证权限
router.post('/data', deviceMiddleware, requireWriteAuth, handler);
// ❌ 错误requireWriteAuth 依赖 deviceMiddleware
router.post('/data', requireWriteAuth, deviceMiddleware, handler);
```
### 2. 选择合适的认证方式
```javascript
// 用户直接操作设备 → 使用 deviceMiddleware + requireWriteAuth
router.post('/device/:deviceUuid/config', deviceMiddleware, requireWriteAuth, handler);
// 应用代表用户操作 → 使用 tokenAuth
router.post('/kv/:key', tokenAuth, handler);
```
### 3. 读操作不需要密码
```javascript
// ✅ 读操作只需要设备信息
router.get('/device/:deviceUuid/data', deviceMiddleware, handler);
// ❌ 读操作不需要密码验证
router.get('/device/:deviceUuid/data', deviceMiddleware, requireWriteAuth, handler);
```
### 4. 错误处理
```javascript
router.post('/data', deviceMiddleware, requireWriteAuth,
async (req, res, next) => {
try {
// 业务逻辑
const device = res.locals.device;
// ...
res.json({ success: true });
} catch (error) {
next(error); // 传递给全局错误处理器
}
}
);
```
### 5. 密码提示信息
```javascript
// 设置设备时提供密码提示
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` 中间件。
---
## 迁移指南
### 从旧的认证系统迁移
**旧代码**:
```javascript
router.post('/kv/:namespace/:key', authMiddleware, handler);
```
**新代码**:
```javascript
// 选项1: 使用 deviceMiddleware (如果通过URL传递UUID)
router.post('/device/:deviceUuid/kv/:key',
deviceMiddleware,
requireWriteAuth,
handler
);
// 选项2: 使用 tokenAuth (推荐,更安全)
router.post('/kv/:key', tokenAuth, handler);
```
### 客户端更新
**旧方式**:
```javascript
// UUID + 密码
fetch('/kv/device-uuid/mykey', {
method: 'POST',
headers: {
'x-namespace-password': 'password'
},
body: JSON.stringify({ data: 'value' })
});
```
**新方式选项1 - UUID**:
```javascript
fetch('/device/device-uuid/kv/mykey', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
password: 'password',
data: 'value'
})
});
```
**新方式选项2 - Token推荐**:
```javascript
// 先获取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`):
```javascript
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` 包装,统一错误处理
---
## 参考
- [API重构文档](./API_REFACTOR.md)
- [Token认证示例](./token-auth-examples.md)
- [KV存储文档](./kv.md)
- [应用管理文档](./apps.md)