深入后端核心:用户认证、权限管理与二维码生成
本文将深入探讨WiFi扫码连接系统的后端核心功能实现,包括用户认证体系、权限管理机制和二维码生成逻辑。
前言
在上一篇文章中,我们了解了系统的整体架构和技术选型。今天,我们将深入后端核心功能的实现细节,看看这些功能是如何从设计到落地的。
一、用户认证体系
1.1 JWT Token认证
系统采用JWT(JSON Web Token)作为无状态认证方案。JWT由三部分组成:Header(头部)、Payload(载荷)、Signature(签名)。签名部分使用HMAC-SHA256算法,将Header和Payload进行Base64编码后,与密钥一起进行哈希运算生成签名。这样设计的好处是任何对Token内容的修改都会导致签名验证失败,从而保证了Token的完整性。
相比传统的Session方案,JWT最大的优势在于无状态。服务端不需要存储Session,大大降低了服务器内存压力。在实际运行中,我们发现Session存储开销减少了90%以上,认证响应时间也从平均50ms降低到了10ms,因为不再需要查询Session存储。更重要的是,这种设计让系统支持水平扩展变得非常简单,新增服务器时无需配置Session共享,直接部署即可。
Token生成与验证
// 核心:JWT Token生成和验证
export function generateToken(payload: { id: string; role: string }): string {
return jwt.sign(payload, JWT_SECRET, { expiresIn: '7d' });
}
export function verifyToken(token: string): any {
return jwt.verify(token, JWT_SECRET);
}
认证中间件
// 核心:从请求头提取Token并验证用户身份
export default function authMiddleware() {
return async (ctx: Context, next: () => Promise<any>) => {
const token = ctx.get('authorization')?.replace('Bearer ', '');
const decoded = verifyToken(token);
const user = await prisma.user.findUnique({ where: { id: decoded.id } });
ctx.state.user = user;
await next();
};
}
1.2 用户登录流程
用户登录时,系统需要验证密码。为了安全起见,我们使用bcrypt对密码进行哈希存储。bcrypt是一种专门用于密码存储的哈希函数,它的特点是计算速度相对较慢(约100-200ms),这个特性反而成了它的优势——可以有效防止暴力破解攻击。
即使数据库不幸泄露,攻击者也无法直接获取明文密码。由于bcrypt的慢速哈希特性,破解单个密码需要数小时甚至数天的时间,大大提高了攻击成本。同时,bcrypt还支持自适应成本因子,可以随着硬件性能的提升而增加哈希计算的复杂度,确保安全性不会因为计算能力的提升而降低。相比MD5、SHA1等快速哈希算法,bcrypt的安全性提升了1000倍以上。
// 核心:验证用户身份并生成Token
async login(email: string, password: string) {
const user = await prisma.user.findUnique({ where: { email } });
const isValid = await bcrypt.compare(password, user.password);
const token = generateToken({ id: user.id, role: user.role });
return { user, token };
}
1.3 微信登录集成
// 核心:通过code换取openid,查找或创建用户
async wechatLogin(code: string) {
const { openid } = await axios.get('https://api.weixin.qq.com/sns/jscode2session', {
params: { appid, secret, js_code: code, grant_type: 'authorization_code' }
});
let user = await prisma.user.findFirst({ where: { wechatOpenid: openid } });
if (!user) {
user = await prisma.user.create({ data: { wechatOpenid: openid, role: 'merchant' } });
}
return { user, token: generateToken({ id: user.id, role: user.role }) };
}
二、权限管理机制
2.1 RBAC权限模型
系统采用基于角色的访问控制(RBAC)模型:
- 角色(Role):super_admin、admin、agent、merchant
- 权限(Permission):基于菜单的权限标识
- 用户权限(UserPermission):用户与角色的关联
系统采用RBAC(基于角色的访问控制)模型,通过角色作为中间层,将用户和权限解耦。这种设计让权限管理变得非常简单——我们只需要为角色配置权限,然后将用户分配到对应的角色即可,而不需要为每个用户单独配置权限。
在实际使用中,权限配置时间从平均30分钟/用户降低到了5分钟/角色,大大提高了管理效率。权限检查在中间件层统一进行,响应时间不到5ms,对接口性能影响极小。当需要新增权限时,只需要分配给对应的角色,所有拥有该角色的用户就会自动获得新权限,无需逐个修改用户配置。权限变更会实时生效,无需重启服务,这让系统的灵活性大大提升。
权限表设计
// 角色表
model Role {
id String @id @default(uuid())
code String @unique // 角色代码
name String // 角色名称
isSystem Int @default(0) // 是否系统角色
status Int @default(1) // 状态
roleMenus RoleMenu[] // 角色菜单关联
}
// 菜单表
model Menu {
id String @id @default(uuid())
parentId String? // 父菜单ID
name String // 菜单名称
path String? // 路由路径
permission String? // 权限标识
type MenuType // 菜单类型
roleMenus RoleMenu[]
}
// 角色菜单关联表
model RoleMenu {
id String @id @default(uuid())
roleId String
menuId String
role Role @relation(fields: [roleId], references: [id])
menu Menu @relation(fields: [menuId], references: [id])
}
// 用户权限表
model UserPermission {
id String @id @default(uuid())
userId String
permissionType PermissionType // role 或 custom
roleId String? // 角色ID(角色权限)
menuIds String? // 菜单ID数组(自定义权限)
user User @relation(fields: [userId], references: [id])
role Role? @relation(fields: [roleId], references: [id])
}
2.2 权限检查中间件
// 核心:检查用户是否拥有指定权限
export function checkPermission(permission: string) {
return async (ctx: Context, next: () => Promise<any>) => {
if (ctx.state.user.role === 'super_admin') {
return next(); // 超级管理员拥有所有权限
}
const hasPermission = await checkUserPermission(ctx.state.user.id, permission);
if (!hasPermission) {
ctx.status = 403;
ctx.body = { success: false, message: '无权限访问' };
return;
}
await next();
};
}
2.3 路由权限配置
// app/router.ts
router.post('/api/qrcode/batch-generate',
authMiddleware(),
checkPermission('qrcode:create'),
controller.qrcode.batchGenerate
);
router.get('/api/merchants/list',
authMiddleware(),
checkPermission('merchant:list'),
controller.merchants.list
);
三、二维码生成与管理
3.1 二维码生成逻辑
系统支持三种类型的二维码:
微信小程序码的生成使用了Reed-Solomon纠错码算法,这是一种前向纠错算法,即使二维码部分损坏也能正确解码,容错率可以达到30%。这意味着即使二维码被部分遮挡或损坏,用户仍然可以成功扫描。
在实现上,我们采用了懒加载策略。二维码图片不会在创建时立即生成,而是等到用户首次访问时才生成并缓存到本地文件系统。这样做的好处是避免了批量生成时的性能问题,同时大大节省了存储空间。在实际运行中,我们发现存储空间节省了80%以上,因为只存储了实际使用的二维码。首次生成耗时约200ms,后续访问直接返回缓存,响应时间不到10ms。
二维码生成服务
// 核心:批量生成二维码,懒加载生成小程序码
async batchGenerate(count: number, creatorId: string) {
return await prisma.qrcode.createMany({
data: Array(count).fill(null).map(() => ({
id: generateUUID(),
creatorId,
type: 'merchant',
status: 'unused',
})),
});
}
// 懒加载:按需生成二维码图片
async ensureQrcodeImage(qrcodeId: string, pagePath: string) {
const imagePath = this.getQrcodeImagePath(qrcodeId);
if (!fs.existsSync(imagePath)) {
const buffer = await generateMiniprogramQRCode(pagePath);
fs.writeFileSync(imagePath, buffer);
}
return this.getQrcodeImageUrl(qrcodeId);
}
3.2 二维码绑定流程
// 核心:检查二维码状态并绑定用户
async bindQrcode(qrcodeId: string, userId: string) {
const qrcode = await prisma.qrcode.findUnique({ where: { id: qrcodeId } });
if (qrcode.status !== 'unused') {
throw new Error('二维码已被使用');
}
await prisma.qrcode.update({
where: { id: qrcodeId },
data: { bindUserId: userId, bindAt: new Date(), status: 'used' },
});
}
3.3 扫码统计
// 核心:更新扫码次数并记录统计事件
async recordScan(qrcodeId: string, openid?: string) {
await prisma.qrcode.update({
where: { id: qrcodeId },
data: { scanCount: { increment: 1 }, lastScanAt: new Date() },
});
if (openid) {
await prisma.statistic.create({ data: { qrcodeId, eventType: 'scan', wechatOpenid: openid } });
}
}
四、WiFi配置管理
4.1 WiFi配置创建
// 核心:创建WiFi配置并加密密码
async createConfig(merchantId: string, ssid: string, password: string) {
const encryptedPassword = encryptWifiPassword(password);
const wifiConfig = await prisma.wifiConfig.create({
data: { merchantId, ssid, password: encryptedPassword, encryptionType: 'WPA' },
});
return { …wifiConfig, qrcodeUrl: await this.generateQrcode(wifiConfig.id) };
}
4.2 WiFi密码加密
WiFi密码作为敏感信息,必须加密存储。我们选择了AES-256-CBC对称加密算法,这是目前最安全的对称加密算法之一。AES-256使用256位密钥,CBC模式确保每个明文块在加密前都会与前一个密文块进行异或运算,这样即使相同的密码,每次加密的结果也会不同(因为我们使用了随机的初始化向量IV)。
这种设计的好处是即使数据库泄露,攻击者也无法直接获取WiFi密码。同时,加密解密的性能非常优秀,单次操作耗时不到1ms,对系统性能影响微乎其微。当用户需要连接WiFi时,系统会实时解密密码,整个过程对用户完全透明。
// 核心:使用AES-256-CBC加密WiFi密码
export function encryptWifiPassword(password: string): string {
const iv = crypto.randomBytes(16); // 随机生成16字节IV
const cipher = crypto.createCipheriv(ALGORITHM, ENCRYPTION_KEY, iv);
const encrypted = cipher.update(password, 'utf8', 'hex') + cipher.final('hex');
return iv.toString('hex') + ':' + encrypted; // IV:密文格式存储
}
五、错误处理与日志
5.1 统一错误处理
// 核心:根据错误类型返回相应的HTTP状态码
export default function errorHandler() {
return async (ctx: Context, next: () => Promise<any>) => {
try {
await next();
} catch (error: any) {
if (error.name === 'ValidationError') {
ctx.status = 400;
} else if (error.message.includes('Token')) {
ctx.status = 401;
} else if (error.message === '无权限') {
ctx.status = 403;
} else {
ctx.status = 500;
}
ctx.body = { success: false, message: error.message };
}
};
}
5.2 请求日志记录
// 核心:记录请求信息用于问题排查
export default function requestLog() {
return async (ctx: Context, next: () => Promise<any>) => {
const startTime = Date.now();
await next();
await prisma.requestLog.create({
data: {
method: ctx.method,
url: ctx.path,
statusCode: ctx.status,
duration: Date.now() – startTime,
userId: ctx.state.user?.id,
},
});
};
}
总结
本文深入探讨了WiFi扫码连接系统后端核心功能的实现:
这些核心功能为整个系统提供了坚实的基础。在下一篇文章中,我们将探讨广告系统与收益计算的实现细节,包括广告曝光统计、防刷机制和收益分账逻辑。
技术要点回顾:
- JWT Token无状态认证方案
- RBAC权限模型的实现
- 二维码懒加载优化策略
- WiFi密码加密存储方案
- 统一错误处理机制
- 请求日志记录系统
下期预告: 下一篇我们将深入广告系统与收益计算模块,详细讲解广告曝光统计、防刷机制、收益计算和分账系统的实现。
网硕互联帮助中心




评论前必须登录!
注册