mirror of
https://github.com/ZeroCatDev/ClassworksKV.git
synced 2025-10-22 02:03:11 +00:00
11 KiB
11 KiB
中间件系统文档
概述
本项目使用中间件系统来处理设备信息获取、权限验证和Token认证。所有与UUID相关的操作都通过统一的中间件处理。
中间件架构
1. 设备信息中间件 (deviceMiddleware
)
文件位置: middleware/device.js
功能: 统一处理设备UUID,自动获取或创建设备
使用场景:
- 所有需要设备信息的接口
- 不需要密码验证的读操作
- 需要在后续中间件中访问设备信息的场景
工作流程:
- 从
req.params.deviceUuid
、req.params.namespace
或req.body.deviceUuid
获取UUID - 在数据库中查找设备
- 如果设备不存在,自动创建新设备
- 将设备信息存储到
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)
- 需要验证设备密码的操作
工作流程:
- 从
res.locals.device
获取设备信息 - 如果设备没有设置密码,直接允许操作
- 如果设备设置了密码:
- 从
req.body.password
或req.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的操作
工作流程:
- 从 Header、Query 或 Body 中获取 token
- 在数据库中查找对应的应用安装记录
- 验证 token 是否有效
- 将应用、设备信息存储到
res.locals
Token提供方式:
-
Authorization Header (推荐):
headers: { 'Authorization': 'Bearer <token>' }
-
Query参数:
?token=<token>
-
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:
- 基于UUID的访问:修改设备密码
- 基于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 的异步方法,不阻塞事件循环
安全考虑
- 密码使用 bcrypt 加密存储
- Token 使用
cuid
生成,具有高随机性 - 支持密码提示功能,不暴露实际密码
- 写操作强制密码验证(如果设置了密码)
- 所有中间件使用
errors.catchAsync
包装,统一错误处理