nestjs drizzle-orm 构建rbac权限系统

430 阅读8分钟

使用 NestJS 和 Drizzle ORM 构建最小 RBAC 系统

一个使用 NestJS、Drizzle ORM、MySQL、Redis 等工具构建的角色-based 访问控制(RBAC)系统的实践指南。该项目旨在学习和练习现代后端开发,使用 TypeScript 实现。

引言

最近我一直在深入学习 NestJSNext.js,为了巩固对 NestJS 的理解,我使用 Drizzle ORM 构建了一个最小化的 RBAC(角色-based 访问控制)系统。这个项目是一个实用的学习工具,涵盖了认证、授权和系统管理功能。在本文中,我将带你了解项目的设置、功能、技术栈以及未来的计划,并提供代码片段和入门提示。


项目概述

这个项目是一个轻量级的 RBAC 系统,使用现代工具和框架构建。它包括核心功能,如用户认证、角色管理和菜单权限,是学习 NestJS 和 Drizzle ORM 的绝佳起点。源代码已在 Gitee 上可用,你可以克隆它来探索或扩展。

仓库nestjs-drizzle


前置条件

在开始之前,确保你具备以下条件:

  • Node.js(v16 或更高版本)
  • MySQL 数据库(本地或远程运行)
  • Redis(用于缓存和会话管理)
  • 代码编辑器(例如 VS Code)
  • NestJSTypeScriptDrizzle 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 体验。

Swagger UI


系统功能

RBAC 系统包括以下功能:

  1. JWT 认证:使用 JSON Web Tokens 进行安全登录。
  2. 菜单管理:创建和管理应用的动态菜单。
  3. 部门管理:将用户组织成具有层级结构的部门。
  4. 角色管理:为用户分配具有特定权限的角色。
  5. 日志管理:跟踪用户登录活动和系统事件。
  6. 用户管理:管理用户账户及其角色。
  7. Redis 集成:使用 Redis 缓存数据和管理会话。
  8. 请求速率限制:通过限制 API 请求频率防止滥用。
  9. 邮件通知:使用 SMTP 发送邮件(例如密码重置)。
  10. Swagger 与 Knife4j:增强的 API 文档,便于测试。
  11. 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],
  }),
}));

后续计划

  1. 单点登录:实现管理员踢人下线功能。
  2. 验证码登录:添加验证码验证以增强安全。
  3. 操作日志:记录用户操作以便审计。