使用 NestJS 和 Drizzle ORM 构建最小 RBAC 系统
一个使用 NestJS、Drizzle ORM、MySQL、Redis 等工具构建的角色-based 访问控制(RBAC)系统的实践指南。该项目旨在学习和练习现代后端开发,使用 TypeScript 实现。
引言
最近我一直在深入学习 NestJS 和 Next.js,为了巩固对 NestJS 的理解,我使用 Drizzle ORM 构建了一个最小化的 RBAC(角色-based 访问控制)系统。这个项目是一个实用的学习工具,涵盖了认证、授权和系统管理功能。在本文中,我将带你了解项目的设置、功能、技术栈以及未来的计划,并提供代码片段和入门提示。
项目概述
这个项目是一个轻量级的 RBAC 系统,使用现代工具和框架构建。它包括核心功能,如用户认证、角色管理和菜单权限,是学习 NestJS 和 Drizzle ORM 的绝佳起点。源代码已在 Gitee 上可用,你可以克隆它来探索或扩展。
仓库: nestjs-drizzle
前置条件
在开始之前,确保你具备以下条件:
- Node.js(v16 或更高版本)
- MySQL 数据库(本地或远程运行)
- Redis(用于缓存和会话管理)
- 代码编辑器(例如 VS Code)
- NestJS、TypeScript 和 Drizzle ORM 的基本知识
入门指南
按照以下步骤在本地设置和运行项目:
1. 克隆仓库
git clone https://gitee.com/kang841331654/nestjs-drizzle.git
cd nestjs-drizzle
2. 配置环境变量
在项目根目录创建 .env 文件,并添加以下配置。将占位符(例如 你的数据库密码)替换为实际值。
# 数据库配置
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=你的数据库密码
DB_NAME=你的数据库名称
# Swagger 配置
SWAGGER_ENABLE=true
SWAGGER_PATH=api-docs
SWAGGER_VERSION=1.0
# 应用配置
APP_NAME="NestJS Admin 管理后台"
APP_PORT=3000
NODE_ENV=development
# Redis 配置
REDIS_PORT=6379
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=""
REDIS_DB=0
# SMTP 配置(用于邮件功能)
SMTP_HOST=smtp.163.com
SMTP_PORT=465
SMTP_USER=nest_admin@163.com
SMTP_PASS=VIPLLOIPMETTROYU
3. 安装依赖
选择你喜欢的包管理器安装依赖:
# 使用 pnpm(推荐)
pnpm install
# 或使用 Bun
bun install
# 或使用 Yarn
yarn install
4. 初始化数据库
项目使用 Drizzle ORM 定义和管理数据库 schema。schema 定义在提供的代码中(见数据库 Schema 部分)。初始化数据库:
- 确保 MySQL 服务器正在运行。
- 运行迁移(如果项目中提供)或直接执行 schema 以创建表。
- 或者,仓库中包含预定义的 schema,因此克隆并运行迁移即可。
5. 运行项目
启动 NestJS 应用:
pnpm start:dev
运行后,访问 http://localhost:3000/api-docs 以查看 Swagger API 文档,通过 Knife4j 增强,提供更好的 UI 体验。
系统功能
RBAC 系统包括以下功能:
- JWT 认证:使用 JSON Web Tokens 进行安全登录。
- 菜单管理:创建和管理应用的动态菜单。
- 部门管理:将用户组织成具有层级结构的部门。
- 角色管理:为用户分配具有特定权限的角色。
- 日志管理:跟踪用户登录活动和系统事件。
- 用户管理:管理用户账户及其角色。
- Redis 集成:使用 Redis 缓存数据和管理会话。
- 请求速率限制:通过限制 API 请求频率防止滥用。
- 邮件通知:使用 SMTP 发送邮件(例如密码重置)。
- Swagger 与 Knife4j:增强的 API 文档,便于测试。
- NestJS 事件发射器:发布和处理事件以实现异步操作。
技术栈
项目利用现代技术栈,确保可扩展性和可维护性:
- NestJS:一个渐进式的 Node.js 框架,用于构建高效、可扩展的服务器端应用。
- MySQL:关系型数据库,用于持久化数据存储。
- Redis:内存数据存储,用于缓存和会话管理。
- Drizzle ORM:一个轻量级、TypeScript 优先的 ORM,用于数据库交互。
- Zod:TypeScript 的 schema 验证。
- TypeScript:强类型 JavaScript,提高代码质量。
数据库 Schema
数据库 schema 使用 Drizzle ORM 定义,包括用户、部门、菜单、角色、权限和日志表。以下是参考的 schema 代码:
import {
mysqlTable,
int,
varchar,
char,
timestamp,
datetime,
primaryKey,
index,
} from 'drizzle-orm/mysql-core';
import { sql } from 'drizzle-orm';
import { relations } from 'drizzle-orm';
// 用户表
export const users = mysqlTable('sys_user', {
userId: int('user_id').primaryKey().autoincrement().notNull(),
deptId: int('dept_id'),
userName: varchar('user_name', { length: 30 }).notNull(),
nickName: varchar('nick_name', { length: 30 }).notNull(),
userType: varchar('user_type', { length: 2 }).notNull().default('00'),
email: varchar('email', { length: 50 }).notNull().default(''),
phonenumber: varchar('phonenumber', { length: 11 }).notNull().default(''),
sex: char('sex', { length: 1 }).notNull().default('0'),
password: varchar('password', { length: 200 }).notNull().default(''),
status: char('status', { length: 1 }).notNull().default('0'),
delFlag: char('del_flag', { length: 1 }).notNull().default('0'),
loginIp: varchar('login_ip', { length: 128 }).notNull().default(''),
createBy: varchar('create_by', { length: 64 }).notNull().default(''),
createTime: datetime('create_time', { fsp: 6 }).default(sql`CURRENT_TIMESTAMP(6)`),
updateBy: varchar('update_by', { length: 64 }).notNull().default(''),
updateTime: datetime('update_time', { fsp: 6 }).default(sql`CURRENT_TIMESTAMP(6)`),
remark: varchar('remark', { length: 500 }),
avatar: varchar('avatar', { length: 255 }).notNull().default(''),
loginDate: timestamp('login_date'),
});
// 部门表
export const depts = mysqlTable('sys_dept', {
deptId: int('dept_id').primaryKey().autoincrement().notNull(),
parentId: int('parent_id').notNull().default(0),
ancestors: varchar('ancestors', { length: 50 }).notNull().default('0'),
deptName: varchar('dept_name', { length: 30 }).notNull(),
orderNum: int('order_num').notNull().default(0),
leader: varchar('leader', { length: 20 }).notNull(),
phone: varchar('phone', { length: 11 }).notNull().default(''),
email: varchar('email', { length: 50 }).notNull().default(''),
status: char('status', { length: 1 }).notNull().default('0'),
delFlag: char('del_flag', { length: 1 }).notNull().default('0'),
createBy: varchar('create_by', { length: 64 }).notNull().default(''),
createTime: datetime('create_time', { fsp: 6 }).default(sql`CURRENT_TIMESTAMP(6)`),
updateBy: varchar('update_by', { length: 64 }).notNull().default(''),
updateTime: datetime('update_time', { fsp: 6 }).default(sql`CURRENT_TIMESTAMP(6)`),
remark: varchar('remark', { length: 500 }),
});
// 菜单权限表
export const menus = mysqlTable('sys_menu', {
menuId: int('menu_id').primaryKey().autoincrement().notNull(),
menuName: varchar('menu_name', { length: 50 }).notNull(),
parentId: int('parent_id').notNull(),
orderNum: int('order_num').notNull().default(0),
path: varchar('path', { length: 200 }).notNull().default(''),
component: varchar('component', { length: 255 }),
query: varchar('query', { length: 255 }).notNull().default(''),
isFrame: char('is_frame', { length: 1 }).notNull().default('1'),
isCache: char('is_cache', { length: 1 }).notNull().default('0'),
menuType: char('menu_type', { length: 1 }).notNull().default('M'),
visible: char('visible', { length: 1 }).notNull().default('0'),
status: char('status', { length: 1 }).notNull().default('0'),
perms: varchar('perms', { length: 100 }).notNull().default(''),
icon: varchar('icon', { length: 100 }).notNull().default(''),
createBy: varchar('create_by', { length: 64 }).notNull().default(''),
createTime: datetime('create_time', { fsp: 6 }).default(sql`CURRENT_TIMESTAMP(6)`),
updateBy: varchar('update_by', { length: 64 }).notNull().default(''),
updateTime: datetime('update_time', { fsp: 6 }).default(sql`CURRENT_TIMESTAMP(6)`),
remark: varchar('remark', { length: 500 }),
delFlag: char('del_flag', { length: 1 }).notNull().default('0'),
});
// 角色表
export const roles = mysqlTable('sys_role', {
roleId: int('role_id').primaryKey().autoincrement().notNull(),
roleName: varchar('role_name', { length: 30 }).notNull(),
roleKey: varchar('role_key', { length: 100 }).notNull(), // 角色权限字符串
roleSort: int('role_sort').notNull().default(0),
dataScope: char('data_scope', { length: 1 }).notNull().default('1'), // 数据范围(1:全部数据权限 2:自定数据权限)
menuCheckStrictly: char('menu_check_strictly', { length: 1 }).notNull().default('1'),
deptCheckStrictly: char('dept_check_strictly', { length: 1 }).notNull().default('1'),
status: char('status', { length: 1 }).notNull().default('0'),
delFlag: char('del_flag', { length: 1 }).notNull().default('0'),
createBy: varchar('create_by', { length: 64 }).notNull().default(''),
createTime: datetime('create_time', { fsp: 6 }).default(sql`CURRENT_TIMESTAMP(6)`),
updateBy: varchar('update_by', { length: 64 }).notNull().default(''),
updateTime: datetime('update_time', { fsp: 6 }).default(sql`CURRENT_TIMESTAMP(6)`),
remark: varchar('remark', { length: 500 }),
});
// 权限表
export const permissions = mysqlTable('sys_permission', {
permissionId: int('permission_id').primaryKey().autoincrement().notNull(),
permissionName: varchar('permission_name', { length: 50 }).notNull(),
permissionKey: varchar('permission_key', { length: 100 }).notNull().unique(), // 权限唯一标识
permissionType: char('permission_type', { length: 1 }).notNull().default('B'), // 类型: B-业务, A-API, D-数据
status: char('status', { length: 1 }).notNull().default('0'),
createBy: varchar('create_by', { length: 64 }).notNull().default(''),
createTime: datetime('create_time', { fsp: 6 }).default(sql`CURRENT_TIMESTAMP(6)`),
updateBy: varchar('update_by', { length: 64 }).notNull().default(''),
updateTime: datetime('update_time', { fsp: 6 }).default(sql`CURRENT_TIMESTAMP(6)`),
remark: varchar('remark', { length: 500 }),
});
// 登录日志表
export const loginLogs = mysqlTable('sys_logininfor', {
infoId: int('info_id').primaryKey().autoincrement().notNull(),
userName: varchar('user_name', { length: 50 }).notNull().default(''),
ipaddr: varchar('ipaddr', { length: 128 }).notNull().default(''),
loginLocation: varchar('login_location', { length: 255 }).notNull().default(''),
browser: varchar('browser', { length: 50 }).notNull().default(''),
os: varchar('os', { length: 50 }).notNull().default(''),
status: char('status', { length: 1 }).notNull().default('0'), // 0-成功, 1-失败
msg: varchar('msg', { length: 255 }).notNull().default(''),
loginTime: datetime('login_time', { fsp: 6 }).default(sql`CURRENT_TIMESTAMP(6)`),
}, (t) => ({
userNameIndex: index('idx_user_name').on(t.userName),
statusIndex: index('idx_status').on(t.status),
loginTimeIndex: index('idx_login_time').on(t.loginTime),
}));
// 用户和角色关联表
export const userRoles = mysqlTable('sys_user_role', {
userId: int('user_id').notNull(),
roleId: int('role_id').notNull(),
}, (t) => ({
pk: primaryKey({ columns: [t.userId, t.roleId] }),
}));
// 角色和菜单关联表
export const roleMenus = mysqlTable('sys_role_menu', {
roleId: int('role_id').notNull(),
menuId: int('menu_id').notNull(),
}, (t) => ({
pk: primaryKey({ columns: [t.roleId, t.menuId] }),
}));
// 角色与部门关联表
export const roleDepts = mysqlTable('sys_role_dept', {
roleId: int('role_id').notNull(),
deptId: int('dept_id').notNull(),
}, (t) => ({
pk: primaryKey({ columns: [t.roleId, t.deptId] }),
}));
// 角色和权限关联表
export const rolePermissions = mysqlTable('sys_role_permission', {
roleId: int('role_id').notNull(),
permissionId: int('permission_id').notNull(),
}, (t) => ({
pk: primaryKey({ columns: [t.roleId, t.permissionId] }),
}));
// 定义关系
export const usersRelations = relations(users, ({ many }) => ({
userRoles: many(userRoles),
}));
export const rolesRelations = relations(roles, ({ many }) => ({
userRoles: many(userRoles),
roleMenus: many(roleMenus),
roleDepts: many(roleDepts),
rolePermissions: many(rolePermissions),
}));
export const menusRelations = relations(menus, ({ many }) => ({
roleMenus: many(roleMenus),
}));
export const deptsRelations = relations(depts, ({ many }) => ({
roleDepts: many(roleDepts),
}));
export const permissionsRelations = relations(permissions, ({ many }) => ({
rolePermissions: many(rolePermissions),
}));
export const userRolesRelations = relations(userRoles, ({ one }) => ({
user: one(users, {
fields: [userRoles.userId],
references: [users.userId],
}),
role: one(roles, {
fields: [userRoles.roleId],
references: [roles.roleId],
}),
}));
export const roleMenusRelations = relations(roleMenus, ({ one }) => ({
role: one(roles, {
fields: [roleMenus.roleId],
references: [roles.roleId],
}),
menu: one(menus, {
fields: [roleMenus.menuId],
references: [menus.menuId],
}),
}));
export const roleDeptRelations = relations(roleDepts, ({ one }) => ({
role: one(roles, {
fields: [roleDepts.roleId],
references: [roles.roleId],
}),
dept: one(depts, {
fields: [roleDepts.deptId],
references: [depts.deptId],
}),
}));
export const rolePermissionRelations = relations(rolePermissions, ({ one }) => ({
role: one(roles, {
fields: [rolePermissions.roleId],
references: [roles.roleId],
}),
permission: one(permissions, {
fields: [rolePermissions.permissionId],
references: [permissions.permissionId],
}),
}));
后续计划
- 单点登录:实现管理员踢人下线功能。
- 验证码登录:添加验证码验证以增强安全。
- 操作日志:记录用户操作以便审计。